349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
import { Button, Panel, Textbox, Label, Checkbox, RadioButton, RadioGroup } 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.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();
|
||
}
|
||
|
||
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.setSyncMode("none");
|
||
}, "");
|
||
const rb2 = new RadioButton(panel.x + 20, panel.y + 70, 16, "Sim -> Real", group, false, () => {
|
||
this.setSyncMode("sim_to_real");
|
||
}, "Real robot will mirror simulated movements");
|
||
const rb3 = new RadioButton(panel.x + 20, panel.y + 100, 16, "Real -> Sim", group, false, () => {
|
||
this.setSyncMode("real_to_sim");
|
||
}, "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);
|
||
|
||
|
||
return panel;
|
||
}
|
||
|
||
setSyncMode(mode) {
|
||
|
||
console.log(mode);
|
||
}
|
||
|
||
createSystemPanel() {
|
||
const w = this.overlayCanvas.width * 0.1;
|
||
const h = this.overlayCanvas.height * 0.05;
|
||
const x = this.overlayCanvas.width * 0.9; // bottom right
|
||
const y = this.overlayCanvas.height * 0; console.log(this.overlayCanvas.width, this.overlayCanvas.height);
|
||
|
||
const panel = new Panel(x, y, w, h, "System");
|
||
|
||
// Save button
|
||
panel.addElement(new Button(x, h / 2, 50, 24, "Save", () => {
|
||
// delegate to editor’s save
|
||
this.saveURDF(this.robot);
|
||
}));
|
||
|
||
// Load button
|
||
panel.addElement(new Button(x + w / 2, h / 2, 50, 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 / 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;
|
||
} |