import { Button, Panel, Textbox, Label, Checkbox, RadioButton, RadioGroup, Slider } from './ui/canvasui.js'; const SyncMode = { None: 0, SimToReal: 1, RealToSim: 2, Calibrate: 3 }; export class ViewerOverlay { constructor(renderer, robot, jointAngles, findObjectByName, parent) { this.renderer = renderer; this.robot = robot; this.jointAngles = jointAngles; this.findObjectByName = findObjectByName; this.parent = parent; this.overlayCanvas = document.getElementById('overlay-canvas'); this.overlayCanvas.addEventListener("wheel", (e) => { e.preventDefault(); // stops page scroll/zoom // your zoom logic here }, { passive: false }); // passive:false is required to call preventDefault // Disable browser context menu on canvas this.overlayCanvas.addEventListener("contextmenu", (e) => { e.preventDefault(); // stops the default right‑click menu e.stopPropagation(); // keeps it from bubbling up // Your custom right‑click logic here // e.clientX / e.clientY give you the mouse position //this.handleRightClick(e); }); this.overlayCtx = this.overlayCanvas.getContext('2d'); this.uiElements = []; // for motor list items if you wrap them in UIElements this.panels = []; // for modals or floating panels // this.motorListPanel = this.createMotorListPanel(); // this.panels.push(this.motorListPanel); // console.log(this.panels); this.showMotorList = true; this.showConfigModal = false; this.configMotor = null; this.configMotorName = null; // Handle resizing const resizeOverlay = () => { const { clientWidth, clientHeight } = this.renderer.domElement; this.overlayCanvas.width = clientWidth; this.overlayCanvas.height = clientHeight; }; window.addEventListener('resize', resizeOverlay); resizeOverlay(); this.panels.push(this.createAnimationControlPanel()); this.panels.push(this.createSystemPanel()); const handlePointerEvent = (event) => { const rect = this.overlayCanvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; let consumed = false; // Panels get first chance for (const panel of this.panels) { if (panel.contains(x, y)) { panel.handlePointerEvent(event, x, y); consumed = true; break; } } if (!consumed) { const evt = new event.constructor(event.type, event); this.renderer.domElement.dispatchEvent(evt); } if (['pointermove', 'pointerdown', 'pointerup'].includes(event.type)) { this.draw(); } }; window.addEventListener('keydown', (event) => { this.panels.forEach(panel => panel.handleKeyEvent(event)); this.draw(); }); // Attach handlers ['click', 'pointerdown', 'pointerup', 'pointermove', 'wheel'].forEach(type => { this.overlayCanvas.addEventListener(type, handlePointerEvent); }); } init(robot) { this.robot = robot; this.motorListPanel = this.createMotorListPanel(this.overlayCtx); if (this.motorListPanel) { this.panels.push(this.motorListPanel); } this.draw(); } resetEditor() { if (!this.motorListPanel) return; // filter out the motorListPanel from panels this.panels = this.panels.filter(p => p !== this.motorListPanel); // clear the reference this.motorListPanel = null; } draw() { const ctx = this.overlayCtx; ctx.clearRect(0, 0, this.overlayCanvas.width, this.overlayCanvas.height); this.updateMotorLabels(); this.panels.forEach(panel => panel.draw(ctx)); //console.log(this.panels); } createAnimationControlPanel() { const w = this.overlayCanvas.width; const h = this.overlayCanvas.height / 8; const x = 0; const y = this.overlayCanvas.height - this.overlayCanvas.height / 8; const panel = new Panel(x, y, w, h, "Animation Controls"); const group = new RadioGroup(); const rb1 = new RadioButton(panel.x + 20, panel.y + 40, 16, "None", group, true, () => { this.parent.setSyncMode(SyncMode.None); }, ""); const rb2 = new RadioButton(panel.x + 20, panel.y + 70, 16, "Sim -> Real", group, false, () => { this.parent.setSyncMode(SyncMode.SimToReal); }, "Real robot will mirror simulated movements"); const rb3 = new RadioButton(panel.x + 20, panel.y + 100, 16, "Real -> Sim", group, false, () => { this.parent.setSyncMode(SyncMode.RealToSim); }, "Deactivate all motor torque and simulated robot will mirror real robot motion"); group.addButton(rb1); group.addButton(rb2); group.addButton(rb3); panel.addElement(rb1); panel.addElement(rb2); panel.addElement(rb3); const slider = new Slider( panel.x + panel.w * 0.025, panel.y + panel.h * 0.85, panel.w * 0.925, 20, 0, // min this.parent.curveEditor.getLength(), // max 50, // initial (val) => { this.parent.curveEditor.setCurrentTime(val.toFixed(0)); // update parent if (slider.max != this.parent.curveEditor.getLength()) { slider.max = this.parent.curveEditor.getLength(); } this.parent.syncWithCurveEditor(); } ); panel.addElement(slider); 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++){ 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){ 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); })); return panel; } createSystemPanel() { const x = this.overlayCanvas.width * 0.9; // bottom right const y = this.overlayCanvas.height * 0; const w = this.overlayCanvas.width * 0.1; const h = this.overlayCanvas.height * 0.2; const panel = new Panel(x, y, w, h, "System"); // Save button panel.addElement(new Button(x, y + 28, 50, 24, "Save", () => { // delegate to editor’s save this.parent.saveURDFToIndexedDB(this.robot); })); // Load button panel.addElement(new Button(x, y + 28 + 24 * 1, 50, 24, "Load", () => { // delegate to editor’s load this.parent.loadURDFFromIndexedDB(); })); panel.addElement(new Button(x, y + 28 + 24 * 2, 50, 24, "Download", () => { // delegate to editor’s load this.parent.downloadURDF(); })); panel.addElement(new Button(x, y + 28 + 24 * 3, 50, 24, "Upload", () => { // delegate to editor’s load this.parent.uploadURDF(); })); panel.addElement(new Button(x, y + 28 + 24 * 5, 50, 24, "Calibrate", () => { this.applyCalibrationOffsets(); })); return panel; } createMotorListPanel() { console.log(":-)"); if (!this.robot?.joints) return null; this.panels = this.panels.filter(p => !p.title?.startsWith("Config:")); const panel = new Panel(0, 0, 300, this.overlayCanvas.height / 8 * 7, "Motors"); let y = 10; for (const jointName in this.robot.joints) { const jointData = this.robot.joints[jointName]; const jointObject = this.findObjectByName(this.robot, jointName); if (!jointData || !jointObject) continue; const angle = this.jointAngles?.[jointName] ?? 0; const degrees = (angle * 180 / Math.PI).toFixed(1); const motorName = jointData.transmission?.actuatorName || '(no motor)'; if (motorName === "(no motor)") continue; // Build label string with angle/ticks const t = jointData.transmission; let label = `${motorName} ${degrees}°`; if (t) { const ticks = radiansToTicks(angle, t).toFixed(0); label += ` ${ticks}`; } // Create button const btn = new Button(10, y + 20, 280, 18, label, () => { this.openMotorConfig(jointName, motorName); }); btn.jointName = jointName; btn.motorName = motorName; panel.addElement(btn); y += 22; } return panel; } updateMotorLabels() { const motorPanel = this.panels.find(p => p.title === "Motors"); if (!motorPanel || !this.robot?.joints) return; for (const element of motorPanel.elements) { if (!(element instanceof Button) || !element.jointName) continue; const jointName = element.jointName; const jointData = this.robot.joints[jointName]; if (!jointData) continue; const angle = this.jointAngles?.[jointName] ?? 0; const degrees = (angle * 180 / Math.PI).toFixed(1); const motorName = jointData.transmission?.actuatorName || '(no motor)'; if (motorName === "(no motor)") continue; let label = `${motorName} ${degrees}°`; const t = jointData.transmission; if (t) { const ticks = radiansToTicks(angle, t).toFixed(0); label += ` ${ticks}`; } element.label = label; // 🔄 update text } } openMotorConfig(jointName, motorName) { const motor = this.robot.joints[jointName]; // Remove any existing config panel this.panels = this.panels.filter(p => !p.title?.startsWith("Config:")); const panel = new Panel(400, 50, 400, 250, `Config: ${motorName}`); const motorIDLabel = new Label(panel.x + 20, panel.y + 50, 'Motor ID:'); panel.addElement(motorIDLabel); const motorIdBox = new Textbox(panel.x + 100, panel.y + 35, 80, 24, motor.transmission.motorID || ''); panel.addElement(motorIdBox); panel.addElement(new Button(panel.x + 20, panel.y + 150, 80, 24, 'Save', () => { const idValue = parseInt(motorIdBox.value, 10) || 0; motor.motorId = idValue; motor.transmission.motorID = idValue; this.addOrUpdateActuatorElement(motorName, "motorID", idValue); this.panels = this.panels.filter(p => p !== panel); // close panel this.draw(); })); panel.addElement(new Button(panel.x + 120, panel.y + 150, 80, 24, 'Close', () => { this.panels = this.panels.filter(p => p !== panel); this.draw(); })); this.panels.push(panel); this.draw(); } /** * Ensure an element exists under a given actuator and set its text. * @param {string} transmissionName - The transmission name * @param {string} actuatorName - The actuator name * @param {string} elementName - The child element to add/update (e.g. "motorID") * @param {string|number} value - The value to set */ addOrUpdateActuatorElement(actuatorName, elementName, value) { console.log("Updating URDF:", actuatorName, elementName, value); const urdfNode = this.robot?.urdfRobotNode; if (!urdfNode) return; // find actuator anywhere in the URDF const actuatorNode = urdfNode.querySelector(`actuator[name="${actuatorName}"]`); if (!actuatorNode) return; // find or create the child element let childNode = actuatorNode.querySelector(elementName); if (!childNode) { childNode = urdfNode.ownerDocument.createElement(elementName); actuatorNode.appendChild(childNode); } childNode.textContent = value.toString(); } onClick(event) { console.log("CLICK"); const rect = this.overlayCanvas.getBoundingClientRect(); const x = event.clientX - rect.left; const y = event.clientY - rect.top; for (const hb of this.motorHitboxes) { if (x >= hb.x && x <= hb.x + hb.w && y >= hb.y && y <= hb.y + hb.h) { this.openMotorConfig(hb.jointName, hb.motorName); break; } } } // openMotorConfig(jointName, motorName) { // this.showConfigModal = true; // this.configMotorName = motorName; // this.configMotor = this.robot.joints[jointName]; // this.draw(); // } } function ticksToRadians(ticks, actuator) { const ticksPerDeg = actuator.encoderTicks / actuator.encoderRange; // ≈ 22.75 const centered = ticks - actuator.encoderTicks / 2; // shift so mid = 0 const actuatorDeg = centered / ticksPerDeg; const jointDeg = actuatorDeg / actuator.mechanicalReduction; return jointDeg * (Math.PI / 180); // convert degrees → radians } function radiansToTicks(radians, actuator) { const ticksPerDeg = actuator.encoderTicks / actuator.encoderRange; // ≈ 22.75 const jointDeg = radians * (180 / Math.PI); // radians → degrees const actuatorDeg = jointDeg * actuator.mechanicalReduction; // apply reduction const centered = actuatorDeg * ticksPerDeg; // convert to ticks offset from mid const ticks = centered + actuator.encoderTicks / 2; // shift back to full range return ticks; }