diff --git a/curveEditor.js b/curveEditor.js
index f312ff9..489c192 100644
--- a/curveEditor.js
+++ b/curveEditor.js
@@ -134,8 +134,8 @@ export class CurveEditor {
this.setCurves([
{
startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
- startPointHandle: { x: this.valueToX(this.timelineLength / 2), y: this.valueToY(0.5) },
- endPointHandle: { 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 * 0.75), y: this.valueToY(0) },
endPoint: { x: this.valueToX(this.timelineLength), y: this.valueToY(0) }
}
]);
@@ -149,6 +149,7 @@ export class CurveEditor {
}
loadCurveSets(curveSets) {
+ this.curveSets = []
this.curveSets = curveSets;
// If selectedMotorID is present in the new set, load its curves
@@ -158,6 +159,7 @@ export class CurveEditor {
this.setCurves([]); // fallback to empty
}
console.log("LOADED");
+ console.log()
setSelectedMotor(10); // Global defined in script.js
//this.selectAdjacentMotor(1);
// Optional: update motor selector UI or redraw timeline
@@ -328,6 +330,7 @@ export class CurveEditor {
getMotorPositionAtTime(motorID, timeInFrames) {
if (this.curveSets[motorID] === undefined || this.curveSets[motorID].length === 0) {
+ console.log("THIS");
return null;
}
const curves = this.curveSets[motorID];
@@ -751,7 +754,7 @@ export class CurveEditor {
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') {
const oldX = curve.endPoint.x;
const oldY = curve.endPoint.y;
diff --git a/feetechDefinitions.js b/feetechDefinitions.js
index c98e5e9..38aa760 100644
--- a/feetechDefinitions.js
+++ b/feetechDefinitions.js
@@ -1,5 +1,5 @@
export class ServoMotor {
- constructor(arg1, arg2, arg3) {
+ constructor(arg1, arg2, arg3, arg4) {
if (Array.isArray(arg1)) {
// Full payload constructor
const payload = arg1;
@@ -36,6 +36,8 @@ export class ServoMotor {
this.ID = arg2;
this.MODEL = arg3 || 'Unknown Model';
+
+ this.NAME = arg4 || "UNKNOWN";
}
}
}
diff --git a/index.html b/index.html
index 4e46cf6..f84822a 100644
--- a/index.html
+++ b/index.html
@@ -186,6 +186,8 @@
+
+
+
+
+
+
@@ -204,21 +221,6 @@
-
-
-
-
-
-
diff --git a/nodeeditor/NodeEditor.js b/nodeeditor/NodeEditor.js
index 8b8df9a..c85a6c2 100644
--- a/nodeeditor/NodeEditor.js
+++ b/nodeeditor/NodeEditor.js
@@ -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 {
@@ -17,6 +17,7 @@ export class NodeEditor {
this.isPanning = false;
this.lastPan = { x: 0, y: 0 };
this.zoom = 1.0;
+ this.focusedInput = null;
this._bindEvents();
@@ -33,10 +34,20 @@ export class NodeEditor {
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() {
const bufferSize = 1024; // adjust based on expected graph size
@@ -70,25 +81,20 @@ export class NodeEditor {
break;
case NODE_TYPES.Noise:
- view.setFloat32(offset, node.rate, true); offset += 4;
- view.setFloat32(offset, node.threshold, true); offset += 4;
- view.setFloat32(offset, node.pulseWidth, true); offset += 4;
- view.setFloat32(offset, node.amplitude, true); offset += 4;
- view.setUint8(offset++, node.seed);
+ view.setFloat32(offset, node.inputFrequency.numericValue, true); offset += 4;
+ view.setUint16(offset, node.inputSeed.numericValue); offset += 2;
break;
case NODE_TYPES.Variable:
view.setUint8(offset++, node.variableDropdown.selectedIndex);
- console.log(node.variableDropdown.selectedIndex);
+ view.setUint8(offset++, node.arg0Input.numericValue);
break;
case NODE_TYPES.Math:
view.setUint8(offset++, node.operatorDropdown.selectedIndex);
view.setFloat32(offset, node.valueInput.numericValue, true); offset += 4;
- console.log(node.operatorDropdown.selectedIndex);
- console.log(node.valueInput.numericValue);
break;
case NODE_TYPES.Map:
@@ -117,6 +123,109 @@ export class NodeEditor {
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 = {}) {
@@ -199,6 +308,7 @@ export class NodeEditor {
}
+
_onMouseDown(e) {
const { x, y } = this._getMouse(e);
@@ -210,6 +320,16 @@ export class NodeEditor {
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) {
if (node.contains(x, y)) {
@@ -312,16 +432,30 @@ export class NodeEditor {
_onDoubleClick(e) {
const { x, y } = this._getMouse(e);
+ // First: check for wire deletion
for (let i = this.connections.length - 1; i >= 0; 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)) {
this.connections.splice(i, 1);
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) {
const rect = this.canvas.getBoundingClientRect();
const screenX = e.clientX - rect.left;
@@ -419,6 +553,15 @@ export class NodeEditor {
hideMenu();
};
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);
}
diff --git a/nodeeditor/canvastools.js b/nodeeditor/canvastools.js
index f613611..ce93936 100644
--- a/nodeeditor/canvastools.js
+++ b/nodeeditor/canvastools.js
@@ -66,7 +66,7 @@ export class CanvasTextInput {
if (!this.focused) return false;
console.log(e);
if (e.key === "Backspace") {
- this.value = this.value.slice(0, -1);
+ this.value = String(this.value).slice(0, -1);
} else if (e.key === "Enter") {
this.clampToRange(); // 👈 clamp on Enter
this.focused = false;
diff --git a/nodeeditor/nodes.js b/nodeeditor/nodes.js
index be68d15..3522682 100644
--- a/nodeeditor/nodes.js
+++ b/nodeeditor/nodes.js
@@ -27,6 +27,9 @@ export class Node {
this.hasInput = false;
this.hasOutput = false;
+ this.canDelete = true;
+
+ this.inputs = [];
this.updatePorts();
}
@@ -120,6 +123,7 @@ export class ServoNode extends Node {
this.hasInput = true;
this.hasOutput = false;
+ this.canDelete = false;
}
draw(ctx) {
@@ -170,6 +174,7 @@ export class CurveNode extends Node {
this.hasInput = false;
this.hasOutput = true;
+ this.canDelete = false;
}
draw(ctx) {
@@ -224,6 +229,8 @@ export class InputNode extends Node {
{ 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") {
super(x, y, label);
this.type = NODE_TYPES.Noise;
- this.width = 160;
- this.height = 280;
+ this.width = 180;
+ this.height = 180;
- this.modeDropdown = new CanvasDropdown(
- 10, 35, this.width - 20,
- ["impulse", "pulse", "threshold", "smooth"],
- "impulse"
- );
+ this.inputFrequency = new CanvasTextInput(10, 45, this.width - 20, "1.0", "Frequency", { mode: "float" });
+ this.inputSeed = new CanvasTextInput(10, 70 + 10, this.width - 20, "0", "Seed", { mode: "int" });
- const inputConfigs = [
- { label: "Rate", value: "1.0", min: 0, max: 100 },
- { 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 = [];
+ this.inputs.push(this.inputFrequency);
+ this.inputs.push(this.inputSeed);
- 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) {
- ctx.fillStyle = this.color || "#f0e6ff";
- ctx.strokeStyle = this.border || "#333";
- ctx.lineWidth = 1;
- 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];
+ let max = 0
+ super.draw(ctx);
+ for (const input of [this.inputFrequency, this.inputSeed]) {
input.x = this.x + input.offsetX;
- input.y = this.y + input.offsetY; // 👈 vertical stacking
+ input.y = this.y + input.offsetY;
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.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2);
- ctx.fillStyle = "#888";
- ctx.fill();
+ for (let i = 0; i < steps; i++) {
+ const t = i / steps * freq * 10; // scale up to get more variation
+ 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
- this.modeDropdown.x = this.x + this.modeDropdown.offsetX;
- this.modeDropdown.y = this.y + this.modeDropdown.offsetY;
- this.modeDropdown.draw(ctx);
+ if (i === 0) ctx.moveTo(x, y);
+ else ctx.lineTo(x, y);
+ }
+ ctx.strokeStyle = "#444";
+ ctx.stroke();
}
contains(x, y) {
- return super.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));
+ return super.contains(x, y) || [this.inputFrequency, this.inputSeed].some(i => i.contains(x, y));
}
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) {
- return this.modeDropdown.handleMouseMove(mx, my);
+ return [this.inputFrequency, this.inputSeed].some(i => i.handleMouseMove(mx, my));
}
update(dt) {
- let needsRedraw = this.modeDropdown.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;
+ return [this.inputFrequency, this.inputSeed].some(i => i.update(dt));
}
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(
10, 45, this.width - 20,
- ["faceDetectX", "faceDetectY", "sine", "analogRead()"],
+ ["faceDetectX", "faceDetectY", "sine", "analogRead()", "servo"],
"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.hasOutput = true;
@@ -428,40 +396,42 @@ export class VariableNode extends Node {
// Dropdown label
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
this.variableDropdown.x = this.x + this.variableDropdown.offsetX;
this.variableDropdown.y = this.y + this.variableDropdown.offsetY;
this.variableDropdown.draw(ctx);
+
+
}
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) {
- return this.variableDropdown.handleClick(mx, my);
+ return false;//[this.inputFrequency, this.inputSeed].some(i => i.handleClick(mx, my));
}
handleMouseMove(mx, my) {
- return this.variableDropdown.handleMouseMove(mx, my);
+ return [this.variableDropdown, this.arg0Input].some(i => i.handleMouseMove(mx, my));
}
update(dt) {
- const changed = this.variableDropdown.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;
+ return [this.variableDropdown, this.arg0Input].some(i => i.update(dt));
}
get outputValue() {
@@ -474,7 +444,7 @@ export class MathNode extends Node {
super(x, y, label);
this.type = NODE_TYPES.Math;
this.width = 160;
- this.height = 100;
+ this.height = 110;
this.operatorDropdown = new CanvasDropdown(
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.inputs.push(this.operatorDropdown);
+ this.inputs.push(this.valueInput);
this.hasInput = true;
this.hasOutput = true;
@@ -558,9 +530,14 @@ export class MapNode extends Node {
this.height = 180;
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.outMinInput = new CanvasTextInput(10, 95+20, this.width - 20, "0", "Out Min", { mode: "float" });
- this.outMaxInput = new CanvasTextInput(10, 120+30, this.width - 20, "255", "Out 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, "1024", "Out Min", { 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.hasOutput = true;
@@ -580,13 +557,13 @@ export class MapNode extends Node {
}
handleKey(e) {
- return (
- this.inMinInput.handleKey(e) ||
- this.inMaxInput.handleKey(e) ||
- this.outMinInput.handleKey(e) ||
- this.outMaxInput.handleKey(e)
- );
-}
+ return (
+ this.inMinInput.handleKey(e) ||
+ this.inMaxInput.handleKey(e) ||
+ this.outMinInput.handleKey(e) ||
+ this.outMaxInput.handleKey(e)
+ );
+ }
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;
+}
diff --git a/robot.js b/robot.js
index d67d353..9b2a614 100644
--- a/robot.js
+++ b/robot.js
@@ -2,8 +2,8 @@ import { ServoMotor } from './feetechDefinitions.js';
export class Robot {
constructor(name, firmwareVersionId) {
- this.name = name;
- this.firmwareVersionId = firmwareVersionId;
+ this.deviceName = name;
+ this.firmwareVersion = { major: 0, minor: 0 };
// Map of position ID → ServoMotor
this.positionMap = new Map();
@@ -43,4 +43,41 @@ export class Robot {
}
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;
+ };
}
diff --git a/script.js b/script.js
index 9ee32f2..ffb0d3e 100644
--- a/script.js
+++ b/script.js
@@ -36,7 +36,7 @@ window.onload = () => {
const dialColors = ['red', 'green', 'blue', 'orange', 'purple'];
- const dials = [];
+ let dials = [];
const frameSlider = document.getElementById('frameSlider');
const frameDisplay = document.getElementById('frameDisplay');
@@ -56,24 +56,13 @@ window.onload = () => {
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");
+ let nodeEditor = null;
- const nodeEditor = new NodeEditor(nodeCanvas, {
- motorIds: motorIDList
- });
-
- nodeEditor.generateDefaultNodes(curveEditor.curveSets, motorIDList);
//nodeEditor.addServoNode(400, 150, "Servo Output", 5 );
// 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(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) {
@@ -166,8 +175,8 @@ window.onload = () => {
const filenameBytes = new TextEncoder().encode(filename);
const filenameLength = filenameBytes.length;
- // Total size: 2 bytes for length + filename bytes
- const buffer = new ArrayBuffer(2 + filenameLength);
+ // Total size: 2 bytes for length + filename bytes + oneshot/loop tag + loopCount
+ const buffer = new ArrayBuffer(2 + filenameLength + 2);
const view = new DataView(buffer);
let offset = 0;
@@ -175,6 +184,16 @@ window.onload = () => {
view.setUint16(offset, filenameLength, true); offset += 2;
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);
//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;
// Create dial wrapper
@@ -240,8 +266,10 @@ window.onload = () => {
// Create label
const label = document.createElement('label');
- label.textContent = "Motor " + motorID;
- console.log(motorID);
+ label.textContent = "MotorID " + motorID;
+
+ const label2 = document.createElement('label2');
+ label2.textContent = motorName;
// Create dial container
const dialDiv = document.createElement('div');
@@ -254,6 +282,7 @@ window.onload = () => {
// Assemble and append
dialWrapper.appendChild(label);
+ dialWrapper.appendChild(label2);
dialWrapper.appendChild(dialDiv);
dialWrapper.appendChild(valueSpan);
document.getElementById('dialArea').appendChild(dialWrapper);
@@ -286,6 +315,7 @@ window.onload = () => {
dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame);
}
+
}
@@ -341,6 +371,8 @@ window.onload = () => {
switch (command) {
case 0x01: // ID response
text = new TextDecoder().decode(new Uint8Array(payload));
+ onConnectRobot(Robot.fromBytes(new Uint8Array(payload)));
+ console.log(connectedRobot);
document.getElementById('log').value += `ID Response: ${text}\n`;
break;
@@ -499,8 +531,7 @@ window.onload = () => {
console.log(raw);
for (let i = 0; i < curveCount; i++) {
- const motorID = 10;
- offset += 1;
+ const motorID = view.getUint8(offset++);
const startTime = view.getUint16(offset, true); offset += 2;
const endTime = view.getUint16(offset, true); offset += 2;
const startPointY = view.getInt16(offset, true); offset += 2;
@@ -547,11 +578,13 @@ window.onload = () => {
console.log("🎯 Loaded Curves:", curveSets);
- // 🔁 Inject into your curve editor
- //loadCurvesIntoEditor(curves); // Replace with your actual editor hook
+
curveEditor.loadCurveSets(curveSets);
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
loadButton.disabled = false;
@@ -611,11 +644,11 @@ window.onload = () => {
let curvePacket = curveEditor.encodeCurves()
-
+
let nodeGraphPacket = nodeEditor.encodeNodeGraph();
// 🔹 Append nodeGraphPacket
-
+
// 🔹 Append curvePacket
curvePacket.forEach(byte => view.setUint8(offset++, byte));
@@ -675,12 +708,6 @@ window.onload = () => {
document.getElementById('input').value = '';
};
- document.getElementById('sendNodes').onclick = async () => {
- nodeGraphPacket = nodeEditor.encodeNodeGraph();
- curvePacket = curveEditor.encodeCurves()
-
- };
-
document.addEventListener('click', (e) => {
const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];
@@ -948,7 +975,7 @@ window.onload = () => {
console.log("ERROR: INCORRECT PACKET SIZE: " + payload.length);
return;
}
- const motor = new ServoMotor(payload);
+ const motor = new ServoMotor(Array.from(payload));
console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED);
servoMotors[motor.CHANNEL].push(motor);
diff --git a/style.css b/style.css
index 5132ac0..975cdef 100644
--- a/style.css
+++ b/style.css
@@ -26,7 +26,7 @@ body {
#fileListWrapper {
- width: 300px;
+ width: 400px;
max-height: 200px;
overflow-y: auto;
background-color: #f0f4ff;