sophia_controller/nodeeditor/NodeEditor.js

621 lines
19 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, NoiseNode, MathNode, MapNode, NODE_TYPES } from "./nodes.js"
export class NodeEditor {
constructor(canvas, options = {}) {
this.canvas = canvas;
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
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.focusedInput = null;
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");
}
getNodeByID(id) {
return this.nodes.find(node => node.id === id) || null;
}
getAllInputs() {
let inputs = [];
for (var i = 0; i < this.nodes.length; i++) {
inputs.push(...this.nodes[i].inputs);
}
return inputs;
}
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.inputFrequency.numericValue, true); offset += 4;
view.setUint16(offset, node.inputSeed.numericValue); offset += 2;
break;
case NODE_TYPES.Variable:
view.setUint8(offset++, node.variableDropdown.selectedIndex);
view.setUint8(offset++, node.arg0Input.numericValue);
break;
case NODE_TYPES.Math:
view.setUint8(offset++, node.operatorDropdown.selectedIndex);
view.setFloat32(offset, node.valueInput.numericValue, true); offset += 4;
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));
}
loadFromBinary(data) {
this.nodes = []
console.log(this.connections);
this.connections = []
const view = new DataView(data.buffer);
let offset = 0;
const nodeCount = view.getUint8(offset++);
const idMap = {}; // Map node IDs to actual node instances
for (let i = 0; i < nodeCount; i++) {
const type = view.getUint8(offset++);
const id = view.getUint8(offset++);
const x = view.getUint16(offset, true); offset += 2;
const y = view.getUint16(offset, true); offset += 2;
let node = null;
switch (type) {
case NODE_TYPES.Servo: {
const motorID = view.getUint8(offset++);
node = this.addServoNode(x, y, "Servo Output", motorID);
break;
}
case NODE_TYPES.Curve: {
const curveID = view.getUint8(offset++);
node = this.addCurveNode(x, y, "Curve", curveID);
break;
}
case NODE_TYPES.Noise: {
const frequency = view.getFloat32(offset, true); offset += 4;
const seed = view.getUint16(offset, true); offset += 2;
node = this.addNoiseNode(x, y, "Noise");
node.inputFrequency.value = frequency;
node.inputSeed.value = seed;
break;
}
case NODE_TYPES.Variable: {
const source = view.getUint8(offset++);
const arg0 = view.getUint8(offset++);
node = this.addVariableNode(x, y, "Variable");
node.variableDropdown.selectedIndex = source;
node.arg0Input.value = arg0;
break;
}
case NODE_TYPES.Math: {
const op = view.getUint8(offset++);
const value = view.getFloat32(offset, true); offset += 4;
node = this.addMathNode(x, y, "Math");
console.log(op, value);
node.operatorDropdown.selectedIndex = op;
node.valueInput.value = value;
break;
}
case NODE_TYPES.Map: {
const inMin = view.getFloat32(offset, true); offset += 4;
const inMax = view.getFloat32(offset, true); offset += 4;
const outMin = view.getFloat32(offset, true); offset += 4;
const outMax = view.getFloat32(offset, true); offset += 4;
node = this.addMapNode(x, y, "Map");
node.inMinInput.value = inMin;
node.inMaxInput.value = inMax;
node.outMinInput.value = outMin;
node.outMaxInput.value = outMax;
break;
}
default: {
node = this.addNode(x, y, "Unknown Node");
break;
}
}
if (node) {
node.id = id;
idMap[id] = node;
}
}
console.log(this.getNodeByID(0));
// 🔗 Load connections
this.connection = [];
const connectionCount = view.getUint8(offset++);
for (let i = 0; i < connectionCount; i++) {
const fromID = view.getUint8(offset++);
const toID = view.getUint8(offset++);
const fromNode = idMap[fromID];
const toNode = idMap[toID];
if (fromNode && toNode) {
this.connections.push({ from: fromNode, to: toNode });
}
}
// Reencode all positions to SIGNED ints
this.nodes.forEach(node => {
node.x = (node.x << 16) >> 16;
node.y = (node.y << 16) >> 16;
});
this._redraw();
}
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;
}
this.focusedInput = null;
let allInputs = this.getAllInputs();
for (const input of allInputs) {
if (input.handleClick(x, y)) {
this.focusedInput = input;
} else {
input.focused = false;
}
}
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);
// First: check for wire deletion
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();
return;
}
}
// Second: check for node deletion
for (let i = this.nodes.length - 1; i >= 0; i--) {
const node = this.nodes[i];
if (node.contains(x, y) && node.canDelete) {
// Remove connections to/from this node
this.connections = this.connections.filter(conn => conn.from !== node && conn.to !== node);
this.nodes.splice(i, 1);
this._redraw();
return;
}
}
}
_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);
item = document.createElement("div");
item.className = "menu-item";
item.textContent = " Add Noise Node";
item.onclick = () => {
this.addNoiseNode(canvasX, canvasY, "Noise");
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();
}
}