sophia_controller/nodeeditor/nodes.js

617 lines
16 KiB
JavaScript
Raw Normal View History

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;
}
}