sophia_controller/ros_robot_visualiser/ViewerOverlay.js

387 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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 rightclick menu
e.stopPropagation(); // keeps it from bubbling up
// Your custom rightclick 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, 100, 50,
(val) => console.log("Slider value:", val)
);
panel.addElement(slider);
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 editors save
this.parent.saveURDFToIndexedDB(this.robot);
}));
// Load button
panel.addElement(new Button(x, y + 28 + 24 * 1, 50, 24, "Load", () => {
// delegate to editors load
this.parent.loadURDFFromIndexedDB();
}));
panel.addElement(new Button(x, y + 28 + 24 * 2, 50, 24, "Download", () => {
// delegate to editors load
this.parent.downloadURDF();
}));
panel.addElement(new Button(x, y + 28 + 24 * 3, 50, 24, "Upload", () => {
// delegate to editors 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;
}