From 896198c9d4a2ad691acf47dd071e8c7a62e40cb8 Mon Sep 17 00:00:00 2001 From: igurielidze Date: Mon, 30 Mar 2026 18:28:16 +0400 Subject: [PATCH] Fix label placement for curved and spur symbols - Curved: position text at arc band midpoint, rotated along the curve - Spur: center text in the trapezoid shape, not the bounding box Co-Authored-By: Claude Opus 4.6 (1M context) --- svelte-app/src/lib/canvas/renderer.ts | 77 ++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/svelte-app/src/lib/canvas/renderer.ts b/svelte-app/src/lib/canvas/renderer.ts index 2114b3f..86ba2ac 100644 --- a/svelte-app/src/lib/canvas/renderer.ts +++ b/svelte-app/src/lib/canvas/renderer.ts @@ -546,28 +546,39 @@ function parseConveyanceLabel(label: string): { lines: string[]; stripped: strin /** Draw label inside a conveyance symbol — black bold text, auto-sized to fit */ function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { if (!sym.label) return; + + if (isCurvedType(sym.symbolId)) { drawCurvedLabel(ctx, sym); return; } + const { lines, stripped } = parseConveyanceLabel(sym.label); const pad = 2; - const availW = sym.w - pad * 2; - const availH = sym.h - pad * 2; - const cx = sym.x + sym.w / 2; - const cy = sym.y + sym.h / 2; + + // For spurs, center in the trapezoid shape, not bounding box + let cx: number, cy: number, availW: number, availH: number; + if (isSpurType(sym.symbolId)) { + const w2 = sym.w2 ?? sym.w; + const midW = (w2 + sym.w) / 2; // width at vertical midpoint + cx = sym.x + midW / 2; + cy = sym.y + sym.h / 2; + availW = midW - pad * 2; + availH = sym.h - pad * 2; + } else { + cx = sym.x + sym.w / 2; + cy = sym.y + sym.h / 2; + availW = sym.w - pad * 2; + availH = sym.h - pad * 2; + } ctx.fillStyle = '#000000'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - // Try full lines first, then stripped (prefix removed) if it doesn't fit for (const tryLines of [lines, stripped]) { - // Target font size: 14px, but scale down to fit let fontSize = 14; const lineCount = tryLines.length; const totalTextH = () => fontSize * lineCount + (lineCount - 1) * 1; - // Scale down if too tall while (totalTextH() > availH && fontSize > 4) fontSize -= 0.5; - // Scale down if too wide ctx.font = `bold ${fontSize}px Arial`; let maxW = Math.max(...tryLines.map(l => ctx.measureText(l).width)); while (maxW > availW && fontSize > 4) { @@ -588,6 +599,56 @@ function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { } } +/** Draw label along a curved symbol's arc band */ +function drawCurvedLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { + if (!sym.label) return; + const { lines, stripped } = parseConveyanceLabel(sym.label); + const angle = sym.curveAngle || 90; + const { arcCx, arcCy, outerR, innerR, bandW } = getCurveGeometry(sym.symbolId, sym.x, sym.y, sym.w, sym.h); + const midR = (outerR + innerR) / 2; + const midAngleRad = ((angle / 2) * Math.PI) / 180; // half the sweep angle + + // Position at arc midpoint + const textX = arcCx + midR * Math.cos(midAngleRad); + const textY = arcCy - midR * Math.sin(midAngleRad); + // Rotation: perpendicular to radius = tangent to arc + const textRot = -midAngleRad + Math.PI / 2; + + const availW = bandW - 4; + + ctx.save(); + ctx.translate(textX, textY); + ctx.rotate(textRot); + ctx.fillStyle = '#000000'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + + for (const tryLines of [lines, stripped]) { + let fontSize = 14; + const lineCount = tryLines.length; + while (fontSize * lineCount > availW && fontSize > 4) fontSize -= 0.5; + ctx.font = `bold ${fontSize}px Arial`; + let maxW = Math.max(...tryLines.map(l => ctx.measureText(l).width)); + // Available length along the arc at midR + const arcLen = midR * (angle * Math.PI / 180) * 0.6; // use 60% of arc + while (maxW > arcLen && fontSize > 4) { + fontSize -= 0.5; + ctx.font = `bold ${fontSize}px Arial`; + maxW = Math.max(...tryLines.map(l => ctx.measureText(l).width)); + } + if (fontSize >= 4) { + const lineH = fontSize; + const startY = -(lineCount - 1) * lineH / 2; + for (let i = 0; i < tryLines.length; i++) { + ctx.fillText(tryLines[i], 0, startY + i * lineH); + } + ctx.restore(); + return; + } + } + ctx.restore(); +} + function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { const cx = sym.x + sym.w / 2; const isSelected = layout.selectedIds.has(sym.id);