import { Button, Panel, Textbox } from './ui/canvasui.js'; export class ViewerOverlay { constructor(renderer, robot, jointAngles, findObjectByName, saveURDF, loadURDF) { this.renderer = renderer; this.robot = robot; this.jointAngles = jointAngles; this.findObjectByName = findObjectByName; this.saveURDF = saveURDF; // 🔑 bound to editor this.loadURDF = loadURDF; // 🔑 bound to editor this.overlayCanvas = document.getElementById('overlay-canvas'); 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.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(); } 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); } createSystemPanel() { const w = 160; const h = 80; const x = this.overlayCanvas.width - w - 10; // bottom right const y = this.overlayCanvas.height - h - 10; console.log(this.overlayCanvas.width, this.overlayCanvas.height); const panel = new Panel(x, y, w, h, "System"); // Save button panel.addElement(new Button(x + 20, y + 20, 120, 24, "Save", () => { // delegate to editor’s save this.saveURDF(this.robot); })); // Load button panel.addElement(new Button(x + 20, y + 50, 120, 24, "Load", () => { // delegate to editor’s load this.loadURDF(); })); return panel; } createMotorListPanel() { 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, "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 motorIdBox = new Textbox(panel.x + 100, panel.y + 45, 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; }