sophia_controller/nodeeditor/NodeEditor.js

476 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { ServoNode, CurveNode, VariableNode, MathNode, MapNode, NODE_TYPES } from "./nodes.js"
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.panX = 0;
this.panY = 0;
this.isPanning = false;
this.lastPan = { x: 0, y: 0 };
this.zoom = 1.0;
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 * 140, "Curve", motorIDs[i]);
let outputNode = this.addServoNode(500, 40 + i * 140, "Servo Output", motorIDs[i]);
this.connections.push({ from: inputNode, to: outputNode });
}
this.addVariableNode(50, 120, "Var");
}
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;
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:
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;
}
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() {
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));
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) {
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) {
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;
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) {
//if (node instanceof ServoNode || node instanceof VariableNode) {
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.isPanning) {
this.isPanning = false;
}
if (this.draggingWire) {
for (const node of this.nodes) {
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 });
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();
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 {
x: canvasX,
y: canvasY,
screenX,
screenY
};
}
_onKeyDown(e) {
let needsRedraw = false;
for (const node of this.nodes) {
if (node.handleKey && node.handleKey(e)) {
needsRedraw = true;
}
}
if (needsRedraw) this._redraw();
}
_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) {
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;
}
_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() {
this.ctx.strokeStyle = "#444";
this.ctx.lineWidth = 2;
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.moveTo(x1, y1);
this.ctx.bezierCurveTo(
x1 + dx, y1,
x2 - dx, y2,
x2, y2
);
this.ctx.stroke();
}
}
_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._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();
}
this.ctx.restore();
}
}