Add internal labels for conveyance symbols (canvas + SVG export)
Parse labels like UL17_22_VFD into stacked text: "UL" / "17-22". Bold black Arial, targets 14px but auto-scales down to fit with consistent padding. Strips _VFD suffix, splits prefix from numbers. If full text doesn't fit, strips the letter prefix. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
09cafa4577
commit
07cee1c151
@ -522,6 +522,72 @@ function drawSymbolBody(ctx: CanvasRenderingContext2D, sym: PlacedSymbol): boole
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse a conveyance label like "UL17_22_VFD" into display lines.
|
||||||
|
* Strips known suffixes (_VFD, _VFD1, etc.), splits prefix from numbers,
|
||||||
|
* replaces underscores with dashes in the number part. */
|
||||||
|
function parseConveyanceLabel(label: string): { lines: string[]; stripped: string[] } {
|
||||||
|
// Strip known device suffixes
|
||||||
|
let core = label.replace(/_?VFD\d*$/i, '');
|
||||||
|
if (core === label) core = label; // no suffix found, use as-is
|
||||||
|
|
||||||
|
// Split into letter prefix and rest
|
||||||
|
const m = core.match(/^([A-Za-z]+)(.*)$/);
|
||||||
|
if (m) {
|
||||||
|
const prefix = m[1];
|
||||||
|
const nums = m[2].replace(/_/g, '-').replace(/^-/, '');
|
||||||
|
if (nums) {
|
||||||
|
return { lines: [prefix, nums], stripped: [nums] };
|
||||||
|
}
|
||||||
|
return { lines: [prefix], stripped: [prefix] };
|
||||||
|
}
|
||||||
|
return { lines: [core], stripped: [core] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Draw label inside a conveyance symbol — black bold text, auto-sized to fit */
|
||||||
|
function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
||||||
|
if (!sym.label) 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;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
fontSize -= 0.5;
|
||||||
|
ctx.font = `bold ${fontSize}px Arial`;
|
||||||
|
maxW = Math.max(...tryLines.map(l => ctx.measureText(l).width));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@ -576,20 +642,26 @@ function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
|||||||
strokeOutline(ctx, sym, 0);
|
strokeOutline(ctx, sym, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label: centered inside symbol if large enough, above if too small
|
// Label rendering
|
||||||
if (sym.label) {
|
if (sym.label) {
|
||||||
ctx.fillStyle = THEME.label.color;
|
const isConveyance = isRectConveyanceType(sym.symbolId) || isExtendoType(sym.symbolId)
|
||||||
ctx.font = THEME.label.font;
|
|| isCurvedType(sym.symbolId) || isSpurType(sym.symbolId) || isInductionType(sym.symbolId);
|
||||||
const metrics = ctx.measureText(sym.label);
|
if (isConveyance) {
|
||||||
const textH = 3; // approximate font height
|
drawConveyanceLabel(ctx, sym);
|
||||||
if (sym.w >= metrics.width + 4 && sym.h >= textH + 4) {
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.fillText(sym.label, cx, sym.y + sym.h / 2);
|
|
||||||
} else {
|
} else {
|
||||||
ctx.textAlign = 'center';
|
ctx.fillStyle = THEME.label.color;
|
||||||
ctx.textBaseline = 'bottom';
|
ctx.font = THEME.label.font;
|
||||||
ctx.fillText(sym.label, cx, sym.y + THEME.label.offsetY);
|
const metrics = ctx.measureText(sym.label);
|
||||||
|
const textH = 3;
|
||||||
|
if (sym.w >= metrics.width + 4 && sym.h >= textH + 4) {
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.fillText(sym.label, cx, sym.y + sym.h / 2);
|
||||||
|
} else {
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'bottom';
|
||||||
|
ctx.fillText(sym.label, cx, sym.y + THEME.label.offsetY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,36 @@ import { isEpcType, isInductionType, isSpurType, isCurvedType, isRectConveyanceT
|
|||||||
import { deserializeSymbol } from './serialization.js';
|
import { deserializeSymbol } from './serialization.js';
|
||||||
import type { PlacedSymbol } from './types.js';
|
import type { PlacedSymbol } from './types.js';
|
||||||
|
|
||||||
|
/** Parse conveyance label into display lines — same logic as renderer */
|
||||||
|
function parseConveyanceLabel(label: string): { lines: string[]; stripped: string[] } {
|
||||||
|
let core = label.replace(/_?VFD\d*$/i, '');
|
||||||
|
if (core === label) core = label;
|
||||||
|
const m = core.match(/^([A-Za-z]+)(.*)$/);
|
||||||
|
if (m) {
|
||||||
|
const prefix = m[1];
|
||||||
|
const nums = m[2].replace(/_/g, '-').replace(/^-/, '');
|
||||||
|
if (nums) return { lines: [prefix, nums], stripped: [nums] };
|
||||||
|
return { lines: [prefix], stripped: [prefix] };
|
||||||
|
}
|
||||||
|
return { lines: [core], stripped: [core] };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Emit conveyance label text element inside the shape */
|
||||||
|
function emitConveyanceLabel(lines: string[], sym: PlacedSymbol, outerTransform: string) {
|
||||||
|
if (!sym.label) return;
|
||||||
|
const { lines: textLines } = parseConveyanceLabel(sym.label);
|
||||||
|
const cx = sym.x + sym.w / 2;
|
||||||
|
const cy = sym.y + sym.h / 2;
|
||||||
|
const fontSize = Math.min(14, (sym.h - 4) / textLines.length);
|
||||||
|
if (fontSize < 4) return;
|
||||||
|
const lineH = fontSize;
|
||||||
|
const startY = cy - (textLines.length - 1) * lineH / 2;
|
||||||
|
const tAttr = outerTransform ? ` transform="${outerTransform}"` : '';
|
||||||
|
for (let i = 0; i < textLines.length; i++) {
|
||||||
|
lines.push(` <text x="${cx}" y="${startY + i * lineH}" text-anchor="middle" dominant-baseline="central" font-family="Arial" font-weight="bold" font-size="${fontSize}px" fill="#000000"${tAttr}>${textLines[i]}</text>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function downloadBlob(blob: Blob, filename: string) {
|
function downloadBlob(blob: Blob, filename: string) {
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
@ -65,6 +95,7 @@ export async function exportSVG() {
|
|||||||
const pts = INDUCTION_CONFIG.arrowPoints.map(([xf, yf]) => [sym.x + xf * hw, sym.y + yf * sym.h] as const);
|
const pts = INDUCTION_CONFIG.arrowPoints.map(([xf, yf]) => [sym.x + xf * hw, sym.y + yf * sym.h] as const);
|
||||||
const d = `M ${sym.x + sym.w},${stripTopY} L ${pts[0][0]},${stripTopY} ${pts.map(([px, py]) => `L ${px},${py}`).join(' ')} L ${pts[5][0]},${stripBottomY} L ${sym.x + sym.w},${stripBottomY} Z`;
|
const d = `M ${sym.x + sym.w},${stripTopY} L ${pts[0][0]},${stripTopY} ${pts.map(([px, py]) => `L ${px},${py}`).join(' ')} L ${pts[5][0]},${stripBottomY} L ${sym.x + sym.w},${stripBottomY} Z`;
|
||||||
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
||||||
|
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
|
||||||
} else if (isCurvedType(sym.symbolId)) {
|
} else if (isCurvedType(sym.symbolId)) {
|
||||||
const angle = sym.curveAngle || 90;
|
const angle = sym.curveAngle || 90;
|
||||||
const { arcCx, arcCy, outerR, innerR } = getCurveGeometry(sym.symbolId, sym.x, sym.y, sym.w, sym.h);
|
const { arcCx, arcCy, outerR, innerR } = getCurveGeometry(sym.symbolId, sym.x, sym.y, sym.w, sym.h);
|
||||||
@ -82,12 +113,15 @@ export async function exportSVG() {
|
|||||||
'Z',
|
'Z',
|
||||||
].join(' ');
|
].join(' ');
|
||||||
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
||||||
|
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
|
||||||
} else if (isSpurType(sym.symbolId)) {
|
} else if (isSpurType(sym.symbolId)) {
|
||||||
const w2 = sym.w2 ?? sym.w;
|
const w2 = sym.w2 ?? sym.w;
|
||||||
const d = `M ${sym.x},${sym.y} L ${sym.x + w2},${sym.y} L ${sym.x + sym.w},${sym.y + sym.h} L ${sym.x},${sym.y + sym.h} Z`;
|
const d = `M ${sym.x},${sym.y} L ${sym.x + w2},${sym.y} L ${sym.x + sym.w},${sym.y + sym.h} L ${sym.x},${sym.y + sym.h} Z`;
|
||||||
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
||||||
|
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
|
||||||
} else if (isRectConveyanceType(sym.symbolId)) {
|
} else if (isRectConveyanceType(sym.symbolId)) {
|
||||||
lines.push(` <rect ${idAttr} x="${sym.x}" y="${sym.y}" width="${sym.w}" height="${sym.h}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
lines.push(` <rect ${idAttr} x="${sym.x}" y="${sym.y}" width="${sym.w}" height="${sym.h}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
||||||
|
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
|
||||||
} else if (isExtendoType(sym.symbolId)) {
|
} else if (isExtendoType(sym.symbolId)) {
|
||||||
const bracketW = 10.6 / 31.07 * 73;
|
const bracketW = 10.6 / 31.07 * 73;
|
||||||
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
|
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
|
||||||
@ -107,6 +141,7 @@ export async function exportSVG() {
|
|||||||
];
|
];
|
||||||
const d = `M ${pts[0][0]},${pts[0][1]} ` + pts.slice(1).map(p => `L ${p[0]},${p[1]}`).join(' ') + ' Z';
|
const d = `M ${pts[0][0]},${pts[0][1]} ` + pts.slice(1).map(p => `L ${p[0]},${p[1]}`).join(' ') + ' Z';
|
||||||
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
lines.push(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="0.5"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
||||||
|
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
|
||||||
} else if (isPhotoeyeType(sym.symbolId)) {
|
} else if (isPhotoeyeType(sym.symbolId)) {
|
||||||
const { leftCap, rightCap } = PHOTOEYE_CONFIG;
|
const { leftCap, rightCap } = PHOTOEYE_CONFIG;
|
||||||
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
|
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user