a few interface tweaks

master
Jake Wilkinson 2025-11-24 11:30:36 +08:00
parent 5a8dbc28a7
commit 105db83017
5 changed files with 149 additions and 135 deletions

View File

@ -11,6 +11,14 @@ export class CurveEditor {
this.scaleX = 1; this.scaleX = 1;
this.scaleY = 1; this.scaleY = 1;
this.offset = { x: 0, y: 0 }; this.offset = { x: 0, y: 0 };
this.scaleX = 1.66;
this.scaleY = 0.41;
this.offset.x = 177;
this.offset.y = 38;
this.pixelsPerSecond = 48; this.pixelsPerSecond = 48;
this.exportRange = [0, 4095]; this.exportRange = [0, 4095];
@ -69,8 +77,8 @@ export class CurveEditor {
const curveSegments = []; const curveSegments = [];
Object.entries(this.curveSets).forEach(([motorIDStr, segments]) => { Object.entries(this.curveSets).forEach(([motorIDStr, segments]) => {
const motorID = parseInt(motorIDStr, 10); const motorID = parseInt(motorIDStr, 10);
segments.forEach(segment => {
segments.forEach(segment => {
curveSegments.push({ curveSegments.push({
motorID, motorID,
startTime: segment.startPoint.x, startTime: segment.startPoint.x,
@ -84,14 +92,14 @@ export class CurveEditor {
}); });
}); });
}); });
console.log(curveSegments); //console.log(curveSegments);
const curveCount = curveSegments.length; const curveCount = curveSegments.length;
view.setUint16(offset, curveCount, true); offset += 2; view.setUint16(offset, curveCount, true); offset += 2;
// 🔹 Curve segments // 🔹 Curve segments
curveSegments.forEach(seg => { curveSegments.forEach(seg => {
console.log(offset, seg.motorID); //console.log(offset, seg.motorID);
view.setUint8(offset++, seg.motorID); view.setUint8(offset++, seg.motorID);
view.setUint16(offset, seg.startTime, true); offset += 2; view.setUint16(offset, seg.startTime, true); offset += 2;
view.setUint16(offset, seg.endTime, true); offset += 2; view.setUint16(offset, seg.endTime, true); offset += 2;
@ -101,7 +109,7 @@ export class CurveEditor {
view.setUint16(offset, seg.endHandleX, true); offset += 2; view.setUint16(offset, seg.endHandleX, true); offset += 2;
view.setInt16(offset, this.yToExportRange(seg.endHandleY), true); offset += 2; view.setInt16(offset, this.yToExportRange(seg.endHandleY), true); offset += 2;
view.setInt16(offset, this.yToExportRange(seg.endPointY), true); offset += 2; view.setInt16(offset, this.yToExportRange(seg.endPointY), true); offset += 2;
console.log(seg.startPointY, seg.endPointY); //console.log(seg.startPointY, seg.endPointY);
console.log(this.yToExportRange(seg.startPointY), this.yToExportRange(seg.endPointY)); console.log(this.yToExportRange(seg.startPointY), this.yToExportRange(seg.endPointY));
}); });
//console.log("🧵 Curve segments packed:", curveSegments.length); //console.log("🧵 Curve segments packed:", curveSegments.length);
@ -190,7 +198,7 @@ export class CurveEditor {
// } // }
valueToYNoOffset(v) { valueToYNoOffset(v) {
return (this.canvas.height / 2 - v * (this.canvas.height / 2)); return (this.logicalHeight / 2) * (1 - v); // Keep mapping logic as is
} }
yToValueNoOffset(y) { yToValueNoOffset(y) {
@ -206,42 +214,44 @@ export class CurveEditor {
return ((this.logicalHeight / 2 - (y - this.offset.y) / this.scaleY) / (this.logicalHeight / 2)); return ((this.logicalHeight / 2 - (y - this.offset.y) / this.scaleY) / (this.logicalHeight / 2));
} }
//
yToMotorPosition(yCanvas) {
const [minOut, maxOut] = this.exportRange; // Output bounds (0 - 4095)
// Maps normalised -1 to 1 value to motor range (0, 4095) // Transform yCanvas to the original logical space by adjusting for the current offset and scale.
yToExportRange(yCanvas) { const yLogical = (yCanvas - this.offset.y) / this.scaleY;
// Normalize the logical value based on the actual height of the logical range
const normalized = yLogical / 800; // This assumes yLogical ranges from 0 to 800
// Flip normalized to convert to desired motor range
const flipped = normalized; // This gives the inverted mapping
// Calculate the final motor position
return Math.round(flipped * (maxOut - minOut) + minOut);
}
// exportRangeToY(value) {
// const [minOut, maxOut] = this.exportRange;
// const normalized = 1 - (value - minOut) / (maxOut - minOut);
// const logical = normalized * 2 - 1;
// const yLogical = this.valueToYNoOffset(logical);
// return yLogical * this.scaleY + this.offset.y;
// }
yToExportRange(y) {
const [minOut, maxOut] = this.exportRange; const [minOut, maxOut] = this.exportRange;
const clampedY = Math.max(0, Math.min(y, this.logicalHeight)); // optional safety
// undo offset + scale to get logical Y const normalized = clampedY / this.logicalHeight; // maps to [0, 1]
const yLogical = (yCanvas - this.offset.y) / this.scaleY; return Math.round(normalized * (maxOut - minOut) + minOut); // maps to [minOut, maxOut]
// invert valueToYNoOffset: get back logical [-1,1]
const logical = this.yToValueNoOffset(yLogical);
// map logical [-1,1] → normalized [0,1]
const normalized = (logical + 1) / 2;
// flip back (since exportRangeToY used 1 - normalized)
const flipped = 1 - normalized;
// map to export range
return (flipped * (maxOut - minOut) + minOut)/2;
} }
exportRangeToY(value) { exportRangeToY(value) {
const [minOut, maxOut] = this.exportRange; const [minOut, maxOut] = this.exportRange;
// Inverted normalization
// normalize value into [0,1], flipped
const normalized = 1 - (value - minOut) / (maxOut - minOut); const normalized = 1 - (value - minOut) / (maxOut - minOut);
return this.valueToYNoOffset(normalized * 2 - 1); // maps [0, 1] to [-1, 1]
// map into logical [-1,1]
const logical = normalized * 2 - 1;
// convert logical → logical Y
const yLogical = this.valueToYNoOffset(logical);
// apply scale + offset to get canvas Y
return yLogical * this.scaleY + this.offset.y;
} }
@ -406,7 +416,7 @@ export class CurveEditor {
if (currentTimeX >= minX && currentTimeX <= maxX) { if (currentTimeX >= minX && currentTimeX <= maxX) {
const t = this.solveTForX(currentTimeX, p0, h0, h1, p1); const t = this.solveTForX(currentTimeX, p0, h0, h1, p1);
const pt = this.cubicBezier(t, p0, h0, h1, p1); const pt = this.cubicBezier(t, p0, h0, h1, p1);
return this.yToExportRange(pt.y); // or pt.y if you want raw canvas Y return this.yToMotorPosition(pt.y); // or pt.y if you want raw canvas Y
} }
} }
@ -731,7 +741,7 @@ export class CurveEditor {
this.offset.x += (after.x - before.x) * this.scaleX; this.offset.x += (after.x - before.x) * this.scaleX;
this.offset.y += (after.y - before.y) * this.scaleY; this.offset.y += (after.y - before.y) * this.scaleY;
console.log(this.scaleX, this.scaleY, this.offset.x, this.offset.y);
this.draw(); this.draw();
}); });
@ -748,88 +758,74 @@ export class CurveEditor {
} }
splitCurveAtTime(motorID, timeX, yPosition = null) { splitCurveAtTime(motorID, timeX, yPosition = null) {
const curves = this.curveSets[motorID]; const curves = this.curveSets[motorID];
if (!curves) return; if (!curves) return;
for (let i = 0; i < curves.length; i++) { for (let i = 0; i < curves.length; i++) {
const curve = curves[i]; const curve = curves[i];
// --- merge with previous if near startPoint --- // --- if timeX matches an existing startPoint ---
if (i > 0 && Math.abs(curve.startPoint.x - timeX) < 1e-3) { if (Math.abs(curve.startPoint.x - timeX) < 1e-3) {
const prev = curves[i - 1]; if (yPosition !== null) {
const merged = { const yEditor = (yPosition * 800 / 4095);
startPoint: prev.startPoint, curve.startPoint.y = yEditor;
startPointHandle: prev.startPointHandle, if (curve.startPointHandle) curve.startPointHandle.y = yEditor;
endPointHandle: curve.endPointHandle, }
endPoint: curve.endPoint this.draw();
}; return;
curves.splice(i - 1, 2, merged);
this.curveSets[motorID] = curves;
this.draw();
return;
}
// --- merge with next if near endPoint ---
if (i < curves.length - 1 && Math.abs(curve.endPoint.x - timeX) < 1e-3) {
const next = curves[i + 1];
const merged = {
startPoint: curve.startPoint,
startPointHandle: curve.startPointHandle,
endPointHandle: next.endPointHandle,
endPoint: next.endPoint
};
curves.splice(i, 2, merged);
this.curveSets[motorID] = curves;
this.draw();
return;
}
// --- split inside curve if timeX lies between start and end ---
const x0 = curve.startPoint.x;
const x3 = curve.endPoint.x;
if (timeX >= x0 && timeX <= x3) {
// binary search for t where curve.x ≈ timeX
let t = 0.5, minT = 0, maxT = 1;
for (let j = 0; j < 10; j++) {
const pt = this.cubicBezier(
t,
curve.startPoint,
curve.startPointHandle,
curve.endPointHandle,
curve.endPoint
);
if (pt.x < timeX) minT = t;
else maxT = t;
t = (minT + maxT) / 2;
}
let [left, right] = this.splitCurve(curve, t);
// ✅ If yPosition provided, override the split points Y
if (yPosition !== null) {
const yEditor = yPosition * 800 / 4095
// adjust the shared split point
left.endPoint.y = yEditor;
right.startPoint.y = yEditor;
// ✅ flatten handles to same Y
if (left.endPointHandle) left.endPointHandle.y = yEditor;
if (right.startPointHandle) right.startPointHandle.y = yEditor;
}
curves.splice(i, 1, left, right);
this.curveSets[motorID] = curves;
this.draw();
return;
}
}
} }
// --- if timeX matches an existing endPoint ---
if (Math.abs(curve.endPoint.x - timeX) < 1e-3) {
if (yPosition !== null) {
const yEditor = (yPosition * 800 / 4095);
curve.endPoint.y = yEditor;
if (curve.endPointHandle) curve.endPointHandle.y = yEditor;
}
this.draw();
return;
}
// --- split inside curve if timeX lies between start and end ---
const x0 = curve.startPoint.x;
const x3 = curve.endPoint.x;
if (timeX > x0 && timeX < x3) {
// binary search for t where curve.x ≈ timeX
let t = 0.5, minT = 0, maxT = 1;
for (let j = 0; j < 10; j++) {
const pt = this.cubicBezier(
t,
curve.startPoint,
curve.startPointHandle,
curve.endPointHandle,
curve.endPoint
);
if (pt.x < timeX) minT = t;
else maxT = t;
t = (minT + maxT) / 2;
}
let [left, right] = this.splitCurve(curve, t);
// ✅ If yPosition provided, override the split points Y
if (yPosition !== null) {
const yEditor = (yPosition * 800 / 4095);
left.endPoint.y = yEditor;
right.startPoint.y = yEditor;
if (left.endPointHandle) left.endPointHandle.y = yEditor;
if (right.startPointHandle) right.startPointHandle.y = yEditor;
}
curves.splice(i, 1, left, right);
this.curveSets[motorID] = curves;
this.draw();
return;
}
}
}

View File

@ -11,7 +11,7 @@ export const NODE_TYPES = {
}; };
function GetNodeType(node) { function GetNodeType(node) {
console.log(node.constructor.name); //console.log(node.constructor.name);
switch (node.constructor.name) { switch (node.constructor.name) {
case "ServoNode": case "ServoNode":
return NODE_TYPES.Servo; return NODE_TYPES.Servo;

View File

@ -18,11 +18,12 @@ const SyncMode = {
}; };
export class URDFEditor { export class URDFEditor {
constructor(canvas, sendMotorPosition, serial, curveEditor) { constructor(canvas, sendMotorPosition, serial, curveEditor, tryConnect) {
this.canvas = canvas; this.canvas = canvas;
this.sendMotorPosition = sendMotorPosition; this.sendMotorPosition = sendMotorPosition;
this.serial = serial; this.serial = serial;
this.curveEditor = curveEditor; this.curveEditor = curveEditor;
this.tryConnect = tryConnect;
this.scene = new THREE.Scene(); this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xaaaaaa); this.scene.background = new THREE.Color(0xaaaaaa);
@ -74,7 +75,7 @@ export class URDFEditor {
this.setupScene(); this.setupScene();
//this.loadURDF(); //this.loadURDF();
this.loadURDFFromIndexedDB(); //this.loadURDFFromIndexedDB();
this.setupEvents(); this.setupEvents();
const editorCallbacks = {} const editorCallbacks = {}
@ -478,11 +479,14 @@ export class URDFEditor {
let positions = []; let positions = [];
allIDs.forEach(id => { allIDs.forEach(id => {
let pos = this.curveEditor.getMotorPositionAtTime(id, this.curveEditor.currentTime); let pos = this.curveEditor.getMotorPositionAtTime(id, this.curveEditor.currentTime);
this.setMotorPosition(id, pos); if (this.currentSyncMode !== SyncMode.RealToSim) {
this.setMotorPosition(id, pos);
}
ids.push(id); ids.push(id);
positions.push(pos); positions.push(pos);
}); });
if (this.currentSyncMode === SyncMode.SimToReal) { if (this.currentSyncMode === SyncMode.SimToReal) {
this.sendMotorPosition(ids, positions); this.sendMotorPosition(ids, positions);
} }

View File

@ -54,7 +54,7 @@ export class ViewerOverlay {
window.addEventListener('resize', resizeOverlay); window.addEventListener('resize', resizeOverlay);
resizeOverlay(); resizeOverlay();
this.panels.push(this.createAnimationControlPanel()); //this.panels.push(this.createAnimationControlPanel());
this.panels.push(this.createSystemPanel()); this.panels.push(this.createSystemPanel());
const handlePointerEvent = (event) => { const handlePointerEvent = (event) => {
const rect = this.overlayCanvas.getBoundingClientRect(); const rect = this.overlayCanvas.getBoundingClientRect();
@ -100,9 +100,13 @@ export class ViewerOverlay {
init(robot) { init(robot) {
this.robot = robot; this.robot = robot;
this.motorListPanel = this.createMotorListPanel(this.overlayCtx); this.motorListPanel = this.createMotorListPanel(this.overlayCtx);
this.animationPanel = this.createAnimationControlPanel(this.overlayCtx);
if (this.motorListPanel) { if (this.motorListPanel) {
this.panels.push(this.motorListPanel); this.panels.push(this.motorListPanel);
} }
if (this.animationPanel) {
this.panels.push(this.animationPanel);
}
this.draw(); this.draw();
} }
@ -111,7 +115,7 @@ export class ViewerOverlay {
// filter out the motorListPanel from panels // filter out the motorListPanel from panels
this.panels = this.panels.filter(p => p !== this.motorListPanel); this.panels = this.panels.filter(p => p !== this.motorListPanel);
this.panels = this.panels.filter(p => p !== this.animationPanel);
// clear the reference // clear the reference
this.motorListPanel = null; this.motorListPanel = null;
} }
@ -172,21 +176,21 @@ export class ViewerOverlay {
panel.addElement(new Button(panel.x + 200, panel.y + 40, 200, 24, "Record Keyframe ALL", () => { panel.addElement(new Button(panel.x + 200, panel.y + 40, 200, 24, "Record Keyframe ALL", () => {
let allIDs = this.parent.findAllMotorIDs(); let allIDs = this.parent.findAllMotorIDs();
for (var i = 0; i < allIDs.length; i++){ for (var i = 0; i < allIDs.length; i++) {
this.parent.curveEditor.splitCurveAtTime(allIDs[i], this.parent.curveEditor.currentTime, this.parent.getMotorTicks(allIDs[i])); this.parent.curveEditor.splitCurveAtTime(allIDs[i], this.parent.curveEditor.currentTime, this.parent.getMotorTicks(allIDs[i]));
} }
})); }));
panel.addElement(new Button(panel.x + 200, panel.y + 40 + 24*2, 200, 24, "Record Keyframe Selected", () => { panel.addElement(new Button(panel.x + 200, panel.y + 40 + 24 * 2, 200, 24, "Record Keyframe Selected", () => {
if (!this.parent.selectedJoint){ if (!this.parent.selectedJoint) {
return; return;
} }
const selectedID = this.parent.findJointAncestor(this.parent.selectedJoint, 0).transmission.motorID const selectedID = this.parent.findJointAncestor(this.parent.selectedJoint, 0).transmission.motorID
const currentTime = this.parent.curveEditor.currentTime; const currentTime = this.parent.curveEditor.currentTime;
const motorPosition = this.parent.getMotorTicks(selectedID) const motorPosition = this.parent.getMotorTicks(selectedID)
this.parent.curveEditor.splitCurveAtTime(selectedID, currentTime, motorPosition); this.parent.curveEditor.splitCurveAtTime(selectedID, currentTime, motorPosition);
console.log(motorPosition, this.parent.curveEditor.exportRangeToY(motorPosition), this.parent.curveEditor.yToExportRange(this.parent.curveEditor.exportRangeToY(motorPosition))); //console.log(motorPosition, this.parent.curveEditor.exportRangeToY(motorPosition), this.parent.curveEditor.yToExportRange(this.parent.curveEditor.exportRangeToY(motorPosition)));
//console.log(selectedID, currentTime, motorPosition); console.log(selectedID, currentTime, motorPosition);
})); }));
return panel; return panel;
@ -202,32 +206,37 @@ export class ViewerOverlay {
const panel = new Panel(x, y, w, h, "System"); const panel = new Panel(x, y, w, h, "System");
panel.addElement(new Button(x, y + 28, 50, 24, "Connect", () => {
// delegate to editors save
this.parent.tryConnect();
}));
// Save button // Save button
panel.addElement(new Button(x, y + 28, 50, 24, "Save", () => { panel.addElement(new Button(x, y + 28 + 24 * 1, 50, 24, "Save", () => {
// delegate to editors save // delegate to editors save
this.parent.saveURDFToIndexedDB(this.robot); this.parent.saveURDFToIndexedDB(this.robot);
})); }));
// Load button // Load button
panel.addElement(new Button(x, y + 28 + 24 * 1, 50, 24, "Load", () => { panel.addElement(new Button(x, y + 28 + 24 * 2, 50, 24, "Load", () => {
// delegate to editors load // delegate to editors load
this.parent.loadURDFFromIndexedDB(); this.parent.loadURDFFromIndexedDB();
})); }));
panel.addElement(new Button(x, y + 28 + 24 * 2, 50, 24, "Download", () => { panel.addElement(new Button(x, y + 28 + 24 * 3, 50, 24, "Download", () => {
// delegate to editors load // delegate to editors load
this.parent.downloadURDF(); this.parent.downloadURDF();
})); }));
panel.addElement(new Button(x, y + 28 + 24 * 3, 50, 24, "Upload", () => { panel.addElement(new Button(x, y + 28 + 24 * 4, 50, 24, "Upload", () => {
// delegate to editors load // delegate to editors load
this.parent.uploadURDF(); this.parent.uploadURDF();
})); }));
panel.addElement(new Button(x, y + 28 + 24 * 5, 50, 24, "Calibrate", () => { panel.addElement(new Button(x, y + 28 + 24 * 6, 50, 24, "Calibrate", () => {
this.applyCalibrationOffsets(); this.parent.applyCalibrationOffsets();
})); }));

View File

@ -41,7 +41,7 @@ window.onload = () => {
const serial = new SerialManager(); const serial = new SerialManager();
const urdfCanvas = document.getElementById('urdfCanvas'); const urdfCanvas = document.getElementById('urdfCanvas');
const visualEditor = new URDFEditor(urdfCanvas, sendMotorPosition, serial, curveEditor); const visualEditor = new URDFEditor(urdfCanvas, sendMotorPosition, serial, curveEditor, tryConnect);
@ -94,6 +94,7 @@ window.onload = () => {
}); });
nodeEditor.generateDefaultNodes(curveEditor.curveSets, motorIDList); nodeEditor.generateDefaultNodes(curveEditor.curveSets, motorIDList);
visualEditor.loadURDFFromIndexedDB();
} }
@ -291,8 +292,7 @@ window.onload = () => {
} }
} }
// Connect button async function tryConnect() {
document.getElementById('connect').addEventListener('click', async () => {
try { try {
await serial.connect(); await serial.connect();
statusText.textContent = 'Connected ✅'; statusText.textContent = 'Connected ✅';
@ -412,6 +412,11 @@ window.onload = () => {
statusText.textContent = 'Connection failed ❌'; statusText.textContent = 'Connection failed ❌';
console.error("Connection error:", err); console.error("Connection error:", err);
} }
}
// Connect button
document.getElementById('connect').addEventListener('click', async () => {
tryConnect();
}); });
function handlePositionStreamPacket(data) { function handlePositionStreamPacket(data) {