page styled to be a single page, no scrolling. tooltips added. sync option radioboxes implemented
parent
58eedd06a3
commit
f2e8764f6b
|
|
@ -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> -->
|
||||
|
|
|
|||
|
|
@ -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 editor’s 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 editor’s 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', () => {
|
||||
|
|
|
|||
|
|
@ -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 don’t 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
13
style.css
13
style.css
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue