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 view.setUint16(offset, node.x, true); offset+=2; view.setUint16(offset, node.y, true); offset+=2; 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; } }