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.scaleY = 1;
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.exportRange = [0, 4095];
@ -69,8 +77,8 @@ export class CurveEditor {
const curveSegments = [];
Object.entries(this.curveSets).forEach(([motorIDStr, segments]) => {
const motorID = parseInt(motorIDStr, 10);
segments.forEach(segment => {
segments.forEach(segment => {
curveSegments.push({
motorID,
startTime: segment.startPoint.x,
@ -84,14 +92,14 @@ export class CurveEditor {
});
});
});
console.log(curveSegments);
//console.log(curveSegments);
const curveCount = curveSegments.length;
view.setUint16(offset, curveCount, true); offset += 2;
// 🔹 Curve segments
curveSegments.forEach(seg => {
console.log(offset, seg.motorID);
//console.log(offset, seg.motorID);
view.setUint8(offset++, seg.motorID);
view.setUint16(offset, seg.startTime, 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.setInt16(offset, this.yToExportRange(seg.endHandleY), 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("🧵 Curve segments packed:", curveSegments.length);
@ -190,7 +198,7 @@ export class CurveEditor {
// }
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) {
@ -206,42 +214,44 @@ export class CurveEditor {
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)
yToExportRange(yCanvas) {
// Transform yCanvas to the original logical space by adjusting for the current offset and scale.
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;
// undo offset + scale to get logical Y
const yLogical = (yCanvas - this.offset.y) / this.scaleY;
// 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;
const clampedY = Math.max(0, Math.min(y, this.logicalHeight)); // optional safety
const normalized = clampedY / this.logicalHeight; // maps to [0, 1]
return Math.round(normalized * (maxOut - minOut) + minOut); // maps to [minOut, maxOut]
}
exportRangeToY(value) {
const [minOut, maxOut] = this.exportRange;
// normalize value into [0,1], flipped
// Inverted normalization
const normalized = 1 - (value - minOut) / (maxOut - minOut);
// 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;
return this.valueToYNoOffset(normalized * 2 - 1); // maps [0, 1] to [-1, 1]
}
@ -406,7 +416,7 @@ export class CurveEditor {
if (currentTimeX >= minX && currentTimeX <= maxX) {
const t = this.solveTForX(currentTimeX, 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.y += (after.y - before.y) * this.scaleY;
console.log(this.scaleX, this.scaleY, this.offset.x, this.offset.y);
this.draw();
});
@ -748,88 +758,74 @@ export class CurveEditor {
}
splitCurveAtTime(motorID, timeX, yPosition = null) {
const curves = this.curveSets[motorID];
if (!curves) return;
const curves = this.curveSets[motorID];
if (!curves) return;
for (let i = 0; i < curves.length; i++) {
const curve = curves[i];
for (let i = 0; i < curves.length; i++) {
const curve = curves[i];
// --- merge with previous if near startPoint ---
if (i > 0 && Math.abs(curve.startPoint.x - timeX) < 1e-3) {
const prev = curves[i - 1];
const merged = {
startPoint: prev.startPoint,
startPointHandle: prev.startPointHandle,
endPointHandle: curve.endPointHandle,
endPoint: curve.endPoint
};
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 startPoint ---
if (Math.abs(curve.startPoint.x - timeX) < 1e-3) {
if (yPosition !== null) {
const yEditor = (yPosition * 800 / 4095);
curve.startPoint.y = yEditor;
if (curve.startPointHandle) curve.startPointHandle.y = yEditor;
}
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) {
console.log(node.constructor.name);
//console.log(node.constructor.name);
switch (node.constructor.name) {
case "ServoNode":
return NODE_TYPES.Servo;

View File

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

View File

@ -54,7 +54,7 @@ export class ViewerOverlay {
window.addEventListener('resize', resizeOverlay);
resizeOverlay();
this.panels.push(this.createAnimationControlPanel());
//this.panels.push(this.createAnimationControlPanel());
this.panels.push(this.createSystemPanel());
const handlePointerEvent = (event) => {
const rect = this.overlayCanvas.getBoundingClientRect();
@ -100,9 +100,13 @@ export class ViewerOverlay {
init(robot) {
this.robot = robot;
this.motorListPanel = this.createMotorListPanel(this.overlayCtx);
this.animationPanel = this.createAnimationControlPanel(this.overlayCtx);
if (this.motorListPanel) {
this.panels.push(this.motorListPanel);
}
if (this.animationPanel) {
this.panels.push(this.animationPanel);
}
this.draw();
}
@ -111,7 +115,7 @@ export class ViewerOverlay {
// filter out the motorListPanel from panels
this.panels = this.panels.filter(p => p !== this.motorListPanel);
this.panels = this.panels.filter(p => p !== this.animationPanel);
// clear the reference
this.motorListPanel = null;
}
@ -170,23 +174,23 @@ export class ViewerOverlay {
);
panel.addElement(slider);
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();
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]));
}
}));
panel.addElement(new Button(panel.x + 200, panel.y + 40 + 24*2, 200, 24, "Record Keyframe Selected", () => {
if (!this.parent.selectedJoint){
panel.addElement(new Button(panel.x + 200, panel.y + 40 + 24 * 2, 200, 24, "Record Keyframe Selected", () => {
if (!this.parent.selectedJoint) {
return;
}
const selectedID = this.parent.findJointAncestor(this.parent.selectedJoint, 0).transmission.motorID
const currentTime = this.parent.curveEditor.currentTime;
const motorPosition = this.parent.getMotorTicks(selectedID)
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(selectedID, currentTime, motorPosition);
//console.log(motorPosition, this.parent.curveEditor.exportRangeToY(motorPosition), this.parent.curveEditor.yToExportRange(this.parent.curveEditor.exportRangeToY(motorPosition)));
console.log(selectedID, currentTime, motorPosition);
}));
return panel;
@ -202,32 +206,37 @@ export class ViewerOverlay {
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
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
this.parent.saveURDFToIndexedDB(this.robot);
}));
// 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
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
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
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 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);
visualEditor.loadURDFFromIndexedDB();
}
@ -291,8 +292,7 @@ window.onload = () => {
}
}
// Connect button
document.getElementById('connect').addEventListener('click', async () => {
async function tryConnect() {
try {
await serial.connect();
statusText.textContent = 'Connected ✅';
@ -412,6 +412,11 @@ window.onload = () => {
statusText.textContent = 'Connection failed ❌';
console.error("Connection error:", err);
}
}
// Connect button
document.getElementById('connect').addEventListener('click', async () => {
tryConnect();
});
function handlePositionStreamPacket(data) {