From 31aea343612d304dc1bb8eaf69489d471947d339 Mon Sep 17 00:00:00 2001 From: igurielidze Date: Mon, 30 Mar 2026 21:41:24 +0400 Subject: [PATCH] Spur label: compute optimal position from trapezoid geometry Position text at 55% height (toward wider area), then center horizontally based on the actual right edge at the text's top, minimizing overflow past the angled edge. Co-Authored-By: Claude Opus 4.6 (1M context) --- svelte-app/src/lib/canvas/renderer.ts | 21 +++++++++++++++------ svelte-app/src/lib/export.ts | 10 +++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/svelte-app/src/lib/canvas/renderer.ts b/svelte-app/src/lib/canvas/renderer.ts index 5cf31f3..9580fbd 100644 --- a/svelte-app/src/lib/canvas/renderer.ts +++ b/svelte-app/src/lib/canvas/renderer.ts @@ -556,12 +556,21 @@ function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { let cx: number, cy: number, availW: number, availH: number; if (isSpurType(sym.symbolId)) { const w2 = sym.w2 ?? sym.w; - // The 90° corners are at x=0 (left edge). The rectangular region - // from x=0 to x=min(w2,w) always fits inside the trapezoid. - const safeW = Math.min(w2, sym.w); - cx = sym.x + safeW / 2; - cy = sym.y + sym.h / 2; - availW = Infinity; // never shrink font for spurs — enough visual room + // Find optimal position: push text down where trapezoid is widest, + // then center horizontally in the available width at that height. + // Trapezoid right edge at height y: w2 + (y/h) * (w - w2) + const fontSize = 14; + const lineCount = lines.length; + const th = fontSize * lineCount; + // Optimal cy: as low as possible (widest area), clamped to fit + const optCy = Math.min(sym.h - th / 2 - 1, sym.h * 0.55); + // Right edge at the TOP of the text block (most constraining) + const textTop = optCy - th / 2; + const rightEdgeAtTop = w2 + (Math.max(0, textTop) / sym.h) * (sym.w - w2); + // Center text in the available width (0 to rightEdgeAtTop) + cx = sym.x + rightEdgeAtTop / 2; + cy = sym.y + optCy; + availW = Infinity; // never shrink — position handles fit availH = sym.h; } else { cx = sym.x + sym.w / 2; diff --git a/svelte-app/src/lib/export.ts b/svelte-app/src/lib/export.ts index 20f226d..b917654 100644 --- a/svelte-app/src/lib/export.ts +++ b/svelte-app/src/lib/export.ts @@ -44,9 +44,13 @@ function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) { textRotDeg = (textRotRad * 180) / Math.PI; } else if (isSpurType(sym.symbolId)) { const w2 = sym.w2 ?? sym.w; - const safeW = Math.min(w2, sym.w); - labelCx = sym.x + safeW / 2; - labelCy = sym.y + sym.h / 2; + const fontSize = 14; + const th = fontSize * textLines.length; + const optCy = Math.min(sym.h - th / 2 - 1, sym.h * 0.55); + const textTop = optCy - th / 2; + const rightEdgeAtTop = w2 + (Math.max(0, textTop) / sym.h) * (sym.w - w2); + labelCx = sym.x + rightEdgeAtTop / 2; + labelCy = sym.y + optCy; availH = sym.h; } else if (isInductionType(sym.symbolId)) { const stripTopY = sym.y + sym.h * INDUCTION_CONFIG.stripTopFrac;