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;
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
const cx = sym.x + sym.w / 2;
|
||||
const isSelected = layout.selectedIds.has(sym.id);
|
||||
@ -576,12 +642,17 @@ function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
||||
strokeOutline(ctx, sym, 0);
|
||||
}
|
||||
|
||||
// Label: centered inside symbol if large enough, above if too small
|
||||
// Label rendering
|
||||
if (sym.label) {
|
||||
const isConveyance = isRectConveyanceType(sym.symbolId) || isExtendoType(sym.symbolId)
|
||||
|| isCurvedType(sym.symbolId) || isSpurType(sym.symbolId) || isInductionType(sym.symbolId);
|
||||
if (isConveyance) {
|
||||
drawConveyanceLabel(ctx, sym);
|
||||
} else {
|
||||
ctx.fillStyle = THEME.label.color;
|
||||
ctx.font = THEME.label.font;
|
||||
const metrics = ctx.measureText(sym.label);
|
||||
const textH = 3; // approximate font height
|
||||
const textH = 3;
|
||||
if (sym.w >= metrics.width + 4 && sym.h >= textH + 4) {
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
@ -593,6 +664,7 @@ function drawSymbolOverlays(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) {
|
||||
ctx.save();
|
||||
|
||||
@ -3,6 +3,36 @@ import { isEpcType, isInductionType, isSpurType, isCurvedType, isRectConveyanceT
|
||||
import { deserializeSymbol } from './serialization.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) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
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 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}"` : ''} />`);
|
||||
emitConveyanceLabel(lines, sym as PlacedSymbol, outerTransform);
|
||||
} else if (isCurvedType(sym.symbolId)) {
|
||||
const angle = sym.curveAngle || 90;
|
||||
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',
|
||||
].join(' ');
|
||||
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)) {
|
||||
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`;
|
||||
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)) {
|
||||
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)) {
|
||||
const bracketW = 10.6 / 31.07 * 73;
|
||||
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';
|
||||
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)) {
|
||||
const { leftCap, rightCap } = PHOTOEYE_CONFIG;
|
||||
const x = sym.x, y = sym.y, w = sym.w, h = sym.h;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user