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) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-30 21:09:40 +04:00
parent ea367df42a
commit 533465be3c
2 changed files with 58 additions and 9 deletions

View File

@ -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,10 +598,23 @@ function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
if (fontSize >= 4) {
ctx.font = `bold ${fontSize}px Arial`;
const lineH = fontSize;
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;

View File

@ -17,13 +17,13 @@ function parseConveyanceLabel(label: string): { lines: string[]; stripped: strin
return { lines: [core], stripped: [core] };
}
/** Emit conveyance label text inside a <g> — no transform needed (inherits from group) */
/** Emit conveyance label text inside a <g> — 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(` <text x="${labelCx}" y="${y}" text-anchor="middle" font-family="Arial" font-weight="bold" font-size="${fontSize}px" fill="#000000">${textLines[i]}</text>`);
lines.push(` <text x="${labelCx}" y="${y}" text-anchor="middle" font-family="Arial" font-weight="bold" font-size="${fontSize}px" fill="#000000"${rotAttr}>${textLines[i]}</text>`);
}
}