variable, math, and map() nodes are implemented
parent
1c3bab83ec
commit
dae69f4c0d
|
|
@ -121,9 +121,10 @@ export class CurveEditor {
|
||||||
view.setUint16(offset, seg.endHandleX, true); offset += 2;
|
view.setUint16(offset, seg.endHandleX, true); offset += 2;
|
||||||
view.setInt16(offset, this.yToExportRange(seg.endHandleY), true); offset += 2;
|
view.setInt16(offset, this.yToExportRange(seg.endHandleY), true); offset += 2;
|
||||||
view.setInt16(offset, this.yToExportRange(seg.endPointY), true); offset += 2;
|
view.setInt16(offset, this.yToExportRange(seg.endPointY), true); offset += 2;
|
||||||
|
console.log(this.yToExportRange(seg.startPointY), this.yToExportRange(seg.endPointY));
|
||||||
});
|
});
|
||||||
console.log("🧵 Curve segments packed:", curveSegments.length);
|
//console.log("🧵 Curve segments packed:", curveSegments.length);
|
||||||
console.log(curveSegments);
|
//console.log(curveSegments);
|
||||||
|
|
||||||
|
|
||||||
return new Uint8Array(buffer.slice(0, offset));
|
return new Uint8Array(buffer.slice(0, offset));
|
||||||
|
|
|
||||||
12
index.html
12
index.html
|
|
@ -205,8 +205,18 @@
|
||||||
|
|
||||||
|
|
||||||
<canvas id="nodeeditor" width="800" height="600"></canvas>
|
<canvas id="nodeeditor" width="800" height="600"></canvas>
|
||||||
|
<div id="contextMenu" style="
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
padding: 6px;
|
||||||
|
font-family: sans-serif;
|
||||||
|
z-index: 1000;
|
||||||
|
">
|
||||||
|
</div>
|
||||||
|
|
||||||
<button id="sendNodes">Send Nodes</button>
|
<button id="sendNodes">Send Nodes</button>
|
||||||
|
|
||||||
|
|
||||||
<textarea id="log" rows="10" cols="60" readonly></textarea><br>
|
<textarea id="log" rows="10" cols="60" readonly></textarea><br>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import { CanvasDropdown, CanvasTextInput } from "./canvastools.js"
|
|
||||||
|
|
||||||
const NODE_TYPES = {
|
import { ServoNode, CurveNode, VariableNode, MathNode, MapNode, NODE_TYPES } from "./nodes.js"
|
||||||
Node: 0x01,
|
|
||||||
Servo: 0x02,
|
|
||||||
Curve: 0x03,
|
|
||||||
Noise: 0x04
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export class NodeEditor {
|
export class NodeEditor {
|
||||||
|
|
@ -18,6 +12,11 @@ export class NodeEditor {
|
||||||
this.draggingWire = null;
|
this.draggingWire = null;
|
||||||
this.motorIds = options.motorIds || []; // pass motor ID list here
|
this.motorIds = options.motorIds || []; // pass motor ID list here
|
||||||
|
|
||||||
|
this.panX = 0;
|
||||||
|
this.panY = 0;
|
||||||
|
this.isPanning = false;
|
||||||
|
this.lastPan = { x: 0, y: 0 };
|
||||||
|
this.zoom = 1.0;
|
||||||
|
|
||||||
|
|
||||||
this._bindEvents();
|
this._bindEvents();
|
||||||
|
|
@ -29,12 +28,12 @@ export class NodeEditor {
|
||||||
console.log(curveSets, motorIDs);
|
console.log(curveSets, motorIDs);
|
||||||
|
|
||||||
for (var i = 0; i < motorIDs.length; i++) {
|
for (var i = 0; i < motorIDs.length; i++) {
|
||||||
let inputNode = this.addCurveNode(200, 20 + i * 120, "Curve", motorIDs[i]);
|
let inputNode = this.addCurveNode(200, 20 + i * 140, "Curve", motorIDs[i]);
|
||||||
let outputNode = this.addServoNode(400, 20 + i * 120, "Servo Output", motorIDs[i]);
|
let outputNode = this.addServoNode(500, 40 + i * 140, "Servo Output", motorIDs[i]);
|
||||||
this.connections.push({ from: inputNode, to: outputNode });
|
this.connections.push({ from: inputNode, to: outputNode });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.addVariableNode(50, 120, "Var");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -57,8 +56,8 @@ export class NodeEditor {
|
||||||
|
|
||||||
view.setUint8(offset++, node.type); // Node type
|
view.setUint8(offset++, node.type); // Node type
|
||||||
view.setUint8(offset++, node.id); // Node ID
|
view.setUint8(offset++, node.id); // Node ID
|
||||||
view.setUint16(offset, node.x, true); offset+=2;
|
view.setUint16(offset, node.x, true); offset += 2;
|
||||||
view.setUint16(offset, node.y, true); offset+=2;
|
view.setUint16(offset, node.y, true); offset += 2;
|
||||||
|
|
||||||
|
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
|
|
@ -78,6 +77,28 @@ export class NodeEditor {
|
||||||
view.setUint8(offset++, node.seed);
|
view.setUint8(offset++, node.seed);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
|
case NODE_TYPES.Variable:
|
||||||
|
view.setUint8(offset++, node.variableDropdown.selectedIndex);
|
||||||
|
console.log(node.variableDropdown.selectedIndex);
|
||||||
|
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:
|
||||||
|
view.setFloat32(offset, node.inMinInput.numericValue, true); offset += 4;
|
||||||
|
view.setFloat32(offset, node.inMaxInput.numericValue, true); offset += 4;
|
||||||
|
view.setFloat32(offset, node.outMinInput.numericValue, true); offset += 4;
|
||||||
|
view.setFloat32(offset, node.outMaxInput.numericValue, true); offset += 4;
|
||||||
|
console.log(node.inMinInput.numericValue, node.inMaxInput.numericValue, node.outMinInput.numericValue, node.outMaxInput.numericValue);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn("Unknown node type:", node.type);
|
console.warn("Unknown node type:", node.type);
|
||||||
}
|
}
|
||||||
|
|
@ -142,6 +163,21 @@ export class NodeEditor {
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addMathNode(x, y, label = "Math") {
|
||||||
|
const node = new MathNode(x, y, label);
|
||||||
|
this.nodes.push(node);
|
||||||
|
this._redraw();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMapNode(x, y, label = "Map") {
|
||||||
|
const node = new MapNode(x, y, label);
|
||||||
|
this.nodes.push(node);
|
||||||
|
this._redraw();
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_bindEvents() {
|
_bindEvents() {
|
||||||
|
|
@ -150,11 +186,31 @@ export class NodeEditor {
|
||||||
this.canvas.addEventListener("mouseup", (e) => this._onMouseUp(e));
|
this.canvas.addEventListener("mouseup", (e) => this._onMouseUp(e));
|
||||||
this.canvas.addEventListener("dblclick", (e) => this._onDoubleClick(e));
|
this.canvas.addEventListener("dblclick", (e) => this._onDoubleClick(e));
|
||||||
window.addEventListener("keydown", (e) => this._onKeyDown(e));
|
window.addEventListener("keydown", (e) => this._onKeyDown(e));
|
||||||
|
this.canvas.addEventListener("wheel", (e) => this._onWheel(e));
|
||||||
|
|
||||||
|
this.canvas.addEventListener("contextmenu", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const { x, y } = this._getMouse(e); // canvas-space for node placement
|
||||||
|
this._showContextMenu(e.pageX, e.pageY, x, y); // page-space for menu placement
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onMouseDown(e) {
|
_onMouseDown(e) {
|
||||||
const { x, y } = this._getMouse(e);
|
const { x, y } = this._getMouse(e);
|
||||||
|
|
||||||
|
if (e.button === 1) { // middle mouse
|
||||||
|
this.isPanning = true;
|
||||||
|
console.log("pan")
|
||||||
|
this.lastPan = this._getMouse(e);
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
for (const node of this.nodes) {
|
for (const node of this.nodes) {
|
||||||
if (node.contains(x, y)) {
|
if (node.contains(x, y)) {
|
||||||
if (node.handleClick(x, y)) {
|
if (node.handleClick(x, y)) {
|
||||||
|
|
@ -183,12 +239,25 @@ export class NodeEditor {
|
||||||
|
|
||||||
let needsRedraw = false;
|
let needsRedraw = false;
|
||||||
|
|
||||||
|
if (this.isPanning) {
|
||||||
|
const { x, y } = this._getMouse(e);
|
||||||
|
const { screenX, screenY } = this._getMouse(e);
|
||||||
|
this.panX += screenX - this.lastPan.screenX;
|
||||||
|
this.panY += screenY - this.lastPan.screenY;
|
||||||
|
this.lastPan = { screenX, screenY };
|
||||||
|
|
||||||
|
this._redraw();
|
||||||
|
//return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Required for highlighting dropdown options onMouseOver
|
||||||
for (const node of this.nodes) {
|
for (const node of this.nodes) {
|
||||||
if (node instanceof ServoNode || node instanceof NoiseNode) {
|
//if (node instanceof ServoNode || node instanceof VariableNode) {
|
||||||
if (node.handleMouseMove(x, y)) {
|
if (node.handleMouseMove(x, y)) {
|
||||||
needsRedraw = true;
|
needsRedraw = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (needsRedraw) this._redraw();
|
if (needsRedraw) this._redraw();
|
||||||
|
|
@ -212,9 +281,22 @@ export class NodeEditor {
|
||||||
_onMouseUp(e) {
|
_onMouseUp(e) {
|
||||||
const { x, y } = this._getMouse(e);
|
const { x, y } = this._getMouse(e);
|
||||||
|
|
||||||
|
if (this.isPanning) {
|
||||||
|
this.isPanning = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (this.draggingWire) {
|
if (this.draggingWire) {
|
||||||
for (const node of this.nodes) {
|
for (const node of this.nodes) {
|
||||||
if (node.hitInput(x, y)) {
|
if (node.hitInput(x, y)) {
|
||||||
|
// Remove any existing connection to this input
|
||||||
|
for (let i = this.connections.length - 1; i >= 0; i--) {
|
||||||
|
if (this.connections[i].to === node) {
|
||||||
|
this.connections.splice(i, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new connection
|
||||||
this.connections.push({ from: this.draggingWire.from, to: node });
|
this.connections.push({ from: this.draggingWire.from, to: node });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -223,6 +305,7 @@ export class NodeEditor {
|
||||||
this._redraw();
|
this._redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.draggingNode = null;
|
this.draggingNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -241,12 +324,21 @@ export class NodeEditor {
|
||||||
|
|
||||||
_getMouse(e) {
|
_getMouse(e) {
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
|
const screenX = e.clientX - rect.left;
|
||||||
|
const screenY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const canvasX = (screenX - this.panX) / this.zoom;
|
||||||
|
const canvasY = (screenY - this.panY) / this.zoom;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: e.clientX - rect.left,
|
x: canvasX,
|
||||||
y: e.clientY - rect.top
|
y: canvasY,
|
||||||
|
screenX,
|
||||||
|
screenY
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_onKeyDown(e) {
|
_onKeyDown(e) {
|
||||||
let needsRedraw = false;
|
let needsRedraw = false;
|
||||||
|
|
||||||
|
|
@ -260,6 +352,20 @@ export class NodeEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
_onWheel(e) {
|
||||||
|
const zoomFactor = 1.1;
|
||||||
|
if (e.deltaY < 0) {
|
||||||
|
this.zoom *= zoomFactor;
|
||||||
|
} else {
|
||||||
|
this.zoom /= zoomFactor;
|
||||||
|
}
|
||||||
|
this.zoom = Math.max(0.2, Math.min(3.0, this.zoom)); // clamp zoom
|
||||||
|
this._redraw();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_isPointNearLine(px, py, x1, y1, x2, y2, threshold = 10) {
|
_isPointNearLine(px, py, x1, y1, x2, y2, threshold = 10) {
|
||||||
const dx = x2 - x1;
|
const dx = x2 - x1;
|
||||||
const dy = y2 - y1;
|
const dy = y2 - y1;
|
||||||
|
|
@ -273,18 +379,82 @@ export class NodeEditor {
|
||||||
return dist < threshold;
|
return dist < threshold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_showContextMenu(screenX, screenY, canvasX, canvasY) {
|
||||||
|
const menu = document.getElementById("contextMenu");
|
||||||
|
menu.innerHTML = ""; // clear previous items
|
||||||
|
menu.style.left = `${screenX}px`;
|
||||||
|
menu.style.top = `${screenY}px`;
|
||||||
|
menu.style.display = "block";
|
||||||
|
|
||||||
|
const hideMenu = () => {
|
||||||
|
menu.style.display = "none";
|
||||||
|
window.removeEventListener("click", hideMenu);
|
||||||
|
};
|
||||||
|
window.addEventListener("click", hideMenu);
|
||||||
|
|
||||||
|
// ✅ Only include Variable node
|
||||||
|
let item = document.createElement("div");
|
||||||
|
item.className = "menu-item";
|
||||||
|
item.textContent = "➕ Add Variable Node";
|
||||||
|
item.onclick = () => {
|
||||||
|
this.addVariableNode(canvasX, canvasY, "Variable");
|
||||||
|
hideMenu();
|
||||||
|
};
|
||||||
|
menu.appendChild(item);
|
||||||
|
|
||||||
|
item = document.createElement("div");
|
||||||
|
item.className = "menu-item";
|
||||||
|
item.textContent = "➕ Add Math Node";
|
||||||
|
item.onclick = () => {
|
||||||
|
this.addMathNode(canvasX, canvasY, "Math");
|
||||||
|
hideMenu();
|
||||||
|
};
|
||||||
|
menu.appendChild(item);
|
||||||
|
|
||||||
|
item = document.createElement("div");
|
||||||
|
item.className = "menu-item";
|
||||||
|
item.textContent = "➕ Add Map Node";
|
||||||
|
item.onclick = () => {
|
||||||
|
this.addMapNode(canvasX, canvasY, "Map");
|
||||||
|
hideMenu();
|
||||||
|
};
|
||||||
|
menu.appendChild(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
_drawConnections() {
|
_drawConnections() {
|
||||||
this.ctx.strokeStyle = "#444";
|
this.ctx.strokeStyle = "#444";
|
||||||
this.ctx.lineWidth = 2;
|
this.ctx.lineWidth = 2;
|
||||||
for (const conn of this.connections) {
|
for (const conn of this.connections) {
|
||||||
|
const x1 = conn.from.output.x;
|
||||||
|
const y1 = conn.from.output.y;
|
||||||
|
const x2 = conn.to.input.x;
|
||||||
|
const y2 = conn.to.input.y;
|
||||||
|
|
||||||
|
const dx = Math.abs(x2 - x1) * 0.5;
|
||||||
|
|
||||||
this.ctx.beginPath();
|
this.ctx.beginPath();
|
||||||
this.ctx.moveTo(conn.from.output.x, conn.from.output.y);
|
this.ctx.moveTo(x1, y1);
|
||||||
this.ctx.lineTo(conn.to.input.x, conn.to.input.y);
|
this.ctx.bezierCurveTo(
|
||||||
|
x1 + dx, y1,
|
||||||
|
x2 - dx, y2,
|
||||||
|
x2, y2
|
||||||
|
);
|
||||||
this.ctx.stroke();
|
this.ctx.stroke();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_redraw() {
|
_redraw() {
|
||||||
|
this.ctx.save();
|
||||||
|
this.ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
|
||||||
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // clear full canvas
|
||||||
|
|
||||||
|
this.ctx.setTransform(this.zoom, 0, 0, this.zoom, this.panX, this.panY);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
this._drawConnections();
|
this._drawConnections();
|
||||||
for (const node of this.nodes) {
|
for (const node of this.nodes) {
|
||||||
|
|
@ -299,491 +469,7 @@ export class NodeEditor {
|
||||||
this.ctx.lineTo(this.draggingWire.x2, this.draggingWire.y2);
|
this.ctx.lineTo(this.draggingWire.x2, this.draggingWire.y2);
|
||||||
this.ctx.stroke();
|
this.ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
this.ctx.restore();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,8 @@ export class CanvasTextInput {
|
||||||
this.cursorVisible = false;
|
this.cursorVisible = false;
|
||||||
this.cursorTimer = 0;
|
this.cursorTimer = 0;
|
||||||
|
|
||||||
this.numericOnly = options.numericOnly || false;
|
this.mode = options.mode || "float"; // "int" or "float"
|
||||||
|
|
||||||
this.min = options.min ?? -Infinity;
|
this.min = options.min ?? -Infinity;
|
||||||
this.max = options.max ?? Infinity;
|
this.max = options.max ?? Infinity;
|
||||||
this.label = label || "";
|
this.label = label || "";
|
||||||
|
|
@ -63,24 +64,23 @@ export class CanvasTextInput {
|
||||||
|
|
||||||
handleKey(e) {
|
handleKey(e) {
|
||||||
if (!this.focused) return false;
|
if (!this.focused) return false;
|
||||||
|
console.log(e);
|
||||||
if (e.key === "Backspace") {
|
if (e.key === "Backspace") {
|
||||||
this.value = this.value.slice(0, -1);
|
this.value = this.value.slice(0, -1);
|
||||||
} else if (e.key === "Enter") {
|
} else if (e.key === "Enter") {
|
||||||
this.clampToRange(); // 👈 clamp on Enter
|
this.clampToRange(); // 👈 clamp on Enter
|
||||||
this.focused = false;
|
this.focused = false;
|
||||||
} else if (e.key.length === 1) {
|
} else if (e.key.length === 1) {
|
||||||
if (this.numericOnly) {
|
if (this.mode === "int") {
|
||||||
|
if (/[\d\-]/.test(e.key)) {
|
||||||
|
this.value += e.key;
|
||||||
|
}
|
||||||
|
} else if (this.mode === "float") {
|
||||||
if (/[\d.\-]/.test(e.key)) {
|
if (/[\d.\-]/.test(e.key)) {
|
||||||
this.value += e.key;
|
this.value += e.key;
|
||||||
|
|
||||||
this.clampToRange(); // 👈 clamp on Enter
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
this.value += e.key;
|
|
||||||
|
|
||||||
this.clampToRange(); // 👈 clamp on Enter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -96,6 +96,12 @@ export class CanvasTextInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleMouseMove(mx, my) {
|
||||||
|
// Optional: highlight or cursor change
|
||||||
|
return this.contains(mx, my);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
clampToRange() {
|
clampToRange() {
|
||||||
let val = parseFloat(this.value);
|
let val = parseFloat(this.value);
|
||||||
if (isNaN(val)) val = 0;
|
if (isNaN(val)) val = 0;
|
||||||
|
|
@ -164,6 +170,7 @@ export class CanvasDropdown {
|
||||||
if (mx >= this.x && mx <= this.x + this.width &&
|
if (mx >= this.x && mx <= this.x + this.width &&
|
||||||
my >= itemY && my <= itemY + this.height) {
|
my >= itemY && my <= itemY + this.height) {
|
||||||
this.selectedIndex = i;
|
this.selectedIndex = i;
|
||||||
|
this.selected = this.items[i]; // 👈 This line is missing
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.hoverIndex = -1;
|
this.hoverIndex = -1;
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,616 @@
|
||||||
|
import { CanvasDropdown, CanvasTextInput } from "./canvastools.js"
|
||||||
|
|
||||||
|
export const NODE_TYPES = {
|
||||||
|
Node: 0x01,
|
||||||
|
Servo: 0x02,
|
||||||
|
Curve: 0x03,
|
||||||
|
Noise: 0x04,
|
||||||
|
Variable: 0x05,
|
||||||
|
Math: 0x06,
|
||||||
|
Map: 0x07
|
||||||
|
};
|
||||||
|
|
||||||
|
export 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.hasInput = false;
|
||||||
|
this.hasOutput = false;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.updatePorts();
|
||||||
|
if (this.hasInput) {
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(this.input.x, this.input.y, 6, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "#888";
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasOutput) {
|
||||||
|
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) {
|
||||||
|
if (this.hasOutput) {
|
||||||
|
return Math.hypot(x - this.output.x, y - this.output.y) < 8;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hitInput(x, y) {
|
||||||
|
if (this.hasInput) {
|
||||||
|
return Math.hypot(x - this.input.x, y - this.input.y) < 8;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
this.hasInput = true;
|
||||||
|
this.hasOutput = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
// Node box
|
||||||
|
super.draw(ctx)
|
||||||
|
|
||||||
|
// Motor ID display
|
||||||
|
ctx.font = "12px sans-serif";
|
||||||
|
ctx.fillText(`Motor ${this.motorId}`, this.x + 10, this.y + 40);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
hitOutput(x, y) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
|
||||||
|
this.hasInput = false;
|
||||||
|
this.hasOutput = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
super.draw(ctx);
|
||||||
|
|
||||||
|
// Curve ID display
|
||||||
|
ctx.font = "12px sans-serif";
|
||||||
|
ctx.fillText(`Curve ${this.curveId}`, this.x + 10, this.y + 40);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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.type = NODE_TYPES.Variable;
|
||||||
|
this.width = 160;
|
||||||
|
this.height = 100;
|
||||||
|
|
||||||
|
this.variableDropdown = new CanvasDropdown(
|
||||||
|
10, 45, this.width - 20,
|
||||||
|
["faceDetectX", "faceDetectY", "sine", "analogRead()"],
|
||||||
|
"sine"
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
this.hasInput = false;
|
||||||
|
this.hasOutput = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
super.draw(ctx);
|
||||||
|
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MathNode extends Node {
|
||||||
|
constructor(x, y, label = "Math") {
|
||||||
|
super(x, y, label);
|
||||||
|
this.type = NODE_TYPES.Math;
|
||||||
|
this.width = 160;
|
||||||
|
this.height = 100;
|
||||||
|
|
||||||
|
this.operatorDropdown = new CanvasDropdown(
|
||||||
|
10, 45, this.width - 20,
|
||||||
|
["*", "/", "+", "-"],
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
this.valueInput = new CanvasTextInput(10, 75, this.width - 20, "1.0", "Value", { mode: "float" });
|
||||||
|
|
||||||
|
|
||||||
|
this.hasInput = true;
|
||||||
|
this.hasOutput = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
super.draw(ctx);
|
||||||
|
|
||||||
|
//ctx.fillText("Value", this.x + 10, this.y + 72);
|
||||||
|
this.valueInput.x = this.x + this.valueInput.offsetX;
|
||||||
|
this.valueInput.y = this.y + this.valueInput.offsetY;
|
||||||
|
this.valueInput.draw(ctx);
|
||||||
|
|
||||||
|
ctx.fillText("Operator", this.x + 10, this.y + 42);
|
||||||
|
this.operatorDropdown.x = this.x + this.operatorDropdown.offsetX;
|
||||||
|
this.operatorDropdown.y = this.y + this.operatorDropdown.offsetY;
|
||||||
|
this.operatorDropdown.draw(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
contains(x, y) {
|
||||||
|
return (
|
||||||
|
super.contains(x, y) ||
|
||||||
|
this.operatorDropdown.contains(x, y) ||
|
||||||
|
this.valueInput.contains(x, y)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKey(e) {
|
||||||
|
return this.valueInput.handleKey(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleClick(mx, my) {
|
||||||
|
return (
|
||||||
|
this.operatorDropdown.handleClick(mx, my) ||
|
||||||
|
this.valueInput.handleClick(mx, my)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove(mx, my) {
|
||||||
|
return (
|
||||||
|
this.operatorDropdown.handleMouseMove(mx, my) ||
|
||||||
|
this.valueInput.handleMouseMove(mx, my)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dt) {
|
||||||
|
const changedDropdown = this.operatorDropdown.update(dt);
|
||||||
|
const changedInput = this.valueInput.update(dt);
|
||||||
|
return changedDropdown || changedInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
get outputValue() {
|
||||||
|
const input = this.inputNode?.outputValue ?? 0;
|
||||||
|
const value = parseFloat(this.valueInput.text) || 0;
|
||||||
|
const op = this.operatorDropdown.selected;
|
||||||
|
|
||||||
|
switch (op) {
|
||||||
|
case "*": return input * value;
|
||||||
|
case "/": return value !== 0 ? input / value : 0;
|
||||||
|
case "+": return input + value;
|
||||||
|
case "-": return input - value;
|
||||||
|
default: return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MapNode extends Node {
|
||||||
|
constructor(x, y, label = "Map") {
|
||||||
|
super(x, y, label);
|
||||||
|
this.type = NODE_TYPES.Map;
|
||||||
|
this.width = 180;
|
||||||
|
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.hasInput = true;
|
||||||
|
this.hasOutput = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw(ctx) {
|
||||||
|
super.draw(ctx);
|
||||||
|
for (const input of [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput]) {
|
||||||
|
input.x = this.x + input.offsetX;
|
||||||
|
input.y = this.y + input.offsetY;
|
||||||
|
input.draw(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contains(x, y) {
|
||||||
|
return super.contains(x, y) || [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput].some(i => i.contains(x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKey(e) {
|
||||||
|
return (
|
||||||
|
this.inMinInput.handleKey(e) ||
|
||||||
|
this.inMaxInput.handleKey(e) ||
|
||||||
|
this.outMinInput.handleKey(e) ||
|
||||||
|
this.outMaxInput.handleKey(e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
handleClick(mx, my) {
|
||||||
|
return [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput].some(i => i.handleClick(mx, my));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseMove(mx, my) {
|
||||||
|
return [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput].some(i => i.handleMouseMove(mx, my));
|
||||||
|
}
|
||||||
|
|
||||||
|
update(dt) {
|
||||||
|
return [this.inMinInput, this.inMaxInput, this.outMinInput, this.outMaxInput].some(i => i.update(dt));
|
||||||
|
}
|
||||||
|
|
||||||
|
get outputValue() {
|
||||||
|
const input = this.inputNode?.outputValue ?? 0;
|
||||||
|
const inMin = this.inMinInput.numericValue;
|
||||||
|
const inMax = this.inMaxInput.numericValue;
|
||||||
|
const outMin = this.outMinInput.numericValue;
|
||||||
|
const outMax = this.outMaxInput.numericValue;
|
||||||
|
|
||||||
|
if (inMax === inMin) return outMin;
|
||||||
|
return ((input - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -581,8 +581,8 @@ window.onload = () => {
|
||||||
const filename = sanitizeFilename(rawInput);
|
const filename = sanitizeFilename(rawInput);
|
||||||
console.log("Sanitized filename:", filename);
|
console.log("Sanitized filename:", filename);
|
||||||
|
|
||||||
const frameCount = 800; // or whatever your timeline length is
|
const frameCount = 480; // or whatever your timeline length is
|
||||||
const frameRate = 50;
|
const frameRate = 48;
|
||||||
const version = 1;
|
const version = 1;
|
||||||
|
|
||||||
const headerSize = 16;
|
const headerSize = 16;
|
||||||
|
|
@ -611,6 +611,7 @@ window.onload = () => {
|
||||||
|
|
||||||
|
|
||||||
let curvePacket = curveEditor.encodeCurves()
|
let curvePacket = curveEditor.encodeCurves()
|
||||||
|
|
||||||
let nodeGraphPacket = nodeEditor.encodeNodeGraph();
|
let nodeGraphPacket = nodeEditor.encodeNodeGraph();
|
||||||
|
|
||||||
// 🔹 Append nodeGraphPacket
|
// 🔹 Append nodeGraphPacket
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue