From 17deaaa87329c55195acf101b2d7352733035b4d Mon Sep 17 00:00:00 2001 From: Jake Date: Tue, 21 Oct 2025 00:09:09 +0800 Subject: [PATCH] NoiseNode implemented (octave and persistence hard coded), added servo feedback option to variable --- curveEditor.js | 9 +- feetechDefinitions.js | 4 +- index.html | 32 +++-- nodeeditor/NodeEditor.js | 165 +++++++++++++++++++-- nodeeditor/canvastools.js | 2 +- nodeeditor/nodes.js | 296 ++++++++++++++++++++++---------------- robot.js | 41 +++++- script.js | 95 +++++++----- style.css | 2 +- 9 files changed, 456 insertions(+), 190 deletions(-) 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 @@
Animations
+ @@ -193,6 +195,21 @@
+ + + + + +
+
@@ -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;