NoiseNode implemented (octave and persistence hard coded), added servo feedback option to variable

node_mode
Jake 2025-10-21 00:09:09 +08:00
parent dae69f4c0d
commit 17deaaa873
9 changed files with 456 additions and 190 deletions

View File

@ -134,8 +134,8 @@ export class CurveEditor {
this.setCurves([ this.setCurves([
{ {
startPoint: { x: this.valueToX(0), y: this.valueToY(0) }, startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
startPointHandle: { x: this.valueToX(this.timelineLength / 2), y: this.valueToY(0.5) }, startPointHandle: { x: this.valueToX(this.timelineLength * 0.25), y: this.valueToY(0) },
endPointHandle: { x: this.valueToX(this.timelineLength / 2), y: this.valueToY(-0.5) }, endPointHandle: { x: this.valueToX(this.timelineLength * 0.75), y: this.valueToY(0) },
endPoint: { x: this.valueToX(this.timelineLength), y: this.valueToY(0) } endPoint: { x: this.valueToX(this.timelineLength), y: this.valueToY(0) }
} }
]); ]);
@ -149,6 +149,7 @@ export class CurveEditor {
} }
loadCurveSets(curveSets) { loadCurveSets(curveSets) {
this.curveSets = []
this.curveSets = curveSets; this.curveSets = curveSets;
// If selectedMotorID is present in the new set, load its curves // If selectedMotorID is present in the new set, load its curves
@ -158,6 +159,7 @@ export class CurveEditor {
this.setCurves([]); // fallback to empty this.setCurves([]); // fallback to empty
} }
console.log("LOADED"); console.log("LOADED");
console.log()
setSelectedMotor(10); // Global defined in script.js setSelectedMotor(10); // Global defined in script.js
//this.selectAdjacentMotor(1); //this.selectAdjacentMotor(1);
// Optional: update motor selector UI or redraw timeline // Optional: update motor selector UI or redraw timeline
@ -328,6 +330,7 @@ export class CurveEditor {
getMotorPositionAtTime(motorID, timeInFrames) { getMotorPositionAtTime(motorID, timeInFrames) {
if (this.curveSets[motorID] === undefined || this.curveSets[motorID].length === 0) { if (this.curveSets[motorID] === undefined || this.curveSets[motorID].length === 0) {
console.log("THIS");
return null; return null;
} }
const curves = this.curveSets[motorID]; const curves = this.curveSets[motorID];
@ -751,7 +754,7 @@ export class CurveEditor {
this.dragControlPoint(curve, 'startPointHandle', curve.startPointHandle.x, curve.startPointHandle.y, index); this.dragControlPoint(curve, 'startPointHandle', curve.startPointHandle.x, curve.startPointHandle.y, index);
console.log(this.yToExportRange(curve.startPoint.y)); //console.log(this.yToExportRange(curve.startPoint.y));
} else if (key === 'endPoint') { } else if (key === 'endPoint') {
const oldX = curve.endPoint.x; const oldX = curve.endPoint.x;
const oldY = curve.endPoint.y; const oldY = curve.endPoint.y;

View File

@ -1,5 +1,5 @@
export class ServoMotor { export class ServoMotor {
constructor(arg1, arg2, arg3) { constructor(arg1, arg2, arg3, arg4) {
if (Array.isArray(arg1)) { if (Array.isArray(arg1)) {
// Full payload constructor // Full payload constructor
const payload = arg1; const payload = arg1;
@ -36,6 +36,8 @@ export class ServoMotor {
this.ID = arg2; this.ID = arg2;
this.MODEL = arg3 || 'Unknown Model'; this.MODEL = arg3 || 'Unknown Model';
this.NAME = arg4 || "UNKNOWN";
} }
} }
} }

View File

@ -186,6 +186,8 @@
<div id="fileListHeader"> <div id="fileListHeader">
<span>Animations</span> <span>Animations</span>
<div id="fileActions"> <div id="fileActions">
<input type="number" id="repeatCount" min="1" value="1" style="width: 50px; margin-right: 8px;"
title="Repeat count" />
<button id="playFile">Play</button> <button id="playFile">Play</button>
<button id="loadFile" disabled>Load</button> <button id="loadFile" disabled>Load</button>
<button id="deleteFile" disabled>Delete</button> <button id="deleteFile" disabled>Delete</button>
@ -193,6 +195,21 @@
</div> </div>
<ul id="fileList"></ul> <ul id="fileList"></ul>
</div> </div>
<canvas id="nodeeditor" width="800" height="600"></canvas>
<div id="contextMenu" style="
position: absolute;
display: none;
background: #f0f0f0;
border: 1px solid #aaa;
padding: 6px;
font-family: sans-serif;
z-index: 1000;
">
</div>
</div> </div>
@ -204,21 +221,6 @@
</div> </div>
<canvas id="nodeeditor" width="800" height="600"></canvas>
<div id="contextMenu" style="
position: absolute;
display: none;
background: #f0f0f0;
border: 1px solid #aaa;
padding: 6px;
font-family: sans-serif;
z-index: 1000;
">
</div>
<button id="sendNodes">Send Nodes</button>
<textarea id="log" rows="10" cols="60" readonly></textarea><br> <textarea id="log" rows="10" cols="60" readonly></textarea><br>
<input type="text" id="input" placeholder="Type message here"> <input type="text" id="input" placeholder="Type message here">
<button id="send">Send</button> <button id="send">Send</button>

View File

@ -1,5 +1,5 @@
import { ServoNode, CurveNode, VariableNode, MathNode, MapNode, NODE_TYPES } from "./nodes.js" import { ServoNode, CurveNode, VariableNode, NoiseNode, MathNode, MapNode, NODE_TYPES } from "./nodes.js"
export class NodeEditor { export class NodeEditor {
@ -17,6 +17,7 @@ export class NodeEditor {
this.isPanning = false; this.isPanning = false;
this.lastPan = { x: 0, y: 0 }; this.lastPan = { x: 0, y: 0 };
this.zoom = 1.0; this.zoom = 1.0;
this.focusedInput = null;
this._bindEvents(); this._bindEvents();
@ -33,10 +34,20 @@ export class NodeEditor {
this.connections.push({ from: inputNode, to: outputNode }); this.connections.push({ from: inputNode, to: outputNode });
} }
this.addVariableNode(50, 120, "Var"); //this.addVariableNode(50, 120, "Var");
} }
getNodeByID(id) {
return this.nodes.find(node => node.id === id) || null;
}
getAllInputs() {
let inputs = [];
for (var i = 0; i < this.nodes.length; i++) {
inputs.push(...this.nodes[i].inputs);
}
return inputs;
}
encodeNodeGraph() { encodeNodeGraph() {
const bufferSize = 1024; // adjust based on expected graph size const bufferSize = 1024; // adjust based on expected graph size
@ -70,25 +81,20 @@ export class NodeEditor {
break; break;
case NODE_TYPES.Noise: case NODE_TYPES.Noise:
view.setFloat32(offset, node.rate, true); offset += 4; view.setFloat32(offset, node.inputFrequency.numericValue, true); offset += 4;
view.setFloat32(offset, node.threshold, true); offset += 4; view.setUint16(offset, node.inputSeed.numericValue); offset += 2;
view.setFloat32(offset, node.pulseWidth, true); offset += 4;
view.setFloat32(offset, node.amplitude, true); offset += 4;
view.setUint8(offset++, node.seed);
break; break;
case NODE_TYPES.Variable: case NODE_TYPES.Variable:
view.setUint8(offset++, node.variableDropdown.selectedIndex); view.setUint8(offset++, node.variableDropdown.selectedIndex);
console.log(node.variableDropdown.selectedIndex); view.setUint8(offset++, node.arg0Input.numericValue);
break; break;
case NODE_TYPES.Math: case NODE_TYPES.Math:
view.setUint8(offset++, node.operatorDropdown.selectedIndex); view.setUint8(offset++, node.operatorDropdown.selectedIndex);
view.setFloat32(offset, node.valueInput.numericValue, true); offset += 4; view.setFloat32(offset, node.valueInput.numericValue, true); offset += 4;
console.log(node.operatorDropdown.selectedIndex);
console.log(node.valueInput.numericValue);
break; break;
case NODE_TYPES.Map: case NODE_TYPES.Map:
@ -117,6 +123,109 @@ export class NodeEditor {
return new Uint8Array(buffer.slice(0, offset)); return new Uint8Array(buffer.slice(0, offset));
} }
loadFromBinary(data) {
this.nodes = []
console.log(this.connections);
this.connections = []
const view = new DataView(data.buffer);
let offset = 0;
const nodeCount = view.getUint8(offset++);
const idMap = {}; // Map node IDs to actual node instances
for (let i = 0; i < nodeCount; i++) {
const type = view.getUint8(offset++);
const id = view.getUint8(offset++);
const x = view.getUint16(offset, true); offset += 2;
const y = view.getUint16(offset, true); offset += 2;
let node = null;
switch (type) {
case NODE_TYPES.Servo: {
const motorID = view.getUint8(offset++);
node = this.addServoNode(x, y, "Servo Output", motorID);
break;
}
case NODE_TYPES.Curve: {
const curveID = view.getUint8(offset++);
node = this.addCurveNode(x, y, "Curve", curveID);
break;
}
case NODE_TYPES.Noise: {
const frequency = view.getFloat32(offset, true); offset += 4;
const seed = view.getUint16(offset, true); offset += 2;
node = this.addNoiseNode(x, y, "Noise");
node.inputFrequency.value = frequency;
node.inputSeed.value = seed;
break;
}
case NODE_TYPES.Variable: {
const source = view.getUint8(offset++);
const arg0 = view.getUint8(offset++);
node = this.addVariableNode(x, y, "Variable");
node.variableDropdown.selectedIndex = source;
node.arg0Input.value = arg0;
break;
}
case NODE_TYPES.Math: {
const op = view.getUint8(offset++);
const value = view.getFloat32(offset, true); offset += 4;
node = this.addMathNode(x, y, "Math");
console.log(op, value);
node.operatorDropdown.selectedIndex = op;
node.valueInput.value = value;
break;
}
case NODE_TYPES.Map: {
const inMin = view.getFloat32(offset, true); offset += 4;
const inMax = view.getFloat32(offset, true); offset += 4;
const outMin = view.getFloat32(offset, true); offset += 4;
const outMax = view.getFloat32(offset, true); offset += 4;
node = this.addMapNode(x, y, "Map");
node.inMinInput.value = inMin;
node.inMaxInput.value = inMax;
node.outMinInput.value = outMin;
node.outMaxInput.value = outMax;
break;
}
default: {
node = this.addNode(x, y, "Unknown Node");
break;
}
}
if (node) {
node.id = id;
idMap[id] = node;
}
}
console.log(this.getNodeByID(0));
// 🔗 Load connections
this.connection = [];
const connectionCount = view.getUint8(offset++);
for (let i = 0; i < connectionCount; i++) {
const fromID = view.getUint8(offset++);
const toID = view.getUint8(offset++);
const fromNode = idMap[fromID];
const toNode = idMap[toID];
if (fromNode && toNode) {
this.connections.push({ from: fromNode, to: toNode });
}
}
// Reencode all positions to SIGNED ints
this.nodes.forEach(node => {
node.x = (node.x << 16) >> 16;
node.y = (node.y << 16) >> 16;
});
this._redraw();
}
addNode(x, y, label, options = {}) { addNode(x, y, label, options = {}) {
@ -199,6 +308,7 @@ export class NodeEditor {
} }
_onMouseDown(e) { _onMouseDown(e) {
const { x, y } = this._getMouse(e); const { x, y } = this._getMouse(e);
@ -210,6 +320,16 @@ export class NodeEditor {
return; return;
} }
this.focusedInput = null;
let allInputs = this.getAllInputs();
for (const input of allInputs) {
if (input.handleClick(x, y)) {
this.focusedInput = input;
} else {
input.focused = false;
}
}
for (const node of this.nodes) { for (const node of this.nodes) {
if (node.contains(x, y)) { if (node.contains(x, y)) {
@ -312,16 +432,30 @@ export class NodeEditor {
_onDoubleClick(e) { _onDoubleClick(e) {
const { x, y } = this._getMouse(e); const { x, y } = this._getMouse(e);
// First: check for wire deletion
for (let i = this.connections.length - 1; i >= 0; i--) { for (let i = this.connections.length - 1; i >= 0; i--) {
const conn = this.connections[i]; const conn = this.connections[i];
if (this._isPointNearLine(x, y, conn.from.output.x, conn.from.output.y, conn.to.input.x, conn.to.input.y)) { if (this._isPointNearLine(x, y, conn.from.output.x, conn.from.output.y, conn.to.input.x, conn.to.input.y)) {
this.connections.splice(i, 1); this.connections.splice(i, 1);
this._redraw(); this._redraw();
break; return;
}
}
// Second: check for node deletion
for (let i = this.nodes.length - 1; i >= 0; i--) {
const node = this.nodes[i];
if (node.contains(x, y) && node.canDelete) {
// Remove connections to/from this node
this.connections = this.connections.filter(conn => conn.from !== node && conn.to !== node);
this.nodes.splice(i, 1);
this._redraw();
return;
} }
} }
} }
_getMouse(e) { _getMouse(e) {
const rect = this.canvas.getBoundingClientRect(); const rect = this.canvas.getBoundingClientRect();
const screenX = e.clientX - rect.left; const screenX = e.clientX - rect.left;
@ -419,6 +553,15 @@ export class NodeEditor {
hideMenu(); hideMenu();
}; };
menu.appendChild(item); menu.appendChild(item);
item = document.createElement("div");
item.className = "menu-item";
item.textContent = " Add Noise Node";
item.onclick = () => {
this.addNoiseNode(canvasX, canvasY, "Noise");
hideMenu();
};
menu.appendChild(item);
} }

View File

@ -66,7 +66,7 @@ export class CanvasTextInput {
if (!this.focused) return false; if (!this.focused) return false;
console.log(e); console.log(e);
if (e.key === "Backspace") { if (e.key === "Backspace") {
this.value = this.value.slice(0, -1); this.value = String(this.value).slice(0, -1);
} else if (e.key === "Enter") { } else if (e.key === "Enter") {
this.clampToRange(); // 👈 clamp on Enter this.clampToRange(); // 👈 clamp on Enter
this.focused = false; this.focused = false;

View File

@ -27,6 +27,9 @@ export class Node {
this.hasInput = false; this.hasInput = false;
this.hasOutput = false; this.hasOutput = false;
this.canDelete = true;
this.inputs = [];
this.updatePorts(); this.updatePorts();
} }
@ -120,6 +123,7 @@ export class ServoNode extends Node {
this.hasInput = true; this.hasInput = true;
this.hasOutput = false; this.hasOutput = false;
this.canDelete = false;
} }
draw(ctx) { draw(ctx) {
@ -170,6 +174,7 @@ export class CurveNode extends Node {
this.hasInput = false; this.hasInput = false;
this.hasOutput = true; this.hasOutput = true;
this.canDelete = false;
} }
draw(ctx) { draw(ctx) {
@ -224,6 +229,8 @@ export class InputNode extends Node {
{ defaultValue: 1.0, numericOnly: true, min: 0, max: 10 } { defaultValue: 1.0, numericOnly: true, min: 0, max: 10 }
); );
this.inputs.push(this.inputField);
} }
@ -277,129 +284,88 @@ export class NoiseNode extends Node {
constructor(x, y, label = "Noise Generator") { constructor(x, y, label = "Noise Generator") {
super(x, y, label); super(x, y, label);
this.type = NODE_TYPES.Noise; this.type = NODE_TYPES.Noise;
this.width = 160; this.width = 180;
this.height = 280; this.height = 180;
this.modeDropdown = new CanvasDropdown( this.inputFrequency = new CanvasTextInput(10, 45, this.width - 20, "1.0", "Frequency", { mode: "float" });
10, 35, this.width - 20, this.inputSeed = new CanvasTextInput(10, 70 + 10, this.width - 20, "0", "Seed", { mode: "int" });
["impulse", "pulse", "threshold", "smooth"],
"impulse"
);
const inputConfigs = [ this.inputs.push(this.inputFrequency);
{ label: "Rate", value: "1.0", min: 0, max: 100 }, this.inputs.push(this.inputSeed);
{ label: "Threshold", value: "0.8", min: 0, max: 1 },
{ label: "Pulse Width", value: "0.2", min: 0, max: 10 },
{ label: "Amplitude", value: "1.0", min: 0, max: 10 },
{ label: "Seed", value: "0", min: 0, max: 99999 }
];
this.inputs = [];
const inputSpacing = 42; // includes label + input height this.hasInput = false;
this.hasOutput = true;
for (let i = 0; i < inputConfigs.length; i++) {
const cfg = inputConfigs[i];
const inputY = 74 + i * inputSpacing;
this.inputs.push(new CanvasTextInput(
10,
inputY,
undefined, // uses default width
cfg.value,
cfg.label,
{
numericOnly: true,
min: cfg.min,
max: cfg.max
}
));
}
this.lastValue = 0;
this.timer = 0;
} }
draw(ctx) { draw(ctx) {
ctx.fillStyle = this.color || "#f0e6ff"; let max = 0
ctx.strokeStyle = this.border || "#333"; super.draw(ctx);
ctx.lineWidth = 1; for (const input of [this.inputFrequency, this.inputSeed]) {
this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10);
ctx.fill();
ctx.stroke();
// Draw Label
ctx.fillStyle = "#000";
ctx.font = "14px sans-serif";
ctx.fillText(this.label, this.x + 10, this.y + 20);
// Draw inputs
for (let i = 0; i < this.inputs.length; i++) {
const input = this.inputs[i];
input.x = this.x + input.offsetX; input.x = this.x + input.offsetX;
input.y = this.y + input.offsetY; // 👈 vertical stacking input.y = this.y + input.offsetY;
input.draw(ctx); input.draw(ctx);
} }
// 🔹 Noise preview
const freq = Math.max(0.01, this.inputFrequency.numericValue);
const seed = this.inputSeed.numericValue;
const steps = 480;
const previewHeight = 60;
const previewWidth = this.width - 20;
const startX = this.x + 10;
const startY = this.y + this.height - previewHeight - 10;
// Draw output port
this.updatePorts();
ctx.beginPath(); ctx.beginPath();
ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); for (let i = 0; i < steps; i++) {
ctx.fillStyle = "#888"; const t = i / steps * freq * 10; // scale up to get more variation
ctx.fill();
const noise = perlin1D_octave(seed, t);
if (noise > max) {
max = noise;
//console.log(noise);
}
const x = startX + (i / steps) * previewWidth;
const y = startY + previewHeight / 2 - noise * (previewHeight / 2);
// Draw dropdowns LAST if (i === 0) ctx.moveTo(x, y);
this.modeDropdown.x = this.x + this.modeDropdown.offsetX; else ctx.lineTo(x, y);
this.modeDropdown.y = this.y + this.modeDropdown.offsetY; }
this.modeDropdown.draw(ctx); ctx.strokeStyle = "#444";
ctx.stroke();
} }
contains(x, y) { contains(x, y) {
return super.contains(x, y) || return super.contains(x, y) || [this.inputFrequency, this.inputSeed].some(i => i.contains(x, y));
this.modeDropdown.contains(x, y) ||
this.inputs.some(input => input.contains(x, y));
}
handleClick(mx, my) {
return this.modeDropdown.handleClick(mx, my) ||
this.inputs.some(input => input.handleClick(mx, my));
} }
handleKey(e) { handleKey(e) {
return this.inputs.some(input => input.handleKey(e)); return (
this.inputFrequency.handleKey(e) ||
this.inputSeed.handleKey(e)
);
} }
handleClick(mx, my) {
return false;//[this.inputFrequency, this.inputSeed].some(i => i.handleClick(mx, my));
}
handleMouseMove(mx, my) { handleMouseMove(mx, my) {
return this.modeDropdown.handleMouseMove(mx, my); return [this.inputFrequency, this.inputSeed].some(i => i.handleMouseMove(mx, my));
} }
update(dt) { update(dt) {
let needsRedraw = this.modeDropdown.update(dt); return [this.inputFrequency, this.inputSeed].some(i => i.update(dt));
for (const input of this.inputs) {
if (input.update(dt)) needsRedraw = true;
}
// Basic noise generation logic (placeholder)
const mode = this.modeDropdown.selected;
const rate = this.inputs[0].numericValue;
const threshold = this.inputs[1].numericValue;
const pulseWidth = this.inputs[2].numericValue;
const amplitude = this.inputs[3].numericValue;
const seed = this.inputs[4].numericValue;
// Simple impulse logic for now
if (mode === "impulse") {
this.lastValue = Math.random() < rate * (dt / 1000) ? amplitude : 0;
}
return needsRedraw;
} }
get outputValue() { get outputValue() {
return this.lastValue; const input = this.inputNode?.outputValue ?? 0;
const inFrequency = this.inputFrequency.numericValue;
const inSeed = this.inputSeed.numericValue;
return 0;
} }
} }
@ -412,10 +378,12 @@ export class VariableNode extends Node {
this.variableDropdown = new CanvasDropdown( this.variableDropdown = new CanvasDropdown(
10, 45, this.width - 20, 10, 45, this.width - 20,
["faceDetectX", "faceDetectY", "sine", "analogRead()"], ["faceDetectX", "faceDetectY", "sine", "analogRead()", "servo"],
"sine" "sine"
); );
this.arg0Input = new CanvasTextInput(10, 75, this.width - 20, "0", "Motor ID", { mode: "int" });
this.inputs.push(this.variableDropdown);
this.inputs.push(this.arg0Input);
this.hasInput = false; this.hasInput = false;
this.hasOutput = true; this.hasOutput = true;
@ -428,40 +396,42 @@ export class VariableNode extends Node {
// Dropdown label // Dropdown label
ctx.fillText("Source", this.x + 10, this.y + 42); ctx.fillText("Source", this.x + 10, this.y + 42);
if (this.variableDropdown.selected === "servo") {
this.arg0Input.x = this.x + this.arg0Input.offsetX;
this.arg0Input.y = this.y + this.arg0Input.offsetY;
this.arg0Input.draw(ctx);
}
// Dropdown // Dropdown
this.variableDropdown.x = this.x + this.variableDropdown.offsetX; this.variableDropdown.x = this.x + this.variableDropdown.offsetX;
this.variableDropdown.y = this.y + this.variableDropdown.offsetY; this.variableDropdown.y = this.y + this.variableDropdown.offsetY;
this.variableDropdown.draw(ctx); this.variableDropdown.draw(ctx);
} }
contains(x, y) { contains(x, y) {
return super.contains(x, y) || this.variableDropdown.contains(x, y); return super.contains(x, y) || [this.variableDropdown, this.arg0Input].some(i => i.contains(x, y));
} }
handleKey(e) {
return (
this.arg0Input.handleKey(e)
);
}
handleClick(mx, my) { handleClick(mx, my) {
return this.variableDropdown.handleClick(mx, my); return false;//[this.inputFrequency, this.inputSeed].some(i => i.handleClick(mx, my));
} }
handleMouseMove(mx, my) { handleMouseMove(mx, my) {
return this.variableDropdown.handleMouseMove(mx, my); return [this.variableDropdown, this.arg0Input].some(i => i.handleMouseMove(mx, my));
} }
update(dt) { update(dt) {
const changed = this.variableDropdown.update(dt); return [this.variableDropdown, this.arg0Input].some(i => i.update(dt));
// Simulated variable values (replace with real data source)
const selected = this.variableDropdown.selected;
if (selected === "faceDetectX") {
this.lastValue = Math.random() * 640; // simulate X position
} else if (selected === "faceDetectY") {
this.lastValue = Math.random() * 480; // simulate Y position
} else if (selected === "sine") {
this.timer += dt;
this.lastValue = Math.sin(this.timer / 1000);
}
return changed;
} }
get outputValue() { get outputValue() {
@ -474,7 +444,7 @@ export class MathNode extends Node {
super(x, y, label); super(x, y, label);
this.type = NODE_TYPES.Math; this.type = NODE_TYPES.Math;
this.width = 160; this.width = 160;
this.height = 100; this.height = 110;
this.operatorDropdown = new CanvasDropdown( this.operatorDropdown = new CanvasDropdown(
10, 45, this.width - 20, 10, 45, this.width - 20,
@ -483,6 +453,8 @@ export class MathNode extends Node {
); );
this.valueInput = new CanvasTextInput(10, 75, this.width - 20, "1.0", "Value", { mode: "float" }); this.valueInput = new CanvasTextInput(10, 75, this.width - 20, "1.0", "Value", { mode: "float" });
this.inputs.push(this.operatorDropdown);
this.inputs.push(this.valueInput);
this.hasInput = true; this.hasInput = true;
this.hasOutput = true; this.hasOutput = true;
@ -558,9 +530,14 @@ export class MapNode extends Node {
this.height = 180; this.height = 180;
this.inMinInput = new CanvasTextInput(10, 45, this.width - 20, "0", "In Min", { mode: "float" }); this.inMinInput = new CanvasTextInput(10, 45, this.width - 20, "0", "In Min", { mode: "float" });
this.inMaxInput = new CanvasTextInput(10, 70+10, this.width - 20, "1023", "In Max", { mode: "float" }); this.inMaxInput = new CanvasTextInput(10, 70 + 10, this.width - 20, "4095", "In Max", { mode: "float" });
this.outMinInput = new CanvasTextInput(10, 95+20, this.width - 20, "0", "Out Min", { mode: "float" }); this.outMinInput = new CanvasTextInput(10, 95 + 20, this.width - 20, "1024", "Out Min", { mode: "float" });
this.outMaxInput = new CanvasTextInput(10, 120+30, this.width - 20, "255", "Out Max", { mode: "float" }); this.outMaxInput = new CanvasTextInput(10, 120 + 30, this.width - 20, "3072", "Out Max", { mode: "float" });
this.inputs.push(this.inMinInput);
this.inputs.push(this.inMaxInput);
this.inputs.push(this.outMinInput);
this.inputs.push(this.outMaxInput);
this.hasInput = true; this.hasInput = true;
this.hasOutput = true; this.hasOutput = true;
@ -580,13 +557,13 @@ export class MapNode extends Node {
} }
handleKey(e) { handleKey(e) {
return ( return (
this.inMinInput.handleKey(e) || this.inMinInput.handleKey(e) ||
this.inMaxInput.handleKey(e) || this.inMaxInput.handleKey(e) ||
this.outMinInput.handleKey(e) || this.outMinInput.handleKey(e) ||
this.outMaxInput.handleKey(e) this.outMaxInput.handleKey(e)
); );
} }
handleClick(mx, my) { handleClick(mx, my) {
@ -614,3 +591,78 @@ export class MapNode extends Node {
} }
const fade = t => t * t * t * (t * (t * 6 - 15) + 10);
const lerp = (a, b, t) => a + t * (b - a);
function grad(hash, x) {
const h = hash & 15;
let grad = 1 + (h & 7); // 1 to 8
if (h & 8) grad = -grad;
return (grad * x) / 4;
//return (hash & 1 ? -1 : 1) * x;
}
const perlinCache = new Map();
function perlin1D_octave(seed, x, octaves = 2, persistence = 0.5) {
let total = 0;
let amplitude = 1;
let frequency = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
total += perlin1D(seed, x * frequency) * amplitude;
maxValue += amplitude;
amplitude *= persistence;
frequency *= 2;
}
return total / maxValue;
}
function perlin1D(seed, x) {
let seedCache = perlinCache.get(seed);
if (!seedCache) {
seedCache = new Map();
perlinCache.set(seed, seedCache);
}
const key = Math.round(x * 1000) / 1000; // round to reduce precision noise
if (seedCache.has(key)) {
return seedCache.get(key);
}
const perm = generatePermutation(seed);
const xi = Math.floor(x) & 255;
const xf = x - Math.floor(x);
const u = fade(xf);
const a = perm[xi];
const b = perm[xi + 1];
const result = lerp(grad(a, xf), grad(b, xf - 1), u);
seedCache.set(key, result);
return result;
}
function generatePermutation(seed) {
const perm = new Array(512);
const p = new Array(256);
let s = seed;
for (let i = 0; i < 256; i++) {
s = (s * 1664525 + 1013904223) % 4294967296;
p[i] = i;
}
for (let i = 255; i > 0; i--) {
const j = s % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
s = (s * 1664525 + 1013904223) % 4294967296;
}
for (let i = 0; i < 512; i++) {
perm[i] = p[i & 255];
}
return perm;
}

View File

@ -2,8 +2,8 @@ import { ServoMotor } from './feetechDefinitions.js';
export class Robot { export class Robot {
constructor(name, firmwareVersionId) { constructor(name, firmwareVersionId) {
this.name = name; this.deviceName = name;
this.firmwareVersionId = firmwareVersionId; this.firmwareVersion = { major: 0, minor: 0 };
// Map of position ID → ServoMotor // Map of position ID → ServoMotor
this.positionMap = new Map(); this.positionMap = new Map();
@ -43,4 +43,41 @@ export class Robot {
} }
return null; return null;
} }
static fromBytes(bytes) {
console.log("Loading Robot from:");
console.log(bytes);
const data = bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes);
let offset = 0;
// Device name
const nameLen = data[offset++];
const deviceName = String.fromCharCode(...data.slice(offset, offset + nameLen));
offset += nameLen;
// Firmware version
const firmwareMajor = data[offset++];
const firmwareMinor = data[offset++];
// Create Robot instance
const robot = new Robot(deviceName, `${firmwareMajor}.${firmwareMinor}`);
robot.firmwareVersion.major = firmwareMajor;
robot.firmwareVersion.minor = firmwareMinor;
// Motor count
const motorCount = data[offset++];
for (let i = 0; i < motorCount; i++) {
const motorID = data[offset++];
const nameLen = data[offset++];
const name = String.fromCharCode(...data.slice(offset, offset + nameLen));
offset += nameLen;
const position = (data[offset++] << 8) | data[offset++];
const motor = new ServoMotor(0, motorID, null, name);
robot.assignMotor(motorID, motor); // motorID used as position ID
}
return robot;
};
} }

View File

@ -36,7 +36,7 @@ window.onload = () => {
const dialColors = ['red', 'green', 'blue', 'orange', 'purple']; const dialColors = ['red', 'green', 'blue', 'orange', 'purple'];
const dials = []; let dials = [];
const frameSlider = document.getElementById('frameSlider'); const frameSlider = document.getElementById('frameSlider');
const frameDisplay = document.getElementById('frameDisplay'); const frameDisplay = document.getElementById('frameDisplay');
@ -56,24 +56,13 @@ window.onload = () => {
let selectedFile = null; let selectedFile = null;
let connectedRobot = GenerateTestRobot(); let connectedRobot = null;//GenerateTestRobot();
console.log(connectedRobot);
let motorIDList = []
for (const [position, motor] of connectedRobot.positionMap.entries()) {
console.log(`Assigning ${position} motor with ID ${motor.ID}`);
curveEditor.addChannel(motor.ID);
motorIDList.push(motor.ID);
}
setSelectedMotor(10);
const nodeCanvas = document.getElementById("nodeeditor"); const nodeCanvas = document.getElementById("nodeeditor");
let nodeEditor = null;
const nodeEditor = new NodeEditor(nodeCanvas, {
motorIds: motorIDList
});
nodeEditor.generateDefaultNodes(curveEditor.curveSets, motorIDList);
//nodeEditor.addServoNode(400, 150, "Servo Output", 5 ); //nodeEditor.addServoNode(400, 150, "Servo Output", 5 );
// nodeEditor.addInputNode(100, 500, "Input Nod", { defaultValue: 3 }); // nodeEditor.addInputNode(100, 500, "Input Nod", { defaultValue: 3 });
@ -83,6 +72,26 @@ window.onload = () => {
// nodeEditor.addNode(100, 100, "Time", { fill: "#e0f7e9", stroke: "#2e7d32" }); // mint green // nodeEditor.addNode(100, 100, "Time", { fill: "#e0f7e9", stroke: "#2e7d32" }); // mint green
// nodeEditor.addNode(300, 200, "Output"); // uses default pastel // nodeEditor.addNode(300, 200, "Output"); // uses default pastel
function onConnectRobot(robot) {
connectedRobot = robot;
console.log(connectedRobot);
let motorIDList = []
clearDials();
for (const [position, motor] of connectedRobot.positionMap.entries()) {
console.log(`Assigning ${position} motor with ID ${motor.ID}`);
curveEditor.addChannel(motor.ID);
motorIDList.push(motor.ID);
addDial(motor.ID, motor.NAME);
}
setSelectedMotor(10);
nodeEditor = new NodeEditor(nodeCanvas, {
motorIds: motorIDList
});
nodeEditor.generateDefaultNodes(curveEditor.curveSets, motorIDList);
}
function setSelectedMotor(motorID) { function setSelectedMotor(motorID) {
@ -166,8 +175,8 @@ window.onload = () => {
const filenameBytes = new TextEncoder().encode(filename); const filenameBytes = new TextEncoder().encode(filename);
const filenameLength = filenameBytes.length; const filenameLength = filenameBytes.length;
// Total size: 2 bytes for length + filename bytes // Total size: 2 bytes for length + filename bytes + oneshot/loop tag + loopCount
const buffer = new ArrayBuffer(2 + filenameLength); const buffer = new ArrayBuffer(2 + filenameLength + 2);
const view = new DataView(buffer); const view = new DataView(buffer);
let offset = 0; let offset = 0;
@ -175,6 +184,16 @@ window.onload = () => {
view.setUint16(offset, filenameLength, true); offset += 2; view.setUint16(offset, filenameLength, true); offset += 2;
filenameBytes.forEach(byte => view.setUint8(offset++, byte)); filenameBytes.forEach(byte => view.setUint8(offset++, byte));
const ONESHOT = 0x01; // play once
const LOOP = 0x02; // loop endlessly
const REPEAT = 0x03; // followed by loop count
const repeatCount = parseInt(document.getElementById("repeatCount").value, 10);
let playTag = REPEAT;
view.setUint8(offset++, playTag);
view.setUint8(offset++, repeatCount);
const payload = new Uint8Array(buffer); const payload = new Uint8Array(buffer);
//serial.deleteFile(payload); // CMD_DELETE_FILE //serial.deleteFile(payload); // CMD_DELETE_FILE
@ -230,7 +249,14 @@ window.onload = () => {
}; };
function addDial(motorID) { function clearDials(){
const dialArea = document.getElementById('dialArea');
dialArea.innerHTML = ''; // Remove all child elements
dials = [];
}
function addDial(motorID, motorName) {
const index = dials.length; const index = dials.length;
// Create dial wrapper // Create dial wrapper
@ -240,8 +266,10 @@ window.onload = () => {
// Create label // Create label
const label = document.createElement('label'); const label = document.createElement('label');
label.textContent = "Motor " + motorID; label.textContent = "MotorID " + motorID;
console.log(motorID);
const label2 = document.createElement('label2');
label2.textContent = motorName;
// Create dial container // Create dial container
const dialDiv = document.createElement('div'); const dialDiv = document.createElement('div');
@ -254,6 +282,7 @@ window.onload = () => {
// Assemble and append // Assemble and append
dialWrapper.appendChild(label); dialWrapper.appendChild(label);
dialWrapper.appendChild(label2);
dialWrapper.appendChild(dialDiv); dialWrapper.appendChild(dialDiv);
dialWrapper.appendChild(valueSpan); dialWrapper.appendChild(valueSpan);
document.getElementById('dialArea').appendChild(dialWrapper); document.getElementById('dialArea').appendChild(dialWrapper);
@ -286,6 +315,7 @@ window.onload = () => {
dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame); dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame);
} }
} }
@ -341,6 +371,8 @@ window.onload = () => {
switch (command) { switch (command) {
case 0x01: // ID response case 0x01: // ID response
text = new TextDecoder().decode(new Uint8Array(payload)); text = new TextDecoder().decode(new Uint8Array(payload));
onConnectRobot(Robot.fromBytes(new Uint8Array(payload)));
console.log(connectedRobot);
document.getElementById('log').value += `ID Response: ${text}\n`; document.getElementById('log').value += `ID Response: ${text}\n`;
break; break;
@ -499,8 +531,7 @@ window.onload = () => {
console.log(raw); console.log(raw);
for (let i = 0; i < curveCount; i++) { for (let i = 0; i < curveCount; i++) {
const motorID = 10; const motorID = view.getUint8(offset++);
offset += 1;
const startTime = view.getUint16(offset, true); offset += 2; const startTime = view.getUint16(offset, true); offset += 2;
const endTime = view.getUint16(offset, true); offset += 2; const endTime = view.getUint16(offset, true); offset += 2;
const startPointY = view.getInt16(offset, true); offset += 2; const startPointY = view.getInt16(offset, true); offset += 2;
@ -547,11 +578,13 @@ window.onload = () => {
console.log("🎯 Loaded Curves:", curveSets); console.log("🎯 Loaded Curves:", curveSets);
// 🔁 Inject into your curve editor
//loadCurvesIntoEditor(curves); // Replace with your actual editor hook
curveEditor.loadCurveSets(curveSets); curveEditor.loadCurveSets(curveSets);
curveEditor.setLength(latestEndTime); curveEditor.setLength(latestEndTime);
if (offset < view.byteLength) {
const nodeGraphData = raw.slice(offset); // grab remaining bytes
nodeEditor.loadFromBinary(nodeGraphData); // call your editor's loader
}
// 🔓 Unlock buttons // 🔓 Unlock buttons
loadButton.disabled = false; loadButton.disabled = false;
@ -675,12 +708,6 @@ window.onload = () => {
document.getElementById('input').value = ''; document.getElementById('input').value = '';
}; };
document.getElementById('sendNodes').onclick = async () => {
nodeGraphPacket = nodeEditor.encodeNodeGraph();
curvePacket = curveEditor.encodeCurves()
};
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS']; const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];
@ -948,7 +975,7 @@ window.onload = () => {
console.log("ERROR: INCORRECT PACKET SIZE: " + payload.length); console.log("ERROR: INCORRECT PACKET SIZE: " + payload.length);
return; return;
} }
const motor = new ServoMotor(payload); const motor = new ServoMotor(Array.from(payload));
console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED); console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED);
servoMotors[motor.CHANNEL].push(motor); servoMotors[motor.CHANNEL].push(motor);

View File

@ -26,7 +26,7 @@ body {
#fileListWrapper { #fileListWrapper {
width: 300px; width: 400px;
max-height: 200px; max-height: 200px;
overflow-y: auto; overflow-y: auto;
background-color: #f0f4ff; background-color: #f0f4ff;