diff --git a/index.html b/index.html
index d3dbe8e..b00d8aa 100644
--- a/index.html
+++ b/index.html
@@ -13,13 +13,13 @@
-
+
-
+
Little Sophia Control Panel
@@ -174,7 +174,7 @@
-
+
diff --git a/ros_robot_visualiser/ViewerOverlay.js b/ros_robot_visualiser/ViewerOverlay.js
index 476325e..ced0d03 100644
--- a/ros_robot_visualiser/ViewerOverlay.js
+++ b/ros_robot_visualiser/ViewerOverlay.js
@@ -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', () => {
diff --git a/ros_robot_visualiser/ui/canvasui.js b/ros_robot_visualiser/ui/canvasui.js
index f112a11..07fea4e 100644
--- a/ros_robot_visualiser/ui/canvasui.js
+++ b/ros_robot_visualiser/ui/canvasui.js
@@ -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) {
@@ -33,15 +38,27 @@ class UIElement {
}
break;
}
-
+
}
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);
+ }
+}
+
+
+
+
diff --git a/script.js b/script.js
index 563000a..b83ec62 100644
--- a/script.js
+++ b/script.js
@@ -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
diff --git a/style.css b/style.css
index f765f54..bd65202 100644
--- a/style.css
+++ b/style.css
@@ -1,8 +1,7 @@
-body {
- margin: 0;
+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;
}