page styled to be a single page, no scrolling. tooltips added. sync option radioboxes implemented

master
Jake Wilkinson 2025-11-23 12:14:21 +08:00
parent 58eedd06a3
commit f2e8764f6b
5 changed files with 265 additions and 39 deletions

View File

@ -19,7 +19,7 @@
<canvas id="urdfCanvas"></canvas>
<canvas id="overlay-canvas"></canvas>
</div>
<canvas id="curveCanvas"></canvas>
<h2>Little Sophia Control Panel</h2>
@ -174,7 +174,7 @@
<div class="tab-pane fade show active" id="animation" role="tabpanel" aria-labelledby="animation-tab">
<canvas id="curveCanvas"></canvas>
<!-- <div style="margin-top: 10px; text-align: center;">
<input type="range" id="timeSlider" min="0" step="1" style="width: 80%;">
</div> -->

View File

@ -1,4 +1,4 @@
import { Button, Panel, Textbox } from './ui/canvasui.js';
import { Button, Panel, Textbox, Label, Checkbox, RadioButton, RadioGroup } from './ui/canvasui.js';
export class ViewerOverlay {
constructor(renderer, robot, jointAngles, findObjectByName, saveURDF, loadURDF) {
@ -49,6 +49,7 @@ export class ViewerOverlay {
window.addEventListener('resize', resizeOverlay);
resizeOverlay();
this.panels.push(this.createAnimationControlPanel());
this.panels.push(this.createSystemPanel());
const handlePointerEvent = (event) => {
const rect = this.overlayCanvas.getBoundingClientRect();
@ -109,23 +110,58 @@ export class ViewerOverlay {
//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 = 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 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 + 20, y + 20, 120, 24, "Save", () => {
panel.addElement(new Button(x, h / 2, 50, 24, "Save", () => {
// delegate to editors save
this.saveURDF(this.robot);
}));
// Load button
panel.addElement(new Button(x + 20, y + 50, 120, 24, "Load", () => {
panel.addElement(new Button(x + w / 2, h / 2, 50, 24, "Load", () => {
// delegate to editors load
this.loadURDF();
}));
@ -137,7 +173,7 @@ export class ViewerOverlay {
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");
const panel = new Panel(0, 0, 300, this.overlayCanvas.height / 8 * 7, "Motors");
let y = 10;
for (const jointName in this.robot.joints) {
@ -214,7 +250,10 @@ export class ViewerOverlay {
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 || '');
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', () => {

View File

@ -2,12 +2,17 @@
class UIElement {
constructor(x, y, w, h) {
constructor(x, y, w, h, tooltipText = null) {
this.x = x; this.y = y;
this.w = w; this.h = h;
this.hovered = false;
this.active = false;
this.tooltipDelay = 1000; // ms before showing
this.hoverStart = null; // timestamp when hover began
this.tooltip = tooltipText ? new Tooltip(tooltipText) : null;
}
contains(px, py) {
@ -38,10 +43,22 @@ class UIElement {
onPointerMove(px, py) {
this.hovered = this.contains(px, py);
//console.log(this);
const inside = this.contains(px, py);
// detect entering hover
if (inside && !this.hovered) {
this.hoverStart = performance.now();
}
// detect leaving hover
if (!inside && this.hovered) {
this.hoverStart = null;
}
this.hovered = inside;
}
onPointerDown(px, py) {
if (this.contains(px, py)) {
this.active = true;
@ -59,12 +76,41 @@ class UIElement {
return false;
}
draw(ctx) { /* override in subclasses */ }
draw(ctx) {
if (this.tooltip && this.hovered && this.hoverStart) {
const elapsed = performance.now() - this.hoverStart;
if (elapsed >= this.tooltipDelay) {
this.tooltip.show(this.x + 100, this.y + this.h*2);
this.tooltip.draw(ctx);
} else {
this.tooltip.hide();
}
}
}
}
export class Label extends UIElement {
constructor(x, y, text, font = '14px monospace', color = '#fff', tooltipText = null) {
// width/height are optional for labels, but we can set them to 0
super(x, y, 0, 0, tooltipText);
this.text = text;
this.font = font;
this.color = color;
}
draw(ctx) {
ctx.fillStyle = this.color;
ctx.font = this.font;
ctx.fillText(this.text, this.x, this.y);
}
// labels dont need pointer/key handling, so we leave defaults
}
export class Panel extends UIElement {
constructor(x, y, w, h, title = '') {
super(x, y, w, h);
constructor(x, y, w, h, title = '', tooltipText = null) {
super(x, y, w, h, tooltipText);
this.title = title;
this.elements = [];
}
@ -102,8 +148,8 @@ export class Panel extends UIElement {
export class Button extends UIElement {
constructor(x, y, w, h, label, onClick) {
super(x, y, w, h);
constructor(x, y, w, h, label, onClick, tooltipText = null) {
super(x, y, w, h, tooltipText);
this.label = label;
this.onClick = onClick;
}
@ -115,17 +161,14 @@ export class Button extends UIElement {
ctx.fillStyle = '#fff';
if (this.hovered) {
ctx.fillStyle = '#666';
ctx.fillStyle = '#00a808ff';
}
ctx.font = '14px monospace';
ctx.fillText(this.label, this.x + 10, this.y + this.h - 8);
super.draw(ctx);
}
onPointerMove(px, py) {
this.hovered = this.contains(px, py);
//console.log(this);
}
onPointerUp(px, py) {
const clicked = super.onPointerUp(px, py);
@ -135,8 +178,8 @@ export class Button extends UIElement {
}
export class Textbox extends UIElement {
constructor(x, y, w, h, initialValue = '') {
super(x, y, w, h);
constructor(x, y, w, h, initialValue = '', tooltipText = null) {
super(x, y, w, h, tooltipText);
this.value = initialValue;
this.focused = false;
}
@ -169,3 +212,150 @@ export class Textbox extends UIElement {
}
}
export class Checkbox extends UIElement {
constructor(x, y, size = 16, label = '', initialChecked = false, onChange = null, tooltipText = null) {
super(x, y, size, size, tooltipText);
this.checked = initialChecked;
this.label = label;
this.onChange = onChange;
}
draw(ctx) {
// box
ctx.fillStyle = '#000';
ctx.fillRect(this.x, this.y, this.w, this.h);
ctx.strokeStyle = this.hovered ? '#0f0' : '#aaa';
ctx.strokeRect(this.x, this.y, this.w, this.h);
// check mark
if (this.checked) {
ctx.fillStyle = '#0f0';
ctx.fillRect(this.x + 3, this.y + 3, this.w - 6, this.h - 6);
}
// label text
if (this.label) {
ctx.fillStyle = '#fff';
ctx.font = '14px monospace';
ctx.fillText(this.label, this.x + this.w + 8, this.y + this.h - 4);
}
}
onPointerUp(px, py) {
const clicked = super.onPointerUp(px, py);
if (clicked) {
this.checked = !this.checked;
if (this.onChange) this.onChange(this.checked);
}
return clicked;
}
}
export class RadioButton extends UIElement {
constructor(x, y, size = 16, label = '', group = null, initialSelected = false, onChange = null, tooltipText) {
super(x, y, size, size, tooltipText);
this.selected = initialSelected;
this.label = label;
this.group = group; // reference to RadioGroup
this.onChange = onChange;
}
draw(ctx) {
// outer circle
ctx.strokeStyle = this.hovered ? '#0f0' : '#aaa';
ctx.beginPath();
ctx.arc(this.x + this.w / 2, this.y + this.h / 2, this.w / 2, 0, Math.PI * 2);
ctx.stroke();
// inner dot if selected
if (this.selected) {
ctx.fillStyle = '#0f0';
ctx.beginPath();
ctx.arc(this.x + this.w / 2, this.y + this.h / 2, this.w / 2 - 4, 0, Math.PI * 2);
ctx.fill();
}
// label text
if (this.label) {
ctx.fillStyle = '#fff';
ctx.font = '14px monospace';
ctx.fillText(this.label, this.x + this.w + 8, this.y + this.h - 4);
}
super.draw(ctx);
}
onPointerUp(px, py) {
const clicked = super.onPointerUp(px, py);
if (clicked) {
if (this.group) {
this.group.select(this); // delegate exclusivity
} else {
this.selected = !this.selected;
}
if (this.onChange) this.onChange(this.selected);
}
return clicked;
}
}
export class RadioGroup {
constructor() {
this.buttons = [];
}
addButton(btn) {
btn.group = this;
this.buttons.push(btn);
}
select(selectedBtn) {
this.buttons.forEach(btn => btn.selected = (btn === selectedBtn));
}
getSelected() {
return this.buttons.find(btn => btn.selected);
}
}
export class Tooltip {
constructor(text) {
this.text = text;
this.visible = false;
this.x = 0;
this.y = 0;
}
show(x, y) {
this.visible = true;
this.x = x;
this.y = y;
}
hide() {
this.visible = false;
}
draw(ctx) {
if (!this.visible) return;
ctx.font = '12px monospace';
const padding = 6;
const metrics = ctx.measureText(this.text);
const w = metrics.width + padding * 2;
const h = 20;
ctx.fillStyle = 'rgba(0,0,0,0.8)';
ctx.fillRect(this.x, this.y - h, w, h);
ctx.strokeStyle = '#fff';
ctx.strokeRect(this.x, this.y - h, w, h);
ctx.fillStyle = '#fff';
ctx.fillText(this.text, this.x + padding, this.y - 6);
}
}

View File

@ -12,7 +12,7 @@ import { URDFEditor } from './ros_robot_visualiser/URDFEditor.js';
window.onload = () => {
const urdfCanvas = document.getElementById('urdfCanvas');
const editor = new URDFEditor(urdfCanvas);
const visualEditor = new URDFEditor(urdfCanvas);
const serial = new SerialManager();
const servoMotors = [[], []]; // index 0 = channel 0, index 1 = channel 1

View File

@ -1,8 +1,7 @@
body {
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden; /* optional: prevents scrollbars entirely */
}
.dial-container {
@ -25,8 +24,6 @@ body {
}
#fileListWrapper {
width: 400px;
max-height: 200px;
@ -91,11 +88,11 @@ body {
#curveCanvas,
#nodeeditor {
width: 100vw;
height: 25vh;
background-color: #f0f0f0;
border: 1px solid #ccc;
display: block;
margin: 0 auto;
margin-bottom: 5px;
}
@ -119,7 +116,7 @@ textarea {
#urdf-container {
position: relative;
width: 100vw;
height: 100vh;
height: 75vh;
overflow: hidden;
}