From 533465be3c88c47c8c775a7a3f7833b904ad62b7 Mon Sep 17 00:00:00 2001 From: igurielidze Date: Mon, 30 Mar 2026 21:09:40 +0400 Subject: [PATCH] Fix text readability: always right-side up on all conveyance types Canvas renderer: - Straight/spur: flip text 180 when rotation is 91-269 deg - Mirror: counter-scale text so it doesn't read backwards - Curved: compute world angle (sym rotation + tangent), flip if upside-down SVG export: - Curved text now includes rotation transform along the arc tangent - Straight text includes rotation correction for readability - All text stays grouped with its shape for proper transform inheritance Co-Authored-By: Claude Opus 4.6 (1M context) --- svelte-app/src/lib/canvas/renderer.ts | 42 +++++++++++++++++++++++---- svelte-app/src/lib/export.ts | 25 +++++++++++++--- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/svelte-app/src/lib/canvas/renderer.ts b/svelte-app/src/lib/canvas/renderer.ts index 0ace8e8..ea4d014 100644 --- a/svelte-app/src/lib/canvas/renderer.ts +++ b/svelte-app/src/lib/canvas/renderer.ts @@ -568,6 +568,14 @@ function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { availH = sym.h - pad * 2; } + // Compute text correction so text is always readable: + // - Counter-rotate if symbol rotation makes text upside-down (91-269 deg) + // - Counter-mirror if symbol is mirrored (so text doesn't read backwards) + const rot = ((sym.rotation || 0) % 360 + 360) % 360; + const needsFlip = rot > 90 && rot < 270; + const needsMirrorFix = sym.mirrored; + const hasCorrection = needsFlip || needsMirrorFix; + ctx.fillStyle = '#000000'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; @@ -590,9 +598,22 @@ function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { if (fontSize >= 4) { ctx.font = `bold ${fontSize}px Arial`; const lineH = fontSize; - const startY = cy - (lineCount - 1) * lineH / 2; - for (let i = 0; i < tryLines.length; i++) { - ctx.fillText(tryLines[i], cx, startY + i * lineH); + + if (hasCorrection) { + ctx.save(); + ctx.translate(cx, cy); + if (needsFlip) ctx.rotate(Math.PI); + if (needsMirrorFix) ctx.scale(-1, 1); + for (let i = 0; i < tryLines.length; i++) { + const dy = -(lineCount - 1) * lineH / 2 + i * lineH; + ctx.fillText(tryLines[i], 0, dy); + } + ctx.restore(); + } else { + const startY = cy - (lineCount - 1) * lineH / 2; + for (let i = 0; i < tryLines.length; i++) { + ctx.fillText(tryLines[i], cx, startY + i * lineH); + } } return; } @@ -611,8 +632,19 @@ function drawCurvedLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { // 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; + // Rotation: tangent to arc + let textRot = -midAngleRad + Math.PI / 2; + + // Add symbol rotation to check readability + const symRotRad = ((sym.rotation || 0) * Math.PI) / 180; + let worldAngle = (textRot + symRotRad) % (2 * Math.PI); + if (worldAngle < 0) worldAngle += 2 * Math.PI; + // Flip if text would be upside-down + if (worldAngle > Math.PI / 2 && worldAngle < Math.PI * 3 / 2) { + textRot += Math.PI; + } + // Handle mirror + if (sym.mirrored) textRot = -textRot; const availW = bandW - 4; diff --git a/svelte-app/src/lib/export.ts b/svelte-app/src/lib/export.ts index a0a150b..8f980ab 100644 --- a/svelte-app/src/lib/export.ts +++ b/svelte-app/src/lib/export.ts @@ -17,13 +17,13 @@ function parseConveyanceLabel(label: string): { lines: string[]; stripped: strin return { lines: [core], stripped: [core] }; } -/** Emit conveyance label text inside a — no transform needed (inherits from group) */ +/** Emit conveyance label text inside a — inherits outer transform from group */ function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) { if (!sym.label) return; const { lines: textLines } = parseConveyanceLabel(sym.label); - // Compute label center position based on shape type let labelCx: number, labelCy: number, availH: number; + let textRotDeg = 0; // additional rotation for the text if (isCurvedType(sym.symbolId)) { const angle = sym.curveAngle || 90; @@ -33,6 +33,15 @@ function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) { labelCx = arcCx + midR * Math.cos(midAngleRad); labelCy = arcCy - midR * Math.sin(midAngleRad); availH = bandW - 4; + // Tangent rotation (same as canvas renderer) + let textRotRad = -midAngleRad + Math.PI / 2; + // Check readability with outer rotation + const symRotRad = ((sym.rotation || 0) * Math.PI) / 180; + let worldAngle = (textRotRad + symRotRad) % (2 * Math.PI); + if (worldAngle < 0) worldAngle += 2 * Math.PI; + if (worldAngle > Math.PI / 2 && worldAngle < Math.PI * 3 / 2) textRotRad += Math.PI; + if (sym.mirrored) textRotRad = -textRotRad; + textRotDeg = (textRotRad * 180) / Math.PI; } else if (isSpurType(sym.symbolId)) { const w2 = sym.w2 ?? sym.w; labelCx = sym.x + (w2 + sym.w) / 4; @@ -50,14 +59,22 @@ function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) { availH = sym.h - 4; } + // For non-curved: check readability and flip if needed + if (!isCurvedType(sym.symbolId)) { + const rot = ((sym.rotation || 0) % 360 + 360) % 360; + if (rot > 90 && rot < 270) textRotDeg = 180; + // Mirror fix: text inside group inherits mirror, counter it + if (sym.mirrored && textRotDeg === 0) textRotDeg = 0; // mirrored handled by scale in group + } + const fontSize = Math.min(14, availH / textLines.length); if (fontSize < 4) return; const lineH = fontSize; - // Emit each line at absolute position — y is baseline, offset by 0.35*fontSize to center visually + const rotAttr = textRotDeg ? ` transform="rotate(${textRotDeg.toFixed(1)},${labelCx},${labelCy})"` : ''; for (let i = 0; i < textLines.length; i++) { const dy = -(textLines.length - 1) * lineH / 2 + i * lineH; const y = labelCy + dy + fontSize * 0.35; - lines.push(` ${textLines[i]}`); + lines.push(` ${textLines[i]}`); } }