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:
parent
2f5c43a07c
commit
896198c9d4
@ -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);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user