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) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-30 18:28:16 +04:00
parent 2f5c43a07c
commit 896198c9d4

View File

@ -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 */ /** Draw label inside a conveyance symbol — black bold text, auto-sized to fit */
function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
if (!sym.label) return; if (!sym.label) return;
if (isCurvedType(sym.symbolId)) { drawCurvedLabel(ctx, sym); return; }
const { lines, stripped } = parseConveyanceLabel(sym.label); const { lines, stripped } = parseConveyanceLabel(sym.label);
const pad = 2; const pad = 2;
const availW = sym.w - pad * 2;
const availH = sym.h - pad * 2; // For spurs, center in the trapezoid shape, not bounding box
const cx = sym.x + sym.w / 2; let cx: number, cy: number, availW: number, availH: number;
const cy = sym.y + sym.h / 2; 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.fillStyle = '#000000';
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
// Try full lines first, then stripped (prefix removed) if it doesn't fit
for (const tryLines of [lines, stripped]) { for (const tryLines of [lines, stripped]) {
// Target font size: 14px, but scale down to fit
let fontSize = 14; let fontSize = 14;
const lineCount = tryLines.length; const lineCount = tryLines.length;
const totalTextH = () => fontSize * lineCount + (lineCount - 1) * 1; const totalTextH = () => fontSize * lineCount + (lineCount - 1) * 1;
// Scale down if too tall
while (totalTextH() > availH && fontSize > 4) fontSize -= 0.5; while (totalTextH() > availH && fontSize > 4) fontSize -= 0.5;
// Scale down if too wide
ctx.font = `bold ${fontSize}px Arial`; ctx.font = `bold ${fontSize}px Arial`;
let maxW = Math.max(...tryLines.map(l => ctx.measureText(l).width)); let maxW = Math.max(...tryLines.map(l => ctx.measureText(l).width));
while (maxW > availW && fontSize > 4) { 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) { function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
const cx = sym.x + sym.w / 2; const cx = sym.x + sym.w / 2;
const isSelected = layout.selectedIds.has(sym.id); const isSelected = layout.selectedIds.has(sym.id);