From 855f596cfed3f909e327ddbe74fd8ba46b19f3fb Mon Sep 17 00:00:00 2001 From: ro Date: Thu, 12 Feb 2026 13:51:47 +0200 Subject: [PATCH] Add linkage annotation overlay toggle and joint labels --- README.md | 2 +- index.html | 6 +++++- src/main.js | 1 + src/renderer.js | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ src/ui.js | 6 ++++++ 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 30d72a6..017b53a 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ - body = red - Play/pause, reset, speed slider, angle scrubber. - Foot trajectory traces for both sides. +- Optional linkage annotation overlay (joint and link labels). - Diagnostics panel with closure error and fallback counters. ## File layout @@ -47,4 +48,3 @@ Then open: - No collision/ground dynamics and no body translation. - Side B is driven by `theta + pi`. - Lower branch of each circle-circle intersection is selected to maintain leg-down posture. - diff --git a/index.html b/index.html index 363b078..739d6ad 100644 --- a/index.html +++ b/index.html @@ -47,6 +47,11 @@ Show foot traces + +

Diagnostics

loading…
@@ -57,4 +62,3 @@ - diff --git a/src/main.js b/src/main.js index 0455a52..a9c4346 100644 --- a/src/main.js +++ b/src/main.js @@ -14,6 +14,7 @@ const state = { showNear: true, showFar: true, showTrace: true, + showAnnotations: false, fallbackCount: 0, pose: null, ground: { diff --git a/src/renderer.js b/src/renderer.js index a8e1dd4..763d924 100644 --- a/src/renderer.js +++ b/src/renderer.js @@ -160,6 +160,52 @@ function drawHUD(ctx, data) { ctx.restore(); } +function drawLabel(ctx, p, text, dx = 0, dy = 0) { + ctx.save(); + ctx.font = '12px ui-monospace, SFMono-Regular, Menlo, monospace'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + const x = p.x + dx; + const y = p.y + dy; + const padX = 4; + const padY = 2; + const metrics = ctx.measureText(text); + const w = metrics.width + padX * 2; + const h = 14 + padY * 2; + + ctx.fillStyle = 'rgba(10, 12, 18, 0.72)'; + ctx.fillRect(x - w * 0.5, y - h * 0.5, w, h); + + ctx.fillStyle = '#f4f7ff'; + ctx.fillText(text, x, y + 0.5); + ctx.restore(); +} + +function drawAnnotations(ctx, tf, side) { + if (!side) return; + + const A = tf.toScreen(side.A); + const B = tf.toScreen(side.B); + const O = tf.toScreen(side.O); + const C = tf.toScreen(side.C); + const D = tf.toScreen(side.D); + const E = tf.toScreen(side.E); + const F = tf.toScreen(side.F); + const G = tf.toScreen(side.G); + + // Joint labels + drawLabel(ctx, A, 'A', -12, -10); + drawLabel(ctx, B, 'B', 12, -10); + drawLabel(ctx, O, 'O', 0, -14); + drawLabel(ctx, C, 'C', 0, 14); + drawLabel(ctx, D, 'D', -12, 10); + drawLabel(ctx, E, 'E', 12, 10); + drawLabel(ctx, F, 'F', -12, 12); + drawLabel(ctx, G, 'G', 12, 12); + +} + export function renderScene(canvas, state) { const ctx = canvas.getContext('2d'); ctx.fillStyle = COLORS.bg; @@ -208,5 +254,9 @@ export function renderScene(canvas, state) { drawSide(ctx, tf, state.pose.near, 1, 2.6); } + if (state.showAnnotations && state.showNear) { + drawAnnotations(ctx, tf, state.pose.near); + } + drawHUD(ctx, state.pose); } diff --git a/src/ui.js b/src/ui.js index 7ee6c26..f043088 100644 --- a/src/ui.js +++ b/src/ui.js @@ -10,6 +10,7 @@ export function wireUI(state, hooks) { const showNear = document.getElementById('show-near'); const showFar = document.getElementById('show-far'); const showTrace = document.getElementById('show-trace'); + const showAnnotations = document.getElementById('show-annotations'); speed.value = String(state.speedRps); speedOut.value = Number(state.speedRps).toFixed(2); @@ -20,6 +21,7 @@ export function wireUI(state, hooks) { showNear.checked = state.showNear; showFar.checked = state.showFar; showTrace.checked = state.showTrace; + showAnnotations.checked = state.showAnnotations; speed.addEventListener('input', () => { const v = Number(speed.value); @@ -55,6 +57,10 @@ export function wireUI(state, hooks) { state.showTrace = showTrace.checked; }); + showAnnotations.addEventListener('change', () => { + state.showAnnotations = showAnnotations.checked; + }); + return { syncAngle(theta) { const deg = radToDeg(theta);