NoiseNode implemented (octave and persistence hard coded), added servo feedback option to variable
parent
dae69f4c0d
commit
17deaaa873
|
|
@ -134,8 +134,8 @@ export class CurveEditor {
|
|||
this.setCurves([
|
||||
{
|
||||
startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
|
||||
startPointHandle: { x: this.valueToX(this.timelineLength / 2), y: this.valueToY(0.5) },
|
||||
endPointHandle: { x: this.valueToX(this.timelineLength / 2), y: this.valueToY(-0.5) },
|
||||
startPointHandle: { x: this.valueToX(this.timelineLength * 0.25), y: this.valueToY(0) },
|
||||
endPointHandle: { x: this.valueToX(this.timelineLength * 0.75), y: this.valueToY(0) },
|
||||
endPoint: { x: this.valueToX(this.timelineLength), y: this.valueToY(0) }
|
||||
}
|
||||
]);
|
||||
|
|
@ -149,6 +149,7 @@ export class CurveEditor {
|
|||
}
|
||||
|
||||
loadCurveSets(curveSets) {
|
||||
this.curveSets = []
|
||||
this.curveSets = curveSets;
|
||||
|
||||
// If selectedMotorID is present in the new set, load its curves
|
||||
|
|
@ -158,6 +159,7 @@ export class CurveEditor {
|
|||
this.setCurves([]); // fallback to empty
|
||||
}
|
||||
console.log("LOADED");
|
||||
console.log()
|
||||
setSelectedMotor(10); // Global defined in script.js
|
||||
//this.selectAdjacentMotor(1);
|
||||
// Optional: update motor selector UI or redraw timeline
|
||||
|
|
@ -328,6 +330,7 @@ export class CurveEditor {
|
|||
|
||||
getMotorPositionAtTime(motorID, timeInFrames) {
|
||||
if (this.curveSets[motorID] === undefined || this.curveSets[motorID].length === 0) {
|
||||
console.log("THIS");
|
||||
return null;
|
||||
}
|
||||
const curves = this.curveSets[motorID];
|
||||
|
|
@ -751,7 +754,7 @@ export class CurveEditor {
|
|||
|
||||
this.dragControlPoint(curve, 'startPointHandle', curve.startPointHandle.x, curve.startPointHandle.y, index);
|
||||
|
||||
console.log(this.yToExportRange(curve.startPoint.y));
|
||||
//console.log(this.yToExportRange(curve.startPoint.y));
|
||||
} else if (key === 'endPoint') {
|
||||
const oldX = curve.endPoint.x;
|
||||
const oldY = curve.endPoint.y;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export class ServoMotor {
|
||||
constructor(arg1, arg2, arg3) {
|
||||
constructor(arg1, arg2, arg3, arg4) {
|
||||
if (Array.isArray(arg1)) {
|
||||
// Full payload constructor
|
||||
const payload = arg1;
|
||||
|
|
@ -36,6 +36,8 @@ export class ServoMotor {
|
|||
this.ID = arg2;
|
||||
|
||||
this.MODEL = arg3 || 'Unknown Model';
|
||||
|
||||
this.NAME = arg4 || "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
index.html
18
index.html
|
|
@ -186,6 +186,8 @@
|
|||
<div id="fileListHeader">
|
||||
<span>Animations</span>
|
||||
<div id="fileActions">
|
||||
<input type="number" id="repeatCount" min="1" value="1" style="width: 50px; margin-right: 8px;"
|
||||
title="Repeat count" />
|
||||
<button id="playFile">Play</button>
|
||||
<button id="loadFile" disabled>Load</button>
|
||||
<button id="deleteFile" disabled>Delete</button>
|
||||
|
|
@ -193,16 +195,9 @@
|
|||
</div>
|
||||
<ul id="fileList"></ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="tab-pane fade" id="about" role="tabpanel" aria-labelledby="about-tab">
|
||||
|
||||
<p>Developed by Jake Wilkinson at <a href="https://realrobots.net/">RealRobots.net</a> for Hanson Robotics.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<canvas id="nodeeditor" width="800" height="600"></canvas>
|
||||
<div id="contextMenu" style="
|
||||
|
|
@ -215,8 +210,15 @@
|
|||
z-index: 1000;
|
||||
">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="sendNodes">Send Nodes</button>
|
||||
|
||||
|
||||
<div class="tab-pane fade" id="about" role="tabpanel" aria-labelledby="about-tab">
|
||||
|
||||
<p>Developed by Jake Wilkinson at <a href="https://realrobots.net/">RealRobots.net</a> for Hanson Robotics.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<textarea id="log" rows="10" cols="60" readonly></textarea><br>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
|
||||
import { ServoNode, CurveNode, VariableNode, MathNode, MapNode, NODE_TYPES } from "./nodes.js"
|
||||
import { ServoNode, CurveNode, VariableNode, NoiseNode, MathNode, MapNode, NODE_TYPES } from "./nodes.js"
|
||||
|
||||
|
||||
export class NodeEditor {
|
||||
|
|
@ -17,6 +17,7 @@ export class NodeEditor {
|
|||
this.isPanning = false;
|
||||
this.lastPan = { x: 0, y: 0 };
|
||||
this.zoom = 1.0;
|
||||
this.focusedInput = null;
|
||||
|
||||
|
||||
this._bindEvents();
|
||||
|
|
@ -33,10 +34,20 @@ export class NodeEditor {
|
|||
this.connections.push({ from: inputNode, to: outputNode });
|
||||
}
|
||||
|
||||
this.addVariableNode(50, 120, "Var");
|
||||
//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
|
||||
|
|
@ -70,25 +81,20 @@ export class NodeEditor {
|
|||
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);
|
||||
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);
|
||||
console.log(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;
|
||||
console.log(node.operatorDropdown.selectedIndex);
|
||||
console.log(node.valueInput.numericValue);
|
||||
break;
|
||||
|
||||
case NODE_TYPES.Map:
|
||||
|
|
@ -117,6 +123,109 @@ export class NodeEditor {
|
|||
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 = {}) {
|
||||
|
|
@ -199,6 +308,7 @@ export class NodeEditor {
|
|||
|
||||
}
|
||||
|
||||
|
||||
_onMouseDown(e) {
|
||||
const { x, y } = this._getMouse(e);
|
||||
|
||||
|
|
@ -210,6 +320,16 @@ export class NodeEditor {
|
|||
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)) {
|
||||
|
|
@ -312,16 +432,30 @@ export class NodeEditor {
|
|||
_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();
|
||||
break;
|
||||
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;
|
||||
|
|
@ -419,6 +553,15 @@ export class NodeEditor {
|
|||
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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export class CanvasTextInput {
|
|||
if (!this.focused) return false;
|
||||
console.log(e);
|
||||
if (e.key === "Backspace") {
|
||||
this.value = this.value.slice(0, -1);
|
||||
this.value = String(this.value).slice(0, -1);
|
||||
} else if (e.key === "Enter") {
|
||||
this.clampToRange(); // 👈 clamp on Enter
|
||||
this.focused = false;
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ export class Node {
|
|||
|
||||
this.hasInput = false;
|
||||
this.hasOutput = false;
|
||||
this.canDelete = true;
|
||||
|
||||
this.inputs = [];
|
||||
|
||||
this.updatePorts();
|
||||
}
|
||||
|
|
@ -120,6 +123,7 @@ export class ServoNode extends Node {
|
|||
|
||||
this.hasInput = true;
|
||||
this.hasOutput = false;
|
||||
this.canDelete = false;
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
|
|
@ -170,6 +174,7 @@ export class CurveNode extends Node {
|
|||
|
||||
this.hasInput = false;
|
||||
this.hasOutput = true;
|
||||
this.canDelete = false;
|
||||
}
|
||||
|
||||
draw(ctx) {
|
||||
|
|
@ -224,6 +229,8 @@ export class InputNode extends Node {
|
|||
{ defaultValue: 1.0, numericOnly: true, min: 0, max: 10 }
|
||||
);
|
||||
|
||||
this.inputs.push(this.inputField);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -277,129 +284,88 @@ 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.width = 180;
|
||||
this.height = 180;
|
||||
|
||||
this.modeDropdown = new CanvasDropdown(
|
||||
10, 35, this.width - 20,
|
||||
["impulse", "pulse", "threshold", "smooth"],
|
||||
"impulse"
|
||||
);
|
||||
this.inputFrequency = new CanvasTextInput(10, 45, this.width - 20, "1.0", "Frequency", { mode: "float" });
|
||||
this.inputSeed = new CanvasTextInput(10, 70 + 10, this.width - 20, "0", "Seed", { mode: "int" });
|
||||
|
||||
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 = [];
|
||||
this.inputs.push(this.inputFrequency);
|
||||
this.inputs.push(this.inputSeed);
|
||||
|
||||
const inputSpacing = 42; // includes label + input height
|
||||
this.hasInput = false;
|
||||
this.hasOutput = true;
|
||||
|
||||
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];
|
||||
let max = 0
|
||||
super.draw(ctx);
|
||||
for (const input of [this.inputFrequency, this.inputSeed]) {
|
||||
input.x = this.x + input.offsetX;
|
||||
input.y = this.y + input.offsetY; // 👈 vertical stacking
|
||||
input.y = this.y + input.offsetY;
|
||||
input.draw(ctx);
|
||||
}
|
||||
|
||||
// 🔹 Noise preview
|
||||
const freq = Math.max(0.01, this.inputFrequency.numericValue);
|
||||
const seed = this.inputSeed.numericValue;
|
||||
const steps = 480;
|
||||
const previewHeight = 60;
|
||||
const previewWidth = this.width - 20;
|
||||
const startX = this.x + 10;
|
||||
const startY = this.y + this.height - previewHeight - 10;
|
||||
|
||||
// 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();
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const t = i / steps * freq * 10; // scale up to get more variation
|
||||
|
||||
const noise = perlin1D_octave(seed, t);
|
||||
if (noise > max) {
|
||||
max = noise;
|
||||
//console.log(noise);
|
||||
}
|
||||
const x = startX + (i / steps) * previewWidth;
|
||||
const y = startY + previewHeight / 2 - noise * (previewHeight / 2);
|
||||
|
||||
// Draw dropdowns LAST
|
||||
this.modeDropdown.x = this.x + this.modeDropdown.offsetX;
|
||||
this.modeDropdown.y = this.y + this.modeDropdown.offsetY;
|
||||
this.modeDropdown.draw(ctx);
|
||||
if (i === 0) ctx.moveTo(x, y);
|
||||
else ctx.lineTo(x, y);
|
||||
}
|
||||
ctx.strokeStyle = "#444";
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
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));
|
||||
return super.contains(x, y) || [this.inputFrequency, this.inputSeed].some(i => i.contains(x, y));
|
||||
}
|
||||
|
||||
handleKey(e) {
|
||||
return this.inputs.some(input => input.handleKey(e));
|
||||
return (
|
||||
this.inputFrequency.handleKey(e) ||
|
||||
this.inputSeed.handleKey(e)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
handleClick(mx, my) {
|
||||
return false;//[this.inputFrequency, this.inputSeed].some(i => i.handleClick(mx, my));
|
||||
}
|
||||
|
||||
handleMouseMove(mx, my) {
|
||||
return this.modeDropdown.handleMouseMove(mx, my);
|
||||
return [this.inputFrequency, this.inputSeed].some(i => i.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;
|
||||
return [this.inputFrequency, this.inputSeed].some(i => i.update(dt));
|
||||
}
|
||||
|
||||
get outputValue() {
|
||||
return this.lastValue;
|
||||
const input = this.inputNode?.outputValue ?? 0;
|
||||
const inFrequency = this.inputFrequency.numericValue;
|
||||
const inSeed = this.inputSeed.numericValue;
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -412,10 +378,12 @@ export class VariableNode extends Node {
|
|||
|
||||
this.variableDropdown = new CanvasDropdown(
|
||||
10, 45, this.width - 20,
|
||||
["faceDetectX", "faceDetectY", "sine", "analogRead()"],
|
||||
["faceDetectX", "faceDetectY", "sine", "analogRead()", "servo"],
|
||||
"sine"
|
||||
);
|
||||
|
||||
this.arg0Input = new CanvasTextInput(10, 75, this.width - 20, "0", "Motor ID", { mode: "int" });
|
||||
this.inputs.push(this.variableDropdown);
|
||||
this.inputs.push(this.arg0Input);
|
||||
|
||||
this.hasInput = false;
|
||||
this.hasOutput = true;
|
||||
|
|
@ -428,40 +396,42 @@ export class VariableNode extends Node {
|
|||
// Dropdown label
|
||||
ctx.fillText("Source", this.x + 10, this.y + 42);
|
||||
|
||||
if (this.variableDropdown.selected === "servo") {
|
||||
this.arg0Input.x = this.x + this.arg0Input.offsetX;
|
||||
this.arg0Input.y = this.y + this.arg0Input.offsetY;
|
||||
this.arg0Input.draw(ctx);
|
||||
}
|
||||
|
||||
// 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);
|
||||
return super.contains(x, y) || [this.variableDropdown, this.arg0Input].some(i => i.contains(x, y));
|
||||
}
|
||||
|
||||
handleKey(e) {
|
||||
return (
|
||||
this.arg0Input.handleKey(e)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
handleClick(mx, my) {
|
||||
return this.variableDropdown.handleClick(mx, my);
|
||||
return false;//[this.inputFrequency, this.inputSeed].some(i => i.handleClick(mx, my));
|
||||
}
|
||||
|
||||
handleMouseMove(mx, my) {
|
||||
return this.variableDropdown.handleMouseMove(mx, my);
|
||||
return [this.variableDropdown, this.arg0Input].some(i => i.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;
|
||||
return [this.variableDropdown, this.arg0Input].some(i => i.update(dt));
|
||||
}
|
||||
|
||||
get outputValue() {
|
||||
|
|
@ -474,7 +444,7 @@ export class MathNode extends Node {
|
|||
super(x, y, label);
|
||||
this.type = NODE_TYPES.Math;
|
||||
this.width = 160;
|
||||
this.height = 100;
|
||||
this.height = 110;
|
||||
|
||||
this.operatorDropdown = new CanvasDropdown(
|
||||
10, 45, this.width - 20,
|
||||
|
|
@ -483,6 +453,8 @@ export class MathNode extends Node {
|
|||
);
|
||||
this.valueInput = new CanvasTextInput(10, 75, this.width - 20, "1.0", "Value", { mode: "float" });
|
||||
|
||||
this.inputs.push(this.operatorDropdown);
|
||||
this.inputs.push(this.valueInput);
|
||||
|
||||
this.hasInput = true;
|
||||
this.hasOutput = true;
|
||||
|
|
@ -558,9 +530,14 @@ export class MapNode extends Node {
|
|||
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.inMaxInput = new CanvasTextInput(10, 70 + 10, this.width - 20, "4095", "In Max", { mode: "float" });
|
||||
this.outMinInput = new CanvasTextInput(10, 95 + 20, this.width - 20, "1024", "Out Min", { mode: "float" });
|
||||
this.outMaxInput = new CanvasTextInput(10, 120 + 30, this.width - 20, "3072", "Out Max", { mode: "float" });
|
||||
|
||||
this.inputs.push(this.inMinInput);
|
||||
this.inputs.push(this.inMaxInput);
|
||||
this.inputs.push(this.outMinInput);
|
||||
this.inputs.push(this.outMaxInput);
|
||||
|
||||
this.hasInput = true;
|
||||
this.hasOutput = true;
|
||||
|
|
@ -614,3 +591,78 @@ export class MapNode extends Node {
|
|||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const fade = t => t * t * t * (t * (t * 6 - 15) + 10);
|
||||
const lerp = (a, b, t) => a + t * (b - a);
|
||||
|
||||
function grad(hash, x) {
|
||||
const h = hash & 15;
|
||||
let grad = 1 + (h & 7); // 1 to 8
|
||||
if (h & 8) grad = -grad;
|
||||
return (grad * x) / 4;
|
||||
//return (hash & 1 ? -1 : 1) * x;
|
||||
}
|
||||
|
||||
|
||||
const perlinCache = new Map();
|
||||
|
||||
function perlin1D_octave(seed, x, octaves = 2, persistence = 0.5) {
|
||||
let total = 0;
|
||||
let amplitude = 1;
|
||||
let frequency = 1;
|
||||
let maxValue = 0;
|
||||
|
||||
for (let i = 0; i < octaves; i++) {
|
||||
total += perlin1D(seed, x * frequency) * amplitude;
|
||||
maxValue += amplitude;
|
||||
amplitude *= persistence;
|
||||
frequency *= 2;
|
||||
}
|
||||
|
||||
return total / maxValue;
|
||||
}
|
||||
|
||||
function perlin1D(seed, x) {
|
||||
let seedCache = perlinCache.get(seed);
|
||||
if (!seedCache) {
|
||||
seedCache = new Map();
|
||||
perlinCache.set(seed, seedCache);
|
||||
}
|
||||
|
||||
const key = Math.round(x * 1000) / 1000; // round to reduce precision noise
|
||||
if (seedCache.has(key)) {
|
||||
return seedCache.get(key);
|
||||
}
|
||||
|
||||
const perm = generatePermutation(seed);
|
||||
const xi = Math.floor(x) & 255;
|
||||
const xf = x - Math.floor(x);
|
||||
const u = fade(xf);
|
||||
|
||||
const a = perm[xi];
|
||||
const b = perm[xi + 1];
|
||||
|
||||
const result = lerp(grad(a, xf), grad(b, xf - 1), u);
|
||||
seedCache.set(key, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function generatePermutation(seed) {
|
||||
const perm = new Array(512);
|
||||
const p = new Array(256);
|
||||
let s = seed;
|
||||
for (let i = 0; i < 256; i++) {
|
||||
s = (s * 1664525 + 1013904223) % 4294967296;
|
||||
p[i] = i;
|
||||
}
|
||||
for (let i = 255; i > 0; i--) {
|
||||
const j = s % (i + 1);
|
||||
[p[i], p[j]] = [p[j], p[i]];
|
||||
s = (s * 1664525 + 1013904223) % 4294967296;
|
||||
}
|
||||
for (let i = 0; i < 512; i++) {
|
||||
perm[i] = p[i & 255];
|
||||
}
|
||||
return perm;
|
||||
}
|
||||
|
|
|
|||
41
robot.js
41
robot.js
|
|
@ -2,8 +2,8 @@ import { ServoMotor } from './feetechDefinitions.js';
|
|||
|
||||
export class Robot {
|
||||
constructor(name, firmwareVersionId) {
|
||||
this.name = name;
|
||||
this.firmwareVersionId = firmwareVersionId;
|
||||
this.deviceName = name;
|
||||
this.firmwareVersion = { major: 0, minor: 0 };
|
||||
|
||||
// Map of position ID → ServoMotor
|
||||
this.positionMap = new Map();
|
||||
|
|
@ -43,4 +43,41 @@ export class Robot {
|
|||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static fromBytes(bytes) {
|
||||
console.log("Loading Robot from:");
|
||||
console.log(bytes);
|
||||
const data = bytes instanceof Uint8Array ? bytes : Uint8Array.from(bytes);
|
||||
let offset = 0;
|
||||
|
||||
// Device name
|
||||
const nameLen = data[offset++];
|
||||
const deviceName = String.fromCharCode(...data.slice(offset, offset + nameLen));
|
||||
offset += nameLen;
|
||||
|
||||
// Firmware version
|
||||
const firmwareMajor = data[offset++];
|
||||
const firmwareMinor = data[offset++];
|
||||
|
||||
// Create Robot instance
|
||||
const robot = new Robot(deviceName, `${firmwareMajor}.${firmwareMinor}`);
|
||||
robot.firmwareVersion.major = firmwareMajor;
|
||||
robot.firmwareVersion.minor = firmwareMinor;
|
||||
|
||||
// Motor count
|
||||
const motorCount = data[offset++];
|
||||
|
||||
for (let i = 0; i < motorCount; i++) {
|
||||
const motorID = data[offset++];
|
||||
const nameLen = data[offset++];
|
||||
const name = String.fromCharCode(...data.slice(offset, offset + nameLen));
|
||||
offset += nameLen;
|
||||
|
||||
const position = (data[offset++] << 8) | data[offset++];
|
||||
const motor = new ServoMotor(0, motorID, null, name);
|
||||
robot.assignMotor(motorID, motor); // motorID used as position ID
|
||||
}
|
||||
|
||||
return robot;
|
||||
};
|
||||
}
|
||||
|
|
|
|||
91
script.js
91
script.js
|
|
@ -36,7 +36,7 @@ window.onload = () => {
|
|||
|
||||
const dialColors = ['red', 'green', 'blue', 'orange', 'purple'];
|
||||
|
||||
const dials = [];
|
||||
let dials = [];
|
||||
const frameSlider = document.getElementById('frameSlider');
|
||||
const frameDisplay = document.getElementById('frameDisplay');
|
||||
|
||||
|
|
@ -56,24 +56,13 @@ window.onload = () => {
|
|||
let selectedFile = null;
|
||||
|
||||
|
||||
let connectedRobot = GenerateTestRobot();
|
||||
let connectedRobot = null;//GenerateTestRobot();
|
||||
|
||||
|
||||
console.log(connectedRobot);
|
||||
let motorIDList = []
|
||||
for (const [position, motor] of connectedRobot.positionMap.entries()) {
|
||||
console.log(`Assigning ${position} motor with ID ${motor.ID}`);
|
||||
curveEditor.addChannel(motor.ID);
|
||||
motorIDList.push(motor.ID);
|
||||
}
|
||||
setSelectedMotor(10);
|
||||
|
||||
const nodeCanvas = document.getElementById("nodeeditor");
|
||||
let nodeEditor = null;
|
||||
|
||||
const nodeEditor = new NodeEditor(nodeCanvas, {
|
||||
motorIds: motorIDList
|
||||
});
|
||||
|
||||
nodeEditor.generateDefaultNodes(curveEditor.curveSets, motorIDList);
|
||||
|
||||
//nodeEditor.addServoNode(400, 150, "Servo Output", 5 );
|
||||
// nodeEditor.addInputNode(100, 500, "Input Nod", { defaultValue: 3 });
|
||||
|
|
@ -83,6 +72,26 @@ window.onload = () => {
|
|||
// nodeEditor.addNode(100, 100, "Time", { fill: "#e0f7e9", stroke: "#2e7d32" }); // mint green
|
||||
// nodeEditor.addNode(300, 200, "Output"); // uses default pastel
|
||||
|
||||
function onConnectRobot(robot) {
|
||||
connectedRobot = robot;
|
||||
console.log(connectedRobot);
|
||||
let motorIDList = []
|
||||
clearDials();
|
||||
for (const [position, motor] of connectedRobot.positionMap.entries()) {
|
||||
console.log(`Assigning ${position} motor with ID ${motor.ID}`);
|
||||
curveEditor.addChannel(motor.ID);
|
||||
motorIDList.push(motor.ID);
|
||||
addDial(motor.ID, motor.NAME);
|
||||
}
|
||||
setSelectedMotor(10);
|
||||
|
||||
|
||||
nodeEditor = new NodeEditor(nodeCanvas, {
|
||||
motorIds: motorIDList
|
||||
});
|
||||
|
||||
nodeEditor.generateDefaultNodes(curveEditor.curveSets, motorIDList);
|
||||
}
|
||||
|
||||
|
||||
function setSelectedMotor(motorID) {
|
||||
|
|
@ -166,8 +175,8 @@ window.onload = () => {
|
|||
const filenameBytes = new TextEncoder().encode(filename);
|
||||
const filenameLength = filenameBytes.length;
|
||||
|
||||
// Total size: 2 bytes for length + filename bytes
|
||||
const buffer = new ArrayBuffer(2 + filenameLength);
|
||||
// Total size: 2 bytes for length + filename bytes + oneshot/loop tag + loopCount
|
||||
const buffer = new ArrayBuffer(2 + filenameLength + 2);
|
||||
const view = new DataView(buffer);
|
||||
let offset = 0;
|
||||
|
||||
|
|
@ -175,6 +184,16 @@ window.onload = () => {
|
|||
view.setUint16(offset, filenameLength, true); offset += 2;
|
||||
filenameBytes.forEach(byte => view.setUint8(offset++, byte));
|
||||
|
||||
const ONESHOT = 0x01; // play once
|
||||
const LOOP = 0x02; // loop endlessly
|
||||
const REPEAT = 0x03; // followed by loop count
|
||||
|
||||
const repeatCount = parseInt(document.getElementById("repeatCount").value, 10);
|
||||
|
||||
let playTag = REPEAT;
|
||||
view.setUint8(offset++, playTag);
|
||||
view.setUint8(offset++, repeatCount);
|
||||
|
||||
const payload = new Uint8Array(buffer);
|
||||
//serial.deleteFile(payload); // CMD_DELETE_FILE
|
||||
|
||||
|
|
@ -230,7 +249,14 @@ window.onload = () => {
|
|||
|
||||
};
|
||||
|
||||
function addDial(motorID) {
|
||||
function clearDials(){
|
||||
const dialArea = document.getElementById('dialArea');
|
||||
dialArea.innerHTML = ''; // Remove all child elements
|
||||
dials = [];
|
||||
}
|
||||
|
||||
function addDial(motorID, motorName) {
|
||||
|
||||
const index = dials.length;
|
||||
|
||||
// Create dial wrapper
|
||||
|
|
@ -240,8 +266,10 @@ window.onload = () => {
|
|||
|
||||
// Create label
|
||||
const label = document.createElement('label');
|
||||
label.textContent = "Motor " + motorID;
|
||||
console.log(motorID);
|
||||
label.textContent = "MotorID " + motorID;
|
||||
|
||||
const label2 = document.createElement('label2');
|
||||
label2.textContent = motorName;
|
||||
|
||||
// Create dial container
|
||||
const dialDiv = document.createElement('div');
|
||||
|
|
@ -254,6 +282,7 @@ window.onload = () => {
|
|||
|
||||
// Assemble and append
|
||||
dialWrapper.appendChild(label);
|
||||
dialWrapper.appendChild(label2);
|
||||
dialWrapper.appendChild(dialDiv);
|
||||
dialWrapper.appendChild(valueSpan);
|
||||
document.getElementById('dialArea').appendChild(dialWrapper);
|
||||
|
|
@ -286,6 +315,7 @@ window.onload = () => {
|
|||
dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -341,6 +371,8 @@ window.onload = () => {
|
|||
switch (command) {
|
||||
case 0x01: // ID response
|
||||
text = new TextDecoder().decode(new Uint8Array(payload));
|
||||
onConnectRobot(Robot.fromBytes(new Uint8Array(payload)));
|
||||
console.log(connectedRobot);
|
||||
document.getElementById('log').value += `ID Response: ${text}\n`;
|
||||
break;
|
||||
|
||||
|
|
@ -499,8 +531,7 @@ window.onload = () => {
|
|||
console.log(raw);
|
||||
|
||||
for (let i = 0; i < curveCount; i++) {
|
||||
const motorID = 10;
|
||||
offset += 1;
|
||||
const motorID = view.getUint8(offset++);
|
||||
const startTime = view.getUint16(offset, true); offset += 2;
|
||||
const endTime = view.getUint16(offset, true); offset += 2;
|
||||
const startPointY = view.getInt16(offset, true); offset += 2;
|
||||
|
|
@ -547,11 +578,13 @@ window.onload = () => {
|
|||
|
||||
console.log("🎯 Loaded Curves:", curveSets);
|
||||
|
||||
// 🔁 Inject into your curve editor
|
||||
//loadCurvesIntoEditor(curves); // Replace with your actual editor hook
|
||||
|
||||
curveEditor.loadCurveSets(curveSets);
|
||||
curveEditor.setLength(latestEndTime);
|
||||
|
||||
if (offset < view.byteLength) {
|
||||
const nodeGraphData = raw.slice(offset); // grab remaining bytes
|
||||
nodeEditor.loadFromBinary(nodeGraphData); // call your editor's loader
|
||||
}
|
||||
|
||||
// 🔓 Unlock buttons
|
||||
loadButton.disabled = false;
|
||||
|
|
@ -675,12 +708,6 @@ window.onload = () => {
|
|||
document.getElementById('input').value = '';
|
||||
};
|
||||
|
||||
document.getElementById('sendNodes').onclick = async () => {
|
||||
nodeGraphPacket = nodeEditor.encodeNodeGraph();
|
||||
curvePacket = curveEditor.encodeCurves()
|
||||
|
||||
};
|
||||
|
||||
|
||||
document.addEventListener('click', (e) => {
|
||||
const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];
|
||||
|
|
@ -948,7 +975,7 @@ window.onload = () => {
|
|||
console.log("ERROR: INCORRECT PACKET SIZE: " + payload.length);
|
||||
return;
|
||||
}
|
||||
const motor = new ServoMotor(payload);
|
||||
const motor = new ServoMotor(Array.from(payload));
|
||||
console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED);
|
||||
servoMotors[motor.CHANNEL].push(motor);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue