From 83710c095ee660bb753d3878a6ad92e70caf24ea Mon Sep 17 00:00:00 2001 From: realrobots Date: Mon, 13 Oct 2025 23:06:57 +0800 Subject: [PATCH] encodeNodeGraph encodes entire set of nodes and connections for sending to the robot. should be sent along with curvesets --- curveEditor.js | 6 +- index.html | 3 + nodeeditor/NodeEditor.js | 786 ++++++++++++++++++++++++++++++++++++++ nodeeditor/canvastools.js | 210 ++++++++++ script.js | 68 +++- 5 files changed, 1057 insertions(+), 16 deletions(-) create mode 100644 nodeeditor/NodeEditor.js create mode 100644 nodeeditor/canvastools.js diff --git a/curveEditor.js b/curveEditor.js index c443cff..1de893d 100644 --- a/curveEditor.js +++ b/curveEditor.js @@ -108,7 +108,7 @@ export class CurveEditor { this.setCurves([]); // fallback to empty } console.log("LOADED"); - this.setSelectedMotor(10) + setSelectedMotor(10); // Global defined in script.js //this.selectAdjacentMotor(1); // Optional: update motor selector UI or redraw timeline //this.refreshMotorSelector?.(); // if you have a method for that @@ -117,7 +117,7 @@ export class CurveEditor { - setSelectedMotor(motorID) { + selectMotor(motorID) { this.selectedMotorID = motorID; this.curves = this.curveSets[motorID] || []; this.draw(); @@ -499,7 +499,7 @@ export class CurveEditor { if (currentIndex === -1) return; const newIndex = (currentIndex + direction + ids.length) % ids.length; - this.setSelectedMotor(ids[newIndex]); + setSelectedMotor(ids[newIndex]); // Global defined in script.js } diff --git a/index.html b/index.html index dfe34f7..442c263 100644 --- a/index.html +++ b/index.html @@ -204,6 +204,9 @@ + + +
diff --git a/nodeeditor/NodeEditor.js b/nodeeditor/NodeEditor.js new file mode 100644 index 0000000..991dc94 --- /dev/null +++ b/nodeeditor/NodeEditor.js @@ -0,0 +1,786 @@ +import { CanvasDropdown, CanvasTextInput } from "./canvastools.js" + +const NODE_TYPES = { + Node: 0x01, + Servo: 0x02, + Curve: 0x03, + Noise: 0x04 +}; + + +export class NodeEditor { + constructor(canvas, options = {}) { + this.canvas = canvas; + this.ctx = canvas.getContext("2d"); + this.nodes = []; + this.connections = []; + this.draggingNode = null; + this.draggingWire = null; + this.motorIds = options.motorIds || []; // pass motor ID list here + + + + this._bindEvents(); + this._redraw(); + } + + generateDefaultNodes(curveSets, motorIDs) { + console.log("Generating Default Nodes"); + console.log(curveSets, motorIDs); + + for (var i = 0; i < motorIDs.length; i++) { + let inputNode = this.addCurveNode(200, 20 + i * 120, "Curve", motorIDs[i]); + let outputNode = this.addServoNode(400, 20 + i * 120, "Servo Output", motorIDs[i]); + this.connections.push({ from: inputNode, to: outputNode }); + } + + + } + + + + encodeNodeGraph() { + const bufferSize = 1024; // adjust based on expected graph size + const buffer = new ArrayBuffer(bufferSize); + const view = new DataView(buffer); + let offset = 0; + + const nodes = this.nodes; + + // Node count (1 byte) + view.setUint8(offset++, nodes.length); + + // Encode nodes + nodes.forEach((node, index) => { + const typeCode = NODE_TYPES[node.type]; + node.id = index; + + view.setUint8(offset++, node.type); // Node type + view.setUint8(offset++, node.id); // Node ID + + switch (node.type) { + case NODE_TYPES.Servo: + view.setUint8(offset++, node.motorId); + break; + + case NODE_TYPES.Curve: + view.setUint8(offset++, node.curveId); + 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); + break; + + default: + console.warn("Unknown node type:", node.type); + } + }); + + // Connection count (1 byte) + view.setUint8(offset++, this.connections.length); + + // Encode connections + this.connections.forEach(conn => { + view.setUint8(offset++, conn.from.id); + view.setUint8(offset++, conn.to.id); + }); + + // Slice the buffer to actual used size + return new Uint8Array(buffer.slice(0, offset)); + } + + + + addNode(x, y, label, options = {}) { + const node = new Node(x, y, label, options); + this.nodes.push(node); + this._redraw(); + return node; + } + + addServoNode(x, y, label = "Servo Output", motorID) { + //console.log(motorID); + const node = new ServoNode(x, y, label, motorID); + this.nodes.push(node); + this._redraw(); + return node; + } + + addCurveNode(x, y, label = "Curve", motorID) { + //console.log(motorID); + const node = new CurveNode(x, y, label, motorID); + this.nodes.push(node); + this._redraw(); + return node; + } + + addInputNode(x, y, label = "Input Node", options = { defaultValue: 1.0 }) { + const node = new InputNode(x, y, label, options); + this.nodes.push(node); + this._redraw(); + return node; + } + + addNoiseNode(x, y, label = "Noise Generator", options = {}) { + const node = new NoiseNode(x, y, label, options); + this.nodes.push(node); + this._redraw(); + return node; + } + + addVariableNode(x, y, label = "Variable") { + const node = new VariableNode(x, y, label); + this.nodes.push(node); + this._redraw(); + return node; + } + + + + _bindEvents() { + this.canvas.addEventListener("mousedown", (e) => this._onMouseDown(e)); + this.canvas.addEventListener("mousemove", (e) => this._onMouseMove(e)); + this.canvas.addEventListener("mouseup", (e) => this._onMouseUp(e)); + this.canvas.addEventListener("dblclick", (e) => this._onDoubleClick(e)); + window.addEventListener("keydown", (e) => this._onKeyDown(e)); + } + + _onMouseDown(e) { + const { x, y } = this._getMouse(e); + + for (const node of this.nodes) { + if (node.contains(x, y)) { + if (node.handleClick(x, y)) { + this._redraw(); + return; + } + + this.draggingNode = node; + node.offsetX = x - node.x; + node.offsetY = y - node.y; + return; + } + + if (node.hitOutput(x, y)) { + this.draggingWire = { x1: node.output.x, y1: node.output.y, x2: x, y2: y, from: node }; + return; + } + } + } + + + + + _onMouseMove(e) { + const { x, y } = this._getMouse(e); + + let needsRedraw = false; + + for (const node of this.nodes) { + if (node instanceof ServoNode || node instanceof NoiseNode) { + if (node.handleMouseMove(x, y)) { + needsRedraw = true; + } + } + } + + if (needsRedraw) this._redraw(); + + + if (this.draggingNode) { + this.draggingNode.x = x - this.draggingNode.offsetX; + this.draggingNode.y = y - this.draggingNode.offsetY; + this.draggingNode.updatePorts(); + this._redraw(); + } + + if (this.draggingWire) { + this.draggingWire.x2 = x; + this.draggingWire.y2 = y; + this._redraw(); + } + } + + + _onMouseUp(e) { + const { x, y } = this._getMouse(e); + + if (this.draggingWire) { + for (const node of this.nodes) { + if (node.hitInput(x, y)) { + this.connections.push({ from: this.draggingWire.from, to: node }); + break; + } + } + this.draggingWire = null; + this._redraw(); + } + + this.draggingNode = null; + } + + _onDoubleClick(e) { + const { x, y } = this._getMouse(e); + + 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; + } + } + } + + _getMouse(e) { + const rect = this.canvas.getBoundingClientRect(); + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top + }; + } + + _onKeyDown(e) { + let needsRedraw = false; + + for (const node of this.nodes) { + if (node.handleKey && node.handleKey(e)) { + needsRedraw = true; + } + } + + if (needsRedraw) this._redraw(); + } + + + _isPointNearLine(px, py, x1, y1, x2, y2, threshold = 10) { + const dx = x2 - x1; + const dy = y2 - y1; + const lengthSquared = dx * dx + dy * dy; + if (lengthSquared === 0) return false; + + const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lengthSquared)); + const closestX = x1 + t * dx; + const closestY = y1 + t * dy; + const dist = Math.hypot(px - closestX, py - closestY); + return dist < threshold; + } + + _drawConnections() { + this.ctx.strokeStyle = "#444"; + this.ctx.lineWidth = 2; + for (const conn of this.connections) { + this.ctx.beginPath(); + this.ctx.moveTo(conn.from.output.x, conn.from.output.y); + this.ctx.lineTo(conn.to.input.x, conn.to.input.y); + this.ctx.stroke(); + } + } + + _redraw() { + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + this._drawConnections(); + for (const node of this.nodes) { + node.draw(this.ctx); + } + + if (this.draggingWire) { + this.ctx.strokeStyle = "#888"; + this.ctx.lineWidth = 2; + this.ctx.beginPath(); + this.ctx.moveTo(this.draggingWire.x1, this.draggingWire.y1); + this.ctx.lineTo(this.draggingWire.x2, this.draggingWire.y2); + this.ctx.stroke(); + } + } +} + +class Node { + constructor(x, y, label, options = {}) { + this.type = NODE_TYPES.Node; + this.x = x; + this.y = y; + this.width = 120; + this.height = 60; + this.label = label; + this.input = { x: 0, y: 0 }; + this.output = { x: 0, y: 0 }; + //console.log(options.fill); + // Customizable visual options + this.color = options.fill || "#fef6e4"; // pastel fill default + this.border = options.stroke || "#333"; // border color default + + this.updatePorts(); + } + + + + updatePorts() { + this.input.x = this.x - 5; + this.input.y = this.y + this.height / 2; + this.output.x = this.x + this.width + 5; + this.output.y = this.y + this.height / 2; + } + + + + draw(ctx) { + ctx.fillStyle = this.color; + ctx.strokeStyle = this.border; + ctx.lineWidth = 1; + + this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); + ctx.fill(); + ctx.stroke(); + + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.label, this.x + 10, this.y + 20); + + ctx.beginPath(); + ctx.arc(this.input.x, this.input.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + + ctx.beginPath(); + ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + } + + + + drawRoundedRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.quadraticCurveTo(x + w, y, x + w, y + r); + ctx.lineTo(x + w, y + h - r); + ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + ctx.lineTo(x + r, y + h); + ctx.quadraticCurveTo(x, y + h, x, y + h - r); + ctx.lineTo(x, y + r); + ctx.quadraticCurveTo(x, y, x + r, y); + ctx.closePath(); + } + + + contains(x, y) { + return x >= this.x && x <= this.x + this.width && + y >= this.y && y <= this.y + this.height; + } + + hitOutput(x, y) { + return Math.hypot(x - this.output.x, y - this.output.y) < 8; + } + + hitInput(x, y) { + return Math.hypot(x - this.input.x, y - this.input.y) < 8; + } +} + + +export class ServoNode extends Node { + constructor(x, y, label, motorId) { + super(x, y, label); + this.type = NODE_TYPES.Servo; + this.motorId = motorId; + this.width = 140; + this.height = 80; + } + + draw(ctx) { + // Node box + ctx.fillStyle = this.color || "#fff9d6"; + ctx.strokeStyle = this.border || "#333"; + ctx.lineWidth = 1; + this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); + ctx.fill(); + ctx.stroke(); + + // Label + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.label, this.x + 10, this.y + 20); + + // Motor ID display + ctx.font = "12px sans-serif"; + ctx.fillText(`Motor ${this.motorId}`, this.x + 10, this.y + 40); + + // Ports + this.updatePorts(); + ctx.beginPath(); + ctx.arc(this.input.x, this.input.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + + ctx.beginPath(); + ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + } + + contains(x, y) { + return super.contains(x, y); + } + + handleClick(mx, my) { + + if (this.contains(mx, my)) { + window.setSelectedMotor?.(this.motorId); // or this.selectedMotorId if using dropdown + return false; + } + return false; + } + + + handleMouseMove(mx, my) { + return false; + } + + get selectedMotorId() { + return this.motorId; + } +} + +export class CurveNode extends Node { + constructor(x, y, label = "Curve", curveId = 0) { + super(x, y, label); + this.type = NODE_TYPES.Curve; + this.curveId = curveId; + this.width = 140; + this.height = 80; + } + + draw(ctx) { + // Node box + ctx.fillStyle = this.color || "#d6f0ff"; + ctx.strokeStyle = this.border || "#333"; + ctx.lineWidth = 1; + this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); + ctx.fill(); + ctx.stroke(); + + // Label + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.label, this.x + 10, this.y + 20); + + // Curve ID display + ctx.font = "12px sans-serif"; + ctx.fillText(`Curve ${this.curveId}`, this.x + 10, this.y + 40); + + // Output port only + this.updatePorts(); + ctx.beginPath(); + ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + } + + + contains(x, y) { + return super.contains(x, y); + } + + handleClick(mx, my) { + if (this.contains(mx, my)) { + window.setSelectedMotor?.(this.curveId); + return false; + } + return false; + } + + + handleMouseMove(mx, my) { + return false; + } + + update(dt) { + // Placeholder: simulate curve output + const t = Date.now() / 1000; + this.lastValue = Math.sin(t + this.curveId); // simple sine curve + return false; + } + + get outputValue() { + return this.lastValue; + } +} + + + +export class InputNode extends Node { + constructor(x, y, label, options) { + super(x, y, label); + console.log(options); + this.inputField = new CanvasTextInput( + this.x + 10, + this.y + 35, + this.width - 20, + "1.0", + "", + { defaultValue: 1.0, numericOnly: true, min: 0, max: 10 } + ); + + } + + + + draw(ctx) { + // Node box + ctx.fillStyle = this.color || "#e0f7ff"; + ctx.strokeStyle = this.border || "#333"; + ctx.lineWidth = 1; + this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); + ctx.fill(); + ctx.stroke(); + + // Label + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.label, this.x + 10, this.y + 20); + + // Input field + this.inputField.x = this.x + 10; + this.inputField.y = this.y + 35; + this.inputField.draw(ctx); + + // Output port + ctx.beginPath(); + ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + } + + contains(x, y) { + return super.contains(x, y) || this.inputField.contains(x, y); + } + + handleClick(mx, my) { + console.log(mx, my); + return this.inputField.handleClick(mx, my); + } + + handleKey(e) { + return this.inputField.handleKey(e); + } + + + get outputValue() { + return this.inputField.numericValue; + } +} + + +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.modeDropdown = new CanvasDropdown( + 10, 35, this.width - 20, + ["impulse", "pulse", "threshold", "smooth"], + "impulse" + ); + + 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 = []; + + const inputSpacing = 42; // includes label + input height + + 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]; + input.x = this.x + input.offsetX; + input.y = this.y + input.offsetY; // 👈 vertical stacking + input.draw(ctx); + } + + + // 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(); + + + // Draw dropdowns LAST + this.modeDropdown.x = this.x + this.modeDropdown.offsetX; + this.modeDropdown.y = this.y + this.modeDropdown.offsetY; + this.modeDropdown.draw(ctx); + } + + 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)); + } + + handleKey(e) { + return this.inputs.some(input => input.handleKey(e)); + } + + + handleMouseMove(mx, my) { + return this.modeDropdown.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; + } + + get outputValue() { + return this.lastValue; + } +} + +export class VariableNode extends Node { + constructor(x, y, label = "Variable") { + super(x, y, label); + this.width = 160; + this.height = 100; + + this.variableDropdown = new CanvasDropdown( + 10, 45, this.width - 20, + ["faceDetectX", "faceDetectY", "sine"], + "faceDetectX" + ); + + this.lastValue = 0; + this.timer = 0; + } + + draw(ctx) { + ctx.fillStyle = this.color || "#e6ffe6"; + ctx.strokeStyle = this.border || "#333"; + ctx.lineWidth = 1; + this.drawRoundedRect(ctx, this.x, this.y, this.width, this.height, 10); + ctx.fill(); + ctx.stroke(); + + // Label + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.label, this.x + 10, this.y + 20); + + // Dropdown label + ctx.fillText("Source", this.x + 10, this.y + 42); + + // Dropdown + this.variableDropdown.x = this.x + this.variableDropdown.offsetX; + this.variableDropdown.y = this.y + this.variableDropdown.offsetY; + this.variableDropdown.draw(ctx); + + // Output port + ctx.beginPath(); + ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2); + ctx.fillStyle = "#888"; + ctx.fill(); + } + + contains(x, y) { + return super.contains(x, y) || this.variableDropdown.contains(x, y); + } + + handleClick(mx, my) { + return this.variableDropdown.handleClick(mx, my); + } + + handleMouseMove(mx, my) { + return this.variableDropdown.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; + } + + get outputValue() { + return this.lastValue; + } +} diff --git a/nodeeditor/canvastools.js b/nodeeditor/canvastools.js new file mode 100644 index 0000000..a4bda2d --- /dev/null +++ b/nodeeditor/canvastools.js @@ -0,0 +1,210 @@ +export class CanvasTextInput { + constructor(offsetX, offsetY, width = 120, value = "", label = "", options = {}) { + this.x = 0; + this.y = 0; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.width = width; + this.height = 24; + this.labelOffset = 16; // space above input for label + this.value = String(value); + //this.placeholder = placeholder; + this.focused = false; + this.cursorVisible = false; + this.cursorTimer = 0; + + this.numericOnly = options.numericOnly || false; + this.min = options.min ?? -Infinity; + this.max = options.max ?? Infinity; + this.label = label || ""; + } + + + + + draw(ctx) { + if (this.label) { + ctx.fillStyle = "#000"; + ctx.font = "13px sans-serif"; + ctx.fillText(this.label, this.x, this.y - this.labelOffset + 15); + } + + + ctx.fillStyle = "#fff"; + ctx.strokeStyle = "#666"; + ctx.fillRect(this.x, this.y, this.width, this.height); + ctx.strokeRect(this.x, this.y, this.width, this.height); + + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + const text = this.value || this.placeholder; + ctx.fillText(text, this.x + 5, this.y + 15); + + if (this.focused && this.cursorVisible) { + const textWidth = ctx.measureText(this.value).width; + ctx.beginPath(); + ctx.moveTo(this.x + 5 + textWidth, this.y + 4); + ctx.lineTo(this.x + 5 + textWidth, this.y + this.height - 4); + ctx.strokeStyle = "#000"; + ctx.stroke(); + } + } + + contains(mx, my) { + return mx >= this.x && mx <= this.x + this.width && + my >= this.y && my <= this.y + this.height; + } + + handleClick(mx, my) { + console.log("HERE", mx, my); + this.focused = this.contains(mx, my); + return this.focused; + } + + handleKey(e) { + if (!this.focused) return false; + + if (e.key === "Backspace") { + this.value = this.value.slice(0, -1); + } else if (e.key === "Enter") { + this.clampToRange(); // 👈 clamp on Enter + this.focused = false; + } else if (e.key.length === 1) { + if (this.numericOnly) { + if (/[\d.\-]/.test(e.key)) { + this.value += e.key; + + this.clampToRange(); // 👈 clamp on Enter + } + } else { + this.value += e.key; + + this.clampToRange(); // 👈 clamp on Enter + } + } + + return true; + } + + + + update(dt) { + this.cursorTimer += dt; + if (this.cursorTimer > 500) { + this.cursorVisible = !this.cursorVisible; + this.cursorTimer = 0; + } + } + + clampToRange() { + let val = parseFloat(this.value); + if (isNaN(val)) val = 0; + val = Math.min(this.max, Math.max(this.min, val)); + this.value = String(val); + } + + + get numericValue() { + let val = parseFloat(this.value); + if (isNaN(val)) return 0; + return Math.min(this.max, Math.max(this.min, val)); + } + +} + + +export class CanvasDropdown { + constructor(offsetX, offsetY, width, items = [], selected = 0) { + this.x = 0; + this.y = 0 + this.offsetX = offsetX; + this.offsetY = offsetY; + this.width = width; + this.height = 20; + this.items = items; + this.selectedIndex = 0; + this.open = false; + this.hoverIndex = -1; + } + + draw(ctx) { + // Collapsed box + ctx.fillStyle = "#eee"; + ctx.strokeStyle = "#666"; + ctx.fillRect(this.x, this.y, this.width, this.height); + ctx.strokeRect(this.x, this.y, this.width, this.height); + + ctx.fillStyle = "#000"; + ctx.font = "14px sans-serif"; + ctx.fillText(this.items[this.selectedIndex], this.x + 5, this.y + 15); + + // Expanded list + if (this.open) { + for (let i = 0; i < this.items.length; i++) { + const itemY = this.y + this.height + i * this.height; + ctx.fillStyle = i === this.hoverIndex ? "#cce" : "#fff"; + ctx.fillRect(this.x, itemY, this.width, this.height); + ctx.strokeRect(this.x, itemY, this.width, this.height); + ctx.fillStyle = "#000"; + ctx.fillText(this.items[i], this.x + 5, itemY + 15); + } + } + } + + contains(mx, my) { + const totalHeight = this.height + (this.open ? this.items.length * this.height : 0); + return mx >= this.x && mx <= this.x + this.width && + my >= this.y && my <= this.y + totalHeight; + } + + handleClick(mx, my) { + if (this.open) { + for (let i = 0; i < this.items.length; i++) { + const itemY = this.y + this.height + i * this.height; + if (mx >= this.x && mx <= this.x + this.width && + my >= itemY && my <= itemY + this.height) { + this.selectedIndex = i; + this.open = false; + this.hoverIndex = -1; + return true; + } + } + this.open = false; + this.hoverIndex = -1; + return true; + } + + if (mx >= this.x && mx <= this.x + this.width && + my >= this.y && my <= this.y + this.height) { + this.open = true; + return true; + } + + return false; + } + + handleMouseMove(mx, my) { + if (!this.open) return false; + + let hovered = -1; + for (let i = 0; i < this.items.length; i++) { + const itemY = this.y + this.height + i * this.height; + if (mx >= this.x && mx <= this.x + this.width && + my >= itemY && my <= itemY + this.height) { + hovered = i; + break; + } + } + + if (hovered !== this.hoverIndex) { + this.hoverIndex = hovered; + return true; + } + + return false; + } + + get selectedValue() { + return this.items[this.selectedIndex]; + } +} diff --git a/script.js b/script.js index 4ffa3ce..b382626 100644 --- a/script.js +++ b/script.js @@ -2,6 +2,8 @@ import { SerialManager } from './serial.js'; import { ServoMotor, getModelType, writeData } from './feetechDefinitions.js'; import { CurveEditor } from './curveEditor.js'; import { Robot } from './robot.js'; +import { NodeEditor } from './nodeeditor/NodeEditor.js'; + @@ -57,11 +59,54 @@ window.onload = () => { let connectedRobot = 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); } - curveEditor.setSelectedMotor(10); + setSelectedMotor(10); + + const nodeCanvas = document.getElementById("nodeeditor"); + + 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 }); + // nodeEditor.addNoiseNode(400, 450); // Adds a random generator node at (200, 150) + // nodeEditor.addVariableNode(300, 250); + + // nodeEditor.addNode(100, 100, "Time", { fill: "#e0f7e9", stroke: "#2e7d32" }); // mint green + // nodeEditor.addNode(300, 200, "Output"); // uses default pastel + + + + function setSelectedMotor(motorID) { + console.log(motorID); + curveEditor.selectMotor(motorID); + selectedDial = motorID; + + const dialElements = document.querySelectorAll('.dial'); + + dialElements.forEach((el, index) => { + el.classList.remove('selected'); + + if (dials[index]?.motorID === motorID) { + el.classList.add('selected'); + } + }); + + console.log("Selected motor:", motorID); + // Any other logic you want to run + + + } + window.setSelectedMotor = setSelectedMotor; + // TODO: Info should all be loaded on connect from handshake packet function GenerateTestRobot() { @@ -237,14 +282,10 @@ window.onload = () => { } function syncDialsWithCurveEditor() { - //let pos = curveEditor.getMotorPositionAtTime(11, currentFrame); for (let ch = 0; ch < dials.length; ch++) { - console.log(dials[ch].motorID); dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame); - //const value = dials[ch].value; - //motorPayloads.push({ motorId: ch, position: value }); + } - //console.log(pos); } @@ -279,16 +320,14 @@ window.onload = () => { document.querySelectorAll('.dial').forEach(el => { el.onclick = () => { - selectedDial = parseInt(el.dataset.index); + const selectedDial = parseInt(el.dataset.index); - document.querySelectorAll('.dial').forEach(d => d.classList.remove('selected')); - el.classList.add('selected'); - console.log(dials[selectedDial].motorID); - curveEditor.setSelectedMotor(dials[selectedDial].motorID); + setSelectedMotor(dials[selectedDial].motorID); }; }); + // Connect button document.getElementById('connect').addEventListener('click', async () => { try { @@ -607,8 +646,6 @@ window.onload = () => { view.setUint16(offset, seg.startTime, true); offset += 2; view.setUint16(offset, seg.endTime, true); offset += 2; view.setInt16(offset, curveEditor.yToExportRange(seg.startPointY), true); offset += 2; - - console.log(curveEditor.yToExportRange(seg.startPointY)); view.setUint16(offset, seg.startHandleX, true); offset += 2; view.setInt16(offset, curveEditor.yToExportRange(seg.startHandleY), true); offset += 2; view.setUint16(offset, seg.endHandleX, true); offset += 2; @@ -673,6 +710,11 @@ window.onload = () => { document.getElementById('input').value = ''; }; + document.getElementById('sendNodes').onclick = async () => { + console.log(nodeEditor.encodeNodeGraph()); + + }; + document.addEventListener('click', (e) => { const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];