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([
|
this.setCurves([
|
||||||
{
|
{
|
||||||
startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
|
startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
|
||||||
startPointHandle: { 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 / 2), y: this.valueToY(-0.5) },
|
endPointHandle: { x: this.valueToX(this.timelineLength * 0.75), y: this.valueToY(0) },
|
||||||
endPoint: { x: this.valueToX(this.timelineLength), y: this.valueToY(0) }
|
endPoint: { x: this.valueToX(this.timelineLength), y: this.valueToY(0) }
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
@ -149,6 +149,7 @@ export class CurveEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCurveSets(curveSets) {
|
loadCurveSets(curveSets) {
|
||||||
|
this.curveSets = []
|
||||||
this.curveSets = curveSets;
|
this.curveSets = curveSets;
|
||||||
|
|
||||||
// If selectedMotorID is present in the new set, load its curves
|
// If selectedMotorID is present in the new set, load its curves
|
||||||
|
|
@ -158,6 +159,7 @@ export class CurveEditor {
|
||||||
this.setCurves([]); // fallback to empty
|
this.setCurves([]); // fallback to empty
|
||||||
}
|
}
|
||||||
console.log("LOADED");
|
console.log("LOADED");
|
||||||
|
console.log()
|
||||||
setSelectedMotor(10); // Global defined in script.js
|
setSelectedMotor(10); // Global defined in script.js
|
||||||
//this.selectAdjacentMotor(1);
|
//this.selectAdjacentMotor(1);
|
||||||
// Optional: update motor selector UI or redraw timeline
|
// Optional: update motor selector UI or redraw timeline
|
||||||
|
|
@ -328,6 +330,7 @@ export class CurveEditor {
|
||||||
|
|
||||||
getMotorPositionAtTime(motorID, timeInFrames) {
|
getMotorPositionAtTime(motorID, timeInFrames) {
|
||||||
if (this.curveSets[motorID] === undefined || this.curveSets[motorID].length === 0) {
|
if (this.curveSets[motorID] === undefined || this.curveSets[motorID].length === 0) {
|
||||||
|
console.log("THIS");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const curves = this.curveSets[motorID];
|
const curves = this.curveSets[motorID];
|
||||||
|
|
@ -751,7 +754,7 @@ export class CurveEditor {
|
||||||
|
|
||||||
this.dragControlPoint(curve, 'startPointHandle', curve.startPointHandle.x, curve.startPointHandle.y, index);
|
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') {
|
} else if (key === 'endPoint') {
|
||||||
const oldX = curve.endPoint.x;
|
const oldX = curve.endPoint.x;
|
||||||
const oldY = curve.endPoint.y;
|
const oldY = curve.endPoint.y;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export class ServoMotor {
|
export class ServoMotor {
|
||||||
constructor(arg1, arg2, arg3) {
|
constructor(arg1, arg2, arg3, arg4) {
|
||||||
if (Array.isArray(arg1)) {
|
if (Array.isArray(arg1)) {
|
||||||
// Full payload constructor
|
// Full payload constructor
|
||||||
const payload = arg1;
|
const payload = arg1;
|
||||||
|
|
@ -36,6 +36,8 @@ export class ServoMotor {
|
||||||
this.ID = arg2;
|
this.ID = arg2;
|
||||||
|
|
||||||
this.MODEL = arg3 || 'Unknown Model';
|
this.MODEL = arg3 || 'Unknown Model';
|
||||||
|
|
||||||
|
this.NAME = arg4 || "UNKNOWN";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
32
index.html
32
index.html
|
|
@ -186,6 +186,8 @@
|
||||||
<div id="fileListHeader">
|
<div id="fileListHeader">
|
||||||
<span>Animations</span>
|
<span>Animations</span>
|
||||||
<div id="fileActions">
|
<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="playFile">Play</button>
|
||||||
<button id="loadFile" disabled>Load</button>
|
<button id="loadFile" disabled>Load</button>
|
||||||
<button id="deleteFile" disabled>Delete</button>
|
<button id="deleteFile" disabled>Delete</button>
|
||||||
|
|
@ -193,6 +195,21 @@
|
||||||
</div>
|
</div>
|
||||||
<ul id="fileList"></ul>
|
<ul id="fileList"></ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -204,21 +221,6 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
|
|
||||||
<textarea id="log" rows="10" cols="60" readonly></textarea><br>
|
<textarea id="log" rows="10" cols="60" readonly></textarea><br>
|
||||||
<input type="text" id="input" placeholder="Type message here">
|
<input type="text" id="input" placeholder="Type message here">
|
||||||
<button id="send">Send</button>
|
<button id="send">Send</button>
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
export class NodeEditor {
|
||||||
|
|
@ -17,6 +17,7 @@ export class NodeEditor {
|
||||||
this.isPanning = false;
|
this.isPanning = false;
|
||||||
this.lastPan = { x: 0, y: 0 };
|
this.lastPan = { x: 0, y: 0 };
|
||||||
this.zoom = 1.0;
|
this.zoom = 1.0;
|
||||||
|
this.focusedInput = null;
|
||||||
|
|
||||||
|
|
||||||
this._bindEvents();
|
this._bindEvents();
|
||||||
|
|
@ -33,10 +34,20 @@ export class NodeEditor {
|
||||||
this.connections.push({ from: inputNode, to: outputNode });
|
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() {
|
encodeNodeGraph() {
|
||||||
const bufferSize = 1024; // adjust based on expected graph size
|
const bufferSize = 1024; // adjust based on expected graph size
|
||||||
|
|
@ -70,25 +81,20 @@ export class NodeEditor {
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NODE_TYPES.Noise:
|
case NODE_TYPES.Noise:
|
||||||
view.setFloat32(offset, node.rate, true); offset += 4;
|
view.setFloat32(offset, node.inputFrequency.numericValue, true); offset += 4;
|
||||||
view.setFloat32(offset, node.threshold, true); offset += 4;
|
view.setUint16(offset, node.inputSeed.numericValue); offset += 2;
|
||||||
view.setFloat32(offset, node.pulseWidth, true); offset += 4;
|
|
||||||
view.setFloat32(offset, node.amplitude, true); offset += 4;
|
|
||||||
view.setUint8(offset++, node.seed);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case NODE_TYPES.Variable:
|
case NODE_TYPES.Variable:
|
||||||
view.setUint8(offset++, node.variableDropdown.selectedIndex);
|
view.setUint8(offset++, node.variableDropdown.selectedIndex);
|
||||||
console.log(node.variableDropdown.selectedIndex);
|
view.setUint8(offset++, node.arg0Input.numericValue);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
||||||
case NODE_TYPES.Math:
|
case NODE_TYPES.Math:
|
||||||
view.setUint8(offset++, node.operatorDropdown.selectedIndex);
|
view.setUint8(offset++, node.operatorDropdown.selectedIndex);
|
||||||
view.setFloat32(offset, node.valueInput.numericValue, true); offset += 4;
|
view.setFloat32(offset, node.valueInput.numericValue, true); offset += 4;
|
||||||
console.log(node.operatorDropdown.selectedIndex);
|
|
||||||
console.log(node.valueInput.numericValue);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case NODE_TYPES.Map:
|
case NODE_TYPES.Map:
|
||||||
|
|
@ -117,6 +123,109 @@ export class NodeEditor {
|
||||||
return new Uint8Array(buffer.slice(0, offset));
|
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 = {}) {
|
addNode(x, y, label, options = {}) {
|
||||||
|
|
@ -199,6 +308,7 @@ export class NodeEditor {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_onMouseDown(e) {
|
_onMouseDown(e) {
|
||||||
const { x, y } = this._getMouse(e);
|
const { x, y } = this._getMouse(e);
|
||||||
|
|
||||||
|
|
@ -210,6 +320,16 @@ export class NodeEditor {
|
||||||
return;
|
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) {
|
for (const node of this.nodes) {
|
||||||
if (node.contains(x, y)) {
|
if (node.contains(x, y)) {
|
||||||
|
|
@ -312,16 +432,30 @@ export class NodeEditor {
|
||||||
_onDoubleClick(e) {
|
_onDoubleClick(e) {
|
||||||
const { x, y } = this._getMouse(e);
|
const { x, y } = this._getMouse(e);
|
||||||
|
|
||||||
|
// First: check for wire deletion
|
||||||
for (let i = this.connections.length - 1; i >= 0; i--) {
|
for (let i = this.connections.length - 1; i >= 0; i--) {
|
||||||
const conn = this.connections[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)) {
|
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.connections.splice(i, 1);
|
||||||
this._redraw();
|
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) {
|
_getMouse(e) {
|
||||||
const rect = this.canvas.getBoundingClientRect();
|
const rect = this.canvas.getBoundingClientRect();
|
||||||
const screenX = e.clientX - rect.left;
|
const screenX = e.clientX - rect.left;
|
||||||
|
|
@ -419,6 +553,15 @@ export class NodeEditor {
|
||||||
hideMenu();
|
hideMenu();
|
||||||
};
|
};
|
||||||
menu.appendChild(item);
|
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;
|
if (!this.focused) return false;
|
||||||
console.log(e);
|
console.log(e);
|
||||||
if (e.key === "Backspace") {
|
if (e.key === "Backspace") {
|
||||||
this.value = this.value.slice(0, -1);
|
this.value = String(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;
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,9 @@ export class Node {
|
||||||
|
|
||||||
this.hasInput = false;
|
this.hasInput = false;
|
||||||
this.hasOutput = false;
|
this.hasOutput = false;
|
||||||
|
this.canDelete = true;
|
||||||
|
|
||||||
|
this.inputs = [];
|
||||||
|
|
||||||
this.updatePorts();
|
this.updatePorts();
|
||||||
}
|
}
|
||||||
|
|
@ -120,6 +123,7 @@ export class ServoNode extends Node {
|
||||||
|
|
||||||
this.hasInput = true;
|
this.hasInput = true;
|
||||||
this.hasOutput = false;
|
this.hasOutput = false;
|
||||||
|
this.canDelete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(ctx) {
|
draw(ctx) {
|
||||||
|
|
@ -170,6 +174,7 @@ export class CurveNode extends Node {
|
||||||
|
|
||||||
this.hasInput = false;
|
this.hasInput = false;
|
||||||
this.hasOutput = true;
|
this.hasOutput = true;
|
||||||
|
this.canDelete = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
draw(ctx) {
|
draw(ctx) {
|
||||||
|
|
@ -224,6 +229,8 @@ export class InputNode extends Node {
|
||||||
{ defaultValue: 1.0, numericOnly: true, min: 0, max: 10 }
|
{ 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") {
|
constructor(x, y, label = "Noise Generator") {
|
||||||
super(x, y, label);
|
super(x, y, label);
|
||||||
this.type = NODE_TYPES.Noise;
|
this.type = NODE_TYPES.Noise;
|
||||||
this.width = 160;
|
this.width = 180;
|
||||||
this.height = 280;
|
this.height = 180;
|
||||||
|
|
||||||
this.modeDropdown = new CanvasDropdown(
|
this.inputFrequency = new CanvasTextInput(10, 45, this.width - 20, "1.0", "Frequency", { mode: "float" });
|
||||||
10, 35, this.width - 20,
|
this.inputSeed = new CanvasTextInput(10, 70 + 10, this.width - 20, "0", "Seed", { mode: "int" });
|
||||||
["impulse", "pulse", "threshold", "smooth"],
|
|
||||||
"impulse"
|
|
||||||
);
|
|
||||||
|
|
||||||
const inputConfigs = [
|
this.inputs.push(this.inputFrequency);
|
||||||
{ label: "Rate", value: "1.0", min: 0, max: 100 },
|
this.inputs.push(this.inputSeed);
|
||||||
{ 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
|
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) {
|
draw(ctx) {
|
||||||
ctx.fillStyle = this.color || "#f0e6ff";
|
let max = 0
|
||||||
ctx.strokeStyle = this.border || "#333";
|
super.draw(ctx);
|
||||||
ctx.lineWidth = 1;
|
for (const input of [this.inputFrequency, this.inputSeed]) {
|
||||||
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.x = this.x + input.offsetX;
|
||||||
input.y = this.y + input.offsetY; // 👈 vertical stacking
|
input.y = this.y + input.offsetY;
|
||||||
input.draw(ctx);
|
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.beginPath();
|
||||||
ctx.arc(this.output.x, this.output.y, 6, 0, Math.PI * 2);
|
for (let i = 0; i < steps; i++) {
|
||||||
ctx.fillStyle = "#888";
|
const t = i / steps * freq * 10; // scale up to get more variation
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
|
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
|
if (i === 0) ctx.moveTo(x, y);
|
||||||
this.modeDropdown.x = this.x + this.modeDropdown.offsetX;
|
else ctx.lineTo(x, y);
|
||||||
this.modeDropdown.y = this.y + this.modeDropdown.offsetY;
|
}
|
||||||
this.modeDropdown.draw(ctx);
|
ctx.strokeStyle = "#444";
|
||||||
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
|
|
||||||
contains(x, y) {
|
contains(x, y) {
|
||||||
return super.contains(x, y) ||
|
return super.contains(x, y) || [this.inputFrequency, this.inputSeed].some(i => i.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) {
|
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) {
|
handleMouseMove(mx, my) {
|
||||||
return this.modeDropdown.handleMouseMove(mx, my);
|
return [this.inputFrequency, this.inputSeed].some(i => i.handleMouseMove(mx, my));
|
||||||
}
|
}
|
||||||
|
|
||||||
update(dt) {
|
update(dt) {
|
||||||
let needsRedraw = this.modeDropdown.update(dt);
|
return [this.inputFrequency, this.inputSeed].some(i => i.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() {
|
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(
|
this.variableDropdown = new CanvasDropdown(
|
||||||
10, 45, this.width - 20,
|
10, 45, this.width - 20,
|
||||||
["faceDetectX", "faceDetectY", "sine", "analogRead()"],
|
["faceDetectX", "faceDetectY", "sine", "analogRead()", "servo"],
|
||||||
"sine"
|
"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.hasInput = false;
|
||||||
this.hasOutput = true;
|
this.hasOutput = true;
|
||||||
|
|
@ -428,40 +396,42 @@ export class VariableNode extends Node {
|
||||||
// Dropdown label
|
// Dropdown label
|
||||||
ctx.fillText("Source", this.x + 10, this.y + 42);
|
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
|
// Dropdown
|
||||||
this.variableDropdown.x = this.x + this.variableDropdown.offsetX;
|
this.variableDropdown.x = this.x + this.variableDropdown.offsetX;
|
||||||
this.variableDropdown.y = this.y + this.variableDropdown.offsetY;
|
this.variableDropdown.y = this.y + this.variableDropdown.offsetY;
|
||||||
this.variableDropdown.draw(ctx);
|
this.variableDropdown.draw(ctx);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
contains(x, y) {
|
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) {
|
handleClick(mx, my) {
|
||||||
return this.variableDropdown.handleClick(mx, my);
|
return false;//[this.inputFrequency, this.inputSeed].some(i => i.handleClick(mx, my));
|
||||||
}
|
}
|
||||||
|
|
||||||
handleMouseMove(mx, my) {
|
handleMouseMove(mx, my) {
|
||||||
return this.variableDropdown.handleMouseMove(mx, my);
|
return [this.variableDropdown, this.arg0Input].some(i => i.handleMouseMove(mx, my));
|
||||||
}
|
}
|
||||||
|
|
||||||
update(dt) {
|
update(dt) {
|
||||||
const changed = this.variableDropdown.update(dt);
|
return [this.variableDropdown, this.arg0Input].some(i => i.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() {
|
get outputValue() {
|
||||||
|
|
@ -474,7 +444,7 @@ export class MathNode extends Node {
|
||||||
super(x, y, label);
|
super(x, y, label);
|
||||||
this.type = NODE_TYPES.Math;
|
this.type = NODE_TYPES.Math;
|
||||||
this.width = 160;
|
this.width = 160;
|
||||||
this.height = 100;
|
this.height = 110;
|
||||||
|
|
||||||
this.operatorDropdown = new CanvasDropdown(
|
this.operatorDropdown = new CanvasDropdown(
|
||||||
10, 45, this.width - 20,
|
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.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.hasInput = true;
|
||||||
this.hasOutput = true;
|
this.hasOutput = true;
|
||||||
|
|
@ -558,9 +530,14 @@ export class MapNode extends Node {
|
||||||
this.height = 180;
|
this.height = 180;
|
||||||
|
|
||||||
this.inMinInput = new CanvasTextInput(10, 45, this.width - 20, "0", "In Min", { mode: "float" });
|
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.inMaxInput = new CanvasTextInput(10, 70 + 10, this.width - 20, "4095", "In Max", { mode: "float" });
|
||||||
this.outMinInput = new CanvasTextInput(10, 95+20, this.width - 20, "0", "Out Min", { 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, "255", "Out Max", { 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.hasInput = true;
|
||||||
this.hasOutput = true;
|
this.hasOutput = true;
|
||||||
|
|
@ -580,13 +557,13 @@ export class MapNode extends Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKey(e) {
|
handleKey(e) {
|
||||||
return (
|
return (
|
||||||
this.inMinInput.handleKey(e) ||
|
this.inMinInput.handleKey(e) ||
|
||||||
this.inMaxInput.handleKey(e) ||
|
this.inMaxInput.handleKey(e) ||
|
||||||
this.outMinInput.handleKey(e) ||
|
this.outMinInput.handleKey(e) ||
|
||||||
this.outMaxInput.handleKey(e)
|
this.outMaxInput.handleKey(e)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
handleClick(mx, my) {
|
handleClick(mx, my) {
|
||||||
|
|
@ -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 {
|
export class Robot {
|
||||||
constructor(name, firmwareVersionId) {
|
constructor(name, firmwareVersionId) {
|
||||||
this.name = name;
|
this.deviceName = name;
|
||||||
this.firmwareVersionId = firmwareVersionId;
|
this.firmwareVersion = { major: 0, minor: 0 };
|
||||||
|
|
||||||
// Map of position ID → ServoMotor
|
// Map of position ID → ServoMotor
|
||||||
this.positionMap = new Map();
|
this.positionMap = new Map();
|
||||||
|
|
@ -43,4 +43,41 @@ export class Robot {
|
||||||
}
|
}
|
||||||
return null;
|
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 dialColors = ['red', 'green', 'blue', 'orange', 'purple'];
|
||||||
|
|
||||||
const dials = [];
|
let dials = [];
|
||||||
const frameSlider = document.getElementById('frameSlider');
|
const frameSlider = document.getElementById('frameSlider');
|
||||||
const frameDisplay = document.getElementById('frameDisplay');
|
const frameDisplay = document.getElementById('frameDisplay');
|
||||||
|
|
||||||
|
|
@ -56,24 +56,13 @@ window.onload = () => {
|
||||||
let selectedFile = null;
|
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");
|
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.addServoNode(400, 150, "Servo Output", 5 );
|
||||||
// nodeEditor.addInputNode(100, 500, "Input Nod", { defaultValue: 3 });
|
// 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(100, 100, "Time", { fill: "#e0f7e9", stroke: "#2e7d32" }); // mint green
|
||||||
// nodeEditor.addNode(300, 200, "Output"); // uses default pastel
|
// 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) {
|
function setSelectedMotor(motorID) {
|
||||||
|
|
@ -166,8 +175,8 @@ window.onload = () => {
|
||||||
const filenameBytes = new TextEncoder().encode(filename);
|
const filenameBytes = new TextEncoder().encode(filename);
|
||||||
const filenameLength = filenameBytes.length;
|
const filenameLength = filenameBytes.length;
|
||||||
|
|
||||||
// Total size: 2 bytes for length + filename bytes
|
// Total size: 2 bytes for length + filename bytes + oneshot/loop tag + loopCount
|
||||||
const buffer = new ArrayBuffer(2 + filenameLength);
|
const buffer = new ArrayBuffer(2 + filenameLength + 2);
|
||||||
const view = new DataView(buffer);
|
const view = new DataView(buffer);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
||||||
|
|
@ -175,6 +184,16 @@ window.onload = () => {
|
||||||
view.setUint16(offset, filenameLength, true); offset += 2;
|
view.setUint16(offset, filenameLength, true); offset += 2;
|
||||||
filenameBytes.forEach(byte => view.setUint8(offset++, byte));
|
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);
|
const payload = new Uint8Array(buffer);
|
||||||
//serial.deleteFile(payload); // CMD_DELETE_FILE
|
//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;
|
const index = dials.length;
|
||||||
|
|
||||||
// Create dial wrapper
|
// Create dial wrapper
|
||||||
|
|
@ -240,8 +266,10 @@ window.onload = () => {
|
||||||
|
|
||||||
// Create label
|
// Create label
|
||||||
const label = document.createElement('label');
|
const label = document.createElement('label');
|
||||||
label.textContent = "Motor " + motorID;
|
label.textContent = "MotorID " + motorID;
|
||||||
console.log(motorID);
|
|
||||||
|
const label2 = document.createElement('label2');
|
||||||
|
label2.textContent = motorName;
|
||||||
|
|
||||||
// Create dial container
|
// Create dial container
|
||||||
const dialDiv = document.createElement('div');
|
const dialDiv = document.createElement('div');
|
||||||
|
|
@ -254,6 +282,7 @@ window.onload = () => {
|
||||||
|
|
||||||
// Assemble and append
|
// Assemble and append
|
||||||
dialWrapper.appendChild(label);
|
dialWrapper.appendChild(label);
|
||||||
|
dialWrapper.appendChild(label2);
|
||||||
dialWrapper.appendChild(dialDiv);
|
dialWrapper.appendChild(dialDiv);
|
||||||
dialWrapper.appendChild(valueSpan);
|
dialWrapper.appendChild(valueSpan);
|
||||||
document.getElementById('dialArea').appendChild(dialWrapper);
|
document.getElementById('dialArea').appendChild(dialWrapper);
|
||||||
|
|
@ -286,6 +315,7 @@ window.onload = () => {
|
||||||
dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame);
|
dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -341,6 +371,8 @@ window.onload = () => {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case 0x01: // ID response
|
case 0x01: // ID response
|
||||||
text = new TextDecoder().decode(new Uint8Array(payload));
|
text = new TextDecoder().decode(new Uint8Array(payload));
|
||||||
|
onConnectRobot(Robot.fromBytes(new Uint8Array(payload)));
|
||||||
|
console.log(connectedRobot);
|
||||||
document.getElementById('log').value += `ID Response: ${text}\n`;
|
document.getElementById('log').value += `ID Response: ${text}\n`;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|
@ -499,8 +531,7 @@ window.onload = () => {
|
||||||
console.log(raw);
|
console.log(raw);
|
||||||
|
|
||||||
for (let i = 0; i < curveCount; i++) {
|
for (let i = 0; i < curveCount; i++) {
|
||||||
const motorID = 10;
|
const motorID = view.getUint8(offset++);
|
||||||
offset += 1;
|
|
||||||
const startTime = view.getUint16(offset, true); offset += 2;
|
const startTime = view.getUint16(offset, true); offset += 2;
|
||||||
const endTime = view.getUint16(offset, true); offset += 2;
|
const endTime = view.getUint16(offset, true); offset += 2;
|
||||||
const startPointY = view.getInt16(offset, true); offset += 2;
|
const startPointY = view.getInt16(offset, true); offset += 2;
|
||||||
|
|
@ -547,11 +578,13 @@ window.onload = () => {
|
||||||
|
|
||||||
console.log("🎯 Loaded Curves:", curveSets);
|
console.log("🎯 Loaded Curves:", curveSets);
|
||||||
|
|
||||||
// 🔁 Inject into your curve editor
|
|
||||||
//loadCurvesIntoEditor(curves); // Replace with your actual editor hook
|
|
||||||
curveEditor.loadCurveSets(curveSets);
|
curveEditor.loadCurveSets(curveSets);
|
||||||
curveEditor.setLength(latestEndTime);
|
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
|
// 🔓 Unlock buttons
|
||||||
loadButton.disabled = false;
|
loadButton.disabled = false;
|
||||||
|
|
@ -675,12 +708,6 @@ window.onload = () => {
|
||||||
document.getElementById('input').value = '';
|
document.getElementById('input').value = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('sendNodes').onclick = async () => {
|
|
||||||
nodeGraphPacket = nodeEditor.encodeNodeGraph();
|
|
||||||
curvePacket = curveEditor.encodeCurves()
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];
|
const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];
|
||||||
|
|
@ -948,7 +975,7 @@ window.onload = () => {
|
||||||
console.log("ERROR: INCORRECT PACKET SIZE: " + payload.length);
|
console.log("ERROR: INCORRECT PACKET SIZE: " + payload.length);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const motor = new ServoMotor(payload);
|
const motor = new ServoMotor(Array.from(payload));
|
||||||
console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED);
|
console.log(motor.MODEL, motor.POSITION, motor.CURRENT_SPEED);
|
||||||
servoMotors[motor.CHANNEL].push(motor);
|
servoMotors[motor.CHANNEL].push(motor);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue