296 lines
9.9 KiB
JavaScript
296 lines
9.9 KiB
JavaScript
|
|
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;
|
|||
|
|
}
|