From 4be8ddd6c3fa76a0860263dc259dd35b70115559 Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Mon, 3 Aug 2026 23:20:49 +0200 Subject: [PATCH] fix(ui): a pin stack taller than the part was clipped by the preview The ReSpeaker's left side carries 13 labels: at the minimum spacing that is 208px against a 190px body, so the column ran past both ends and the first and last labels were cut off by the preview's clip. The gutters now grow to whatever the labels actually occupy - the box follows the content. --- .../__tests__/pin-inspector-layout.test.ts | 11 +++++++ frontend/src/utils/pinInspectorLayout.ts | 31 ++++++++++++++++--- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/frontend/src/__tests__/pin-inspector-layout.test.ts b/frontend/src/__tests__/pin-inspector-layout.test.ts index 7a26064d..f9d86985 100644 --- a/frontend/src/__tests__/pin-inspector-layout.test.ts +++ b/frontend/src/__tests__/pin-inspector-layout.test.ts @@ -193,6 +193,17 @@ describe('layoutInspectorPins — inset columns (the ReSpeaker Lite case)', () = it('never puts a column end-pin in the top or bottom gutter', () => { expect(r.pins.some((p) => p.edge === 'top' || p.edge === 'bottom')).toBe(false); }); + + it('grows the box so a stack taller than the body is not clipped', () => { + // 13 labels on the left at >=16px need more room than the scaled body, + // so the first and last used to be cut off by the preview's clip. + const top = -r.padTop; + const bottom = r.artHeight + r.padBottom; + for (const p of r.pins) { + expect(p.labelY).toBeGreaterThanOrEqual(top); + expect(p.labelY).toBeLessThanOrEqual(bottom); + } + }); }); describe('signalKindOf', () => { diff --git a/frontend/src/utils/pinInspectorLayout.ts b/frontend/src/utils/pinInspectorLayout.ts index 495d61d5..5ae3603c 100644 --- a/frontend/src/utils/pinInspectorLayout.ts +++ b/frontend/src/utils/pinInspectorLayout.ts @@ -287,14 +287,37 @@ export function layoutInspectorPins( } const has = (e: PinEdge) => classified.some((c) => c.edge === e); + let padLeft = has('left') ? SIDE_GUTTER : 0; + let padRight = has('right') ? SIDE_GUTTER : 0; + let padTop = has('top') ? CAP_GUTTER : 0; + let padBottom = has('bottom') ? CAP_GUTTER : 0; + + // A stack taller than the art must not be clipped. A ReSpeaker's left side + // carries 13 labels: at the minimum spacing that is 208 px against a 190 px + // body, so the column necessarily runs past both ends and the first and + // last labels were cut off. Grow the gutters to whatever the labels + // actually occupy — the box follows the content, not the other way round. + const HALF_LINE = 7; // half a label's line box, plus a hair of margin + for (const p of laid) { + if (!p) continue; + if (p.edge === 'left' || p.edge === 'right') { + padTop = Math.max(padTop, HALF_LINE - p.labelY); + padBottom = Math.max(padBottom, p.labelY + HALF_LINE - H); + } else { + // Vertical text on the caps: its extent along x is the line box. + padLeft = Math.max(padLeft, HALF_LINE - p.labelX); + padRight = Math.max(padRight, p.labelX + HALF_LINE - W); + } + } + return { scale, artWidth: W, artHeight: H, pins: laid, - padLeft: has('left') ? SIDE_GUTTER : 0, - padRight: has('right') ? SIDE_GUTTER : 0, - padTop: has('top') ? CAP_GUTTER : 0, - padBottom: has('bottom') ? CAP_GUTTER : 0, + padLeft: Math.max(0, Math.round(padLeft)), + padRight: Math.max(0, Math.round(padRight)), + padTop: Math.max(0, Math.round(padTop)), + padBottom: Math.max(0, Math.round(padBottom)), }; }