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'];