From 3f122d917799fa02a86d1205695db7f25e0fb099 Mon Sep 17 00:00:00 2001 From: igurielidze Date: Tue, 31 Mar 2026 22:58:36 +0400 Subject: [PATCH] Refactor: extract shared code, add constants, clean up dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract parseConveyanceLabel to shared label-utils.ts (was duplicated) - Add EXTENDO_CONFIG, LABEL_CONFIG, CONVEYANCE_STYLE to symbol-config.ts - Replace all hardcoded fill/stroke/lineWidth with CONVEYANCE_STYLE - Replace magic font numbers (14, 4, 0.5) with LABEL_CONFIG constants - Extract drawSpurSymbol, drawExtendoSymbol, drawRectConveyanceSymbol from inline code — drawSymbolBody is now a clean dispatch - Convert getIgnitionTagPath from 18 if-statements to data-driven table - Add THEME.marquee for selection rectangle colors - Remove no-op assignment in parseConveyanceLabel Co-Authored-By: Claude Opus 4.6 (1M context) --- svelte-app/src/lib/canvas/render-theme.ts | 6 + svelte-app/src/lib/canvas/renderer.ts | 181 +++++++++++----------- svelte-app/src/lib/export.ts | 135 ++++++++-------- svelte-app/src/lib/label-utils.ts | 21 +++ svelte-app/src/lib/symbol-config.ts | 34 ++++ svelte-app/src/lib/symbols.ts | 4 +- 6 files changed, 214 insertions(+), 167 deletions(-) create mode 100644 svelte-app/src/lib/label-utils.ts diff --git a/svelte-app/src/lib/canvas/render-theme.ts b/svelte-app/src/lib/canvas/render-theme.ts index 9eecd50..85100e0 100644 --- a/svelte-app/src/lib/canvas/render-theme.ts +++ b/svelte-app/src/lib/canvas/render-theme.ts @@ -62,6 +62,12 @@ export const THEME = { strokeColor: '#000000', lineWidth: 1, }, + marquee: { + strokeColor: '#4a9eff', + lineWidth: 1, + dash: [4, 3] as readonly number[], + fillColor: 'rgba(74, 158, 255, 0.1)', + }, canvas: { maxRenderScale: 4, }, diff --git a/svelte-app/src/lib/canvas/renderer.ts b/svelte-app/src/lib/canvas/renderer.ts index ba5e32e..6115b7c 100644 --- a/svelte-app/src/lib/canvas/renderer.ts +++ b/svelte-app/src/lib/canvas/renderer.ts @@ -1,8 +1,9 @@ import { layout } from '../stores/layout.svelte.js'; -import { getSymbolImage, isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, isPhotoeyeType, isRectConveyanceType, isExtendoType, getCurveGeometry, getSymbolGroup, SPACING_EXEMPT, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG } from '../symbols.js'; +import { getSymbolImage, isResizable, isCurvedType, isSpurType, isEpcType, isInductionType, isPhotoeyeType, isRectConveyanceType, isExtendoType, getCurveGeometry, getSymbolGroup, SPACING_EXEMPT, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG, EXTENDO_CONFIG, LABEL_CONFIG, CONVEYANCE_STYLE } from '../symbols.js'; import { checkSpacingViolation } from './collision.js'; import { marqueeRect } from './interactions.js'; import { THEME } from './render-theme.js'; +import { parseConveyanceLabel } from '../label-utils.js'; import type { PlacedSymbol } from '../types.js'; let ctx: CanvasRenderingContext2D | null = null; @@ -82,10 +83,10 @@ export function render() { // Marquee selection rectangle if (marqueeRect) { - ctx.strokeStyle = '#4a9eff'; - ctx.lineWidth = 1; - ctx.setLineDash([4, 3]); - ctx.fillStyle = 'rgba(74, 158, 255, 0.1)'; + ctx.strokeStyle = THEME.marquee.strokeColor; + ctx.lineWidth = THEME.marquee.lineWidth; + ctx.setLineDash(THEME.marquee.dash as number[]); + ctx.fillStyle = THEME.marquee.fillColor; ctx.fillRect(marqueeRect.x, marqueeRect.y, marqueeRect.w, marqueeRect.h); ctx.strokeRect(marqueeRect.x, marqueeRect.y, marqueeRect.w, marqueeRect.h); ctx.setLineDash([]); @@ -436,9 +437,9 @@ function drawPhotoeyeSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { ctx.lineTo(x + w - rightCap, beamTop); ctx.closePath(); - ctx.fillStyle = '#ffffff'; - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 1; + ctx.fillStyle = CONVEYANCE_STYLE.fillColor; + ctx.strokeStyle = CONVEYANCE_STYLE.strokeColor; + ctx.lineWidth = CONVEYANCE_STYLE.lineWidth; ctx.fill(); ctx.stroke(); } @@ -454,13 +455,64 @@ function drawCurvedSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { ctx.arc(arcCx, arcCy, innerR, -sweepRad, 0, false); ctx.closePath(); - ctx.fillStyle = '#ffffff'; - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 1; + ctx.fillStyle = CONVEYANCE_STYLE.fillColor; + ctx.strokeStyle = CONVEYANCE_STYLE.strokeColor; + ctx.lineWidth = CONVEYANCE_STYLE.lineWidth; ctx.fill(); ctx.stroke(); } +/** Draw spur trapezoid programmatically so w and w2 are respected during resize */ +function drawSpurSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { + const w2 = sym.w2 ?? sym.w; + ctx.beginPath(); + ctx.moveTo(sym.x, sym.y); + ctx.lineTo(sym.x + w2, sym.y); + ctx.lineTo(sym.x + sym.w, sym.y + sym.h); + ctx.lineTo(sym.x, sym.y + sym.h); + ctx.closePath(); + ctx.fillStyle = CONVEYANCE_STYLE.fillColor; + ctx.strokeStyle = CONVEYANCE_STYLE.strokeColor; + ctx.lineWidth = CONVEYANCE_STYLE.lineWidth; + ctx.fill(); + ctx.stroke(); +} + +/** Draw extendo: fixed left bracket + stretchy right belt */ +function drawExtendoSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { + const bracketW = EXTENDO_CONFIG.bracketWidthRatio * EXTENDO_CONFIG.defaultWidth; + const p = EXTENDO_CONFIG.points; + const x = sym.x, y = sym.y, w = sym.w, h = sym.h; + ctx.beginPath(); + ctx.moveTo(x + bracketW * p.tabTopRight.x, y + h * p.tabTopRight.y); + ctx.lineTo(x + bracketW * p.bracketTopRight.x, y + h * p.bracketTopRight.y); + ctx.lineTo(x + bracketW * p.beltTop.x, y + h * p.beltTop.y); + ctx.lineTo(x + w, y + h * p.beltTop.y); + ctx.lineTo(x + w, y + h * p.beltBottom.y); + ctx.lineTo(x + bracketW * p.beltBottom.x, y + h * p.beltBottom.y); + ctx.lineTo(x + bracketW * p.bracketBottomRight.x, y + h * p.bracketBottomRight.y); + ctx.lineTo(x + bracketW * p.bracketBottomLeft.x, y + h * p.bracketBottomLeft.y); + ctx.lineTo(x + bracketW * p.notchBottom.x, y + h * p.notchBottom.y); + ctx.lineTo(x, y + h * p.farLeftBottom.y); + ctx.lineTo(x, y + h * p.farLeftTop.y); + ctx.lineTo(x + bracketW * p.tabTopLeft.x, y + h * p.tabTopLeft.y); + ctx.closePath(); + ctx.fillStyle = CONVEYANCE_STYLE.fillColor; + ctx.strokeStyle = CONVEYANCE_STYLE.strokeColor; + ctx.lineWidth = CONVEYANCE_STYLE.lineWidth; + ctx.fill(); + ctx.stroke(); +} + +/** Draw rectangular conveyance (conveyor, chute, etc.) */ +function drawRectConveyanceSymbol(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { + ctx.fillStyle = CONVEYANCE_STYLE.fillColor; + ctx.strokeStyle = CONVEYANCE_STYLE.strokeColor; + ctx.lineWidth = CONVEYANCE_STYLE.lineWidth; + ctx.fillRect(sym.x, sym.y, sym.w, sym.h); + ctx.strokeRect(sym.x, sym.y, sym.w, sym.h); +} + function drawSymbolBody(ctx: CanvasRenderingContext2D, sym: PlacedSymbol): boolean { if (isEpcType(sym.symbolId)) { drawEpcSymbol(ctx, sym); @@ -469,49 +521,11 @@ function drawSymbolBody(ctx: CanvasRenderingContext2D, sym: PlacedSymbol): boole } else if (isCurvedType(sym.symbolId)) { drawCurvedSymbol(ctx, sym); } else if (isSpurType(sym.symbolId)) { - // Draw trapezoid programmatically so w and w2 are respected during resize - const w2 = sym.w2 ?? sym.w; - ctx.beginPath(); - ctx.moveTo(sym.x, sym.y); - ctx.lineTo(sym.x + w2, sym.y); - ctx.lineTo(sym.x + sym.w, sym.y + sym.h); - ctx.lineTo(sym.x, sym.y + sym.h); - ctx.closePath(); - ctx.fillStyle = '#ffffff'; - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 1; - ctx.fill(); - ctx.stroke(); + drawSpurSymbol(ctx, sym); } else if (isExtendoType(sym.symbolId)) { - // Extendo: fixed left bracket + stretchy right belt - // Y fractions from original SVG path, X uses fixed left bracket width - const bracketW = 10.6 / 31.07 * 73; // ~24.9 px at default 73w — fixed portion - const x = sym.x, y = sym.y, w = sym.w, h = sym.h; - ctx.beginPath(); - ctx.moveTo(x + bracketW * 0.44, y + h * 0.085); // tab top-right - ctx.lineTo(x + bracketW, y + h * 0.085); // bracket top-right - ctx.lineTo(x + bracketW, y + h * 0.222); // step down to belt top - ctx.lineTo(x + w, y + h * 0.222); // belt top-right - ctx.lineTo(x + w, y + h * 0.780); // belt bottom-right - ctx.lineTo(x + bracketW, y + h * 0.780); // belt bottom-left - ctx.lineTo(x + bracketW, y + h * 0.917); // step down bracket bottom - ctx.lineTo(x + bracketW * 0.44, y + h * 0.916); // bracket bottom-left - ctx.lineTo(x + bracketW * 0.34, y + h * 0.985); // notch bottom - ctx.lineTo(x, y + h * 0.980); // far left bottom - ctx.lineTo(x, y + h * 0.017); // far left top - ctx.lineTo(x + bracketW * 0.34, y + h * 0.016); // tab top-left - ctx.closePath(); - ctx.fillStyle = '#ffffff'; - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 1; - ctx.fill(); - ctx.stroke(); + drawExtendoSymbol(ctx, sym); } else if (isRectConveyanceType(sym.symbolId)) { - ctx.fillStyle = '#ffffff'; - ctx.strokeStyle = '#000000'; - ctx.lineWidth = 1; - ctx.fillRect(sym.x, sym.y, sym.w, sym.h); - ctx.strokeRect(sym.x, sym.y, sym.w, sym.h); + drawRectConveyanceSymbol(ctx, sym); } else if (isPhotoeyeType(sym.symbolId)) { drawPhotoeyeSymbol(ctx, sym); } else { @@ -522,27 +536,6 @@ 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; @@ -550,16 +543,16 @@ function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { if (isCurvedType(sym.symbolId)) { drawCurvedLabel(ctx, sym); return; } const { lines, stripped } = parseConveyanceLabel(sym.label); - const pad = 2; + const pad = LABEL_CONFIG.padding; // For spurs, center in the trapezoid shape, not bounding box let cx: number, cy: number, availW: number, availH: number; if (isSpurType(sym.symbolId)) { const w2 = sym.w2 ?? sym.w; - // Compute text dimensions at 14px to find optimal placement - ctx.font = 'bold 14px Arial'; + // Compute text dimensions at default fontSize to find optimal placement + ctx.font = LABEL_CONFIG.font; const maxTextW = Math.max(...lines.map(l => ctx.measureText(l).width)); - const th = 14 * lines.length; + const th = LABEL_CONFIG.fontSize * lines.length; // Push text down where trapezoid is wider, but leave room const optCy = Math.min(sym.h - th / 2 - 1, sym.h * 0.55); // Right edge at top of text — text must not exceed this @@ -585,27 +578,27 @@ function drawConveyanceLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { const needsMirrorFix = !!sym.mirrored; const hasCorrection = needsFlip || needsMirrorFix; - ctx.fillStyle = '#000000'; + ctx.fillStyle = LABEL_CONFIG.color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; for (const tryLines of [lines, stripped]) { - let fontSize = 14; + let fontSize = LABEL_CONFIG.fontSize; const lineCount = tryLines.length; const totalTextH = () => fontSize * lineCount + (lineCount - 1) * 1; - while (totalTextH() > availH && fontSize > 4) fontSize -= 0.5; + while (totalTextH() > availH && fontSize > LABEL_CONFIG.minFontSize) fontSize -= LABEL_CONFIG.fontSizeStep; - ctx.font = `bold ${fontSize}px Arial`; + ctx.font = `bold ${fontSize}px ${LABEL_CONFIG.fontFamily}`; 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`; + while (maxW > availW && fontSize > LABEL_CONFIG.minFontSize) { + fontSize -= LABEL_CONFIG.fontSizeStep; + ctx.font = `bold ${fontSize}px ${LABEL_CONFIG.fontFamily}`; maxW = Math.max(...tryLines.map(l => ctx.measureText(l).width)); } - if (fontSize >= 4) { - ctx.font = `bold ${fontSize}px Arial`; + if (fontSize >= LABEL_CONFIG.minFontSize) { + ctx.font = `bold ${fontSize}px ${LABEL_CONFIG.fontFamily}`; const lineH = fontSize; if (hasCorrection) { @@ -659,24 +652,24 @@ function drawCurvedLabel(ctx: CanvasRenderingContext2D, sym: PlacedSymbol) { ctx.save(); ctx.translate(textX, textY); ctx.rotate(textRot); - ctx.fillStyle = '#000000'; + ctx.fillStyle = LABEL_CONFIG.color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; for (const tryLines of [lines, stripped]) { - let fontSize = 14; + let fontSize = LABEL_CONFIG.fontSize; const lineCount = tryLines.length; - while (fontSize * lineCount > availW && fontSize > 4) fontSize -= 0.5; - ctx.font = `bold ${fontSize}px Arial`; + while (fontSize * lineCount > availW && fontSize > LABEL_CONFIG.minFontSize) fontSize -= LABEL_CONFIG.fontSizeStep; + ctx.font = `bold ${fontSize}px ${LABEL_CONFIG.fontFamily}`; 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`; + while (maxW > arcLen && fontSize > LABEL_CONFIG.minFontSize) { + fontSize -= LABEL_CONFIG.fontSizeStep; + ctx.font = `bold ${fontSize}px ${LABEL_CONFIG.fontFamily}`; maxW = Math.max(...tryLines.map(l => ctx.measureText(l).width)); } - if (fontSize >= 4) { + if (fontSize >= LABEL_CONFIG.minFontSize) { const lineH = fontSize; const startY = -(lineCount - 1) * lineH / 2; for (let i = 0; i < tryLines.length; i++) { diff --git a/svelte-app/src/lib/export.ts b/svelte-app/src/lib/export.ts index 1ea53bf..6f82fde 100644 --- a/svelte-app/src/lib/export.ts +++ b/svelte-app/src/lib/export.ts @@ -1,22 +1,9 @@ import { layout } from './stores/layout.svelte.js'; -import { isEpcType, isInductionType, isSpurType, isCurvedType, isRectConveyanceType, isExtendoType, isPhotoeyeType, getSymbolGroup, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG, getCurveGeometry } from './symbols.js'; +import { isEpcType, isInductionType, isSpurType, isCurvedType, isRectConveyanceType, isExtendoType, isPhotoeyeType, getSymbolGroup, EPC_CONFIG, INDUCTION_CONFIG, PHOTOEYE_CONFIG, EXTENDO_CONFIG, LABEL_CONFIG, CONVEYANCE_STYLE, getCurveGeometry } from './symbols.js'; import { serializeSymbol, deserializeSymbol } from './serialization.js'; +import { parseConveyanceLabel } from './label-utils.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 inside a — inherits outer transform from group */ function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) { if (!sym.label) return; @@ -44,7 +31,7 @@ function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) { textRotDeg = (textRotRad * 180) / Math.PI; } else if (isSpurType(sym.symbolId)) { const w2 = sym.w2 ?? sym.w; - const fontSize = 14; + const fontSize = LABEL_CONFIG.fontSize; const th = fontSize * textLines.length; const optCy = Math.min(sym.h - th / 2 - 1, sym.h * 0.55); const textTop = optCy - th / 2; @@ -75,8 +62,8 @@ function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) { needsMirrorFix = !!sym.mirrored; } - const fontSize = Math.min(14, availH / textLines.length); - if (fontSize < 4) return; + const fontSize = Math.min(LABEL_CONFIG.fontSize, availH / textLines.length); + if (fontSize < LABEL_CONFIG.minFontSize) return; const lineH = fontSize; // Build transform: counter-mirror then rotate for readability let transformParts: string[] = []; @@ -85,8 +72,8 @@ function emitConveyanceLabelInner(lines: string[], sym: PlacedSymbol) { const rotAttr = transformParts.length ? ` transform="${transformParts.join(' ')}"` : ''; 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(` ${textLines[i]}`); + const y = labelCy + dy + fontSize * LABEL_CONFIG.baselineOffset; + lines.push(` ${textLines[i]}`); } } @@ -111,44 +98,49 @@ export function exportJSON() { downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `${mcmName}_layout.json`); } +/** Declarative tag-path rules: order matters (longer/more-specific patterns first). + * Each rule maps a label suffix pattern to the Ignition folder path. */ +const TAG_PATH_RULES: Array<{ pattern: RegExp; path: string }> = [ + // VFD / EPC + { pattern: /_VFD\d*$/i, path: 'VFD/APF' }, + { pattern: /_EPC\d*$/i, path: 'VFD/APF' }, + // Sensors + { pattern: /_TPE\d*$/i, path: 'Sensor/Tracking' }, + { pattern: /_LPE\d*$/i, path: 'Sensor/Long_Range' }, + { pattern: /_JPE\d*$/i, path: 'Sensor/Jam' }, + { pattern: /_FPE\d*$/i, path: 'Sensor/Full' }, + { pattern: /_PS\d*$/i, path: 'Sensor/Pressure' }, + { pattern: /_BDS\d+_[RS]$/i, path: 'Sensor/Tracking' }, + { pattern: /_TS\d+_[RS]$/i, path: 'Sensor/Tracking' }, + // Network nodes + { pattern: /_FIOM\d*$/i, path: 'Network_Node/FIO' }, + { pattern: /_FIOH\d*$/i, path: 'Network_Node/HUB' }, + { pattern: /_SIO\d*$/i, path: 'Network_Node/SIO' }, + { pattern: /_DPM\d*$/i, path: 'Network_Node/DPM' }, + // Station — order matters: longer patterns first + { pattern: /_SS\d+_S(?:T)?PB$/i, path: 'Station/Start_Stop' }, + { pattern: /_CH\d*_EN\d*_PB$/i, path: 'Station/Jam_Reset_Chute_Bank' }, + { pattern: /_JR\d*_PB$/i, path: 'Station/Jam_Reset' }, + { pattern: /_S\d+_PB$/i, path: 'Station/Start' }, + // Beacon + { pattern: /_BCN\d*$/i, path: 'Beacon' }, + // Solenoid (including DIV*_SOL) + { pattern: /_SOL\d*$/i, path: 'Solenoid' }, + // Diverter (DIV*_LS) + { pattern: /_DIV\d+_LS\d*$/i, path: 'Diverter' }, + // PDP + { pattern: /^PDP\d*/i, path: 'PDP' }, +]; + /** Build Ignition tag path from device label and MCM name. * Format: System/{MCM}/{Category}/{SubCategory}/{Label} * Mappings derived from Excel device manifest suffixes. */ function getIgnitionTagPath(label: string, mcm: string): string | null { if (!label) return null; - - // VFD / EPC - if (/_VFD\d*$/i.test(label)) return `System/${mcm}/VFD/APF/${label}`; - if (/_EPC\d*$/i.test(label)) return `System/${mcm}/VFD/APF/${label}`; - // Sensors - if (/_TPE\d*$/i.test(label)) return `System/${mcm}/Sensor/Tracking/${label}`; - if (/_LPE\d*$/i.test(label)) return `System/${mcm}/Sensor/Long_Range/${label}`; - if (/_JPE\d*$/i.test(label)) return `System/${mcm}/Sensor/Jam/${label}`; - if (/_FPE\d*$/i.test(label)) return `System/${mcm}/Sensor/Full/${label}`; - if (/_PS\d*$/i.test(label)) return `System/${mcm}/Sensor/Pressure/${label}`; - if (/_BDS\d+_[RS]$/i.test(label)) return `System/${mcm}/Sensor/Tracking/${label}`; - if (/_TS\d+_[RS]$/i.test(label)) return `System/${mcm}/Sensor/Tracking/${label}`; - // Network nodes - if (/_FIOM\d*$/i.test(label)) return `System/${mcm}/Network_Node/FIO/${label}`; - if (/_FIOH\d*$/i.test(label)) return `System/${mcm}/Network_Node/HUB/${label}`; - if (/_SIO\d*$/i.test(label)) return `System/${mcm}/Network_Node/SIO/${label}`; - if (/_DPM\d*$/i.test(label)) return `System/${mcm}/Network_Node/DPM/${label}`; - // Station — order matters: longer patterns first - if (/_SS\d+_S(?:T)?PB$/i.test(label)) return `System/${mcm}/Station/Start_Stop/${label}`; - if (/_CH\d*_EN\d*_PB$/i.test(label)) return `System/${mcm}/Station/Jam_Reset_Chute_Bank/${label}`; - if (/_JR\d*_PB$/i.test(label)) return `System/${mcm}/Station/Jam_Reset/${label}`; - if (/_S\d+_PB$/i.test(label)) return `System/${mcm}/Station/Start/${label}`; - // Beacon - if (/_BCN\d*$/i.test(label)) return `System/${mcm}/Beacon/${label}`; - // Solenoid (including DIV*_SOL) - if (/_SOL\d*$/i.test(label)) return `System/${mcm}/Solenoid/${label}`; - // Diverter (DIV*_LS) - if (/_DIV\d+_LS\d*$/i.test(label)) return `System/${mcm}/Diverter/${label}`; - // PDP - if (/^PDP\d*/i.test(label)) return `System/${mcm}/PDP/${label}`; - // MCM if (/^MCM\d*/i.test(label)) return null; - + for (const rule of TAG_PATH_RULES) { + if (rule.pattern.test(label)) return `System/${mcm}/${rule.path}/${label}`; + } return null; } @@ -226,7 +218,7 @@ async function buildSvgString(): Promise { 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(` `); - lines.push(` `); + lines.push(` `); emitConveyanceLabelInner(lines, sym as PlacedSymbol); lines.push(` `); } else if (isCurvedType(sym.symbolId)) { @@ -246,41 +238,42 @@ async function buildSvgString(): Promise { 'Z', ].join(' '); lines.push(` `); - lines.push(` `); + lines.push(` `); emitConveyanceLabelInner(lines, sym as PlacedSymbol); lines.push(` `); } 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(` `); - lines.push(` `); + lines.push(` `); emitConveyanceLabelInner(lines, sym as PlacedSymbol); lines.push(` `); } else if (isRectConveyanceType(sym.symbolId)) { lines.push(` `); - lines.push(` `); + lines.push(` `); emitConveyanceLabelInner(lines, sym as PlacedSymbol); lines.push(` `); } else if (isExtendoType(sym.symbolId)) { - const bracketW = 10.6 / 31.07 * 73; + const bracketW = EXTENDO_CONFIG.bracketWidthRatio * EXTENDO_CONFIG.defaultWidth; + const p = EXTENDO_CONFIG.points; const x = sym.x, y = sym.y, w = sym.w, h = sym.h; const pts = [ - [x + bracketW * 0.44, y + h * 0.085], - [x + bracketW, y + h * 0.085], - [x + bracketW, y + h * 0.222], - [x + w, y + h * 0.222], - [x + w, y + h * 0.780], - [x + bracketW, y + h * 0.780], - [x + bracketW, y + h * 0.917], - [x + bracketW * 0.44, y + h * 0.916], - [x + bracketW * 0.34, y + h * 0.985], - [x, y + h * 0.980], - [x, y + h * 0.017], - [x + bracketW * 0.34, y + h * 0.016], + [x + bracketW * p.tabTopRight.x, y + h * p.tabTopRight.y], + [x + bracketW * p.bracketTopRight.x, y + h * p.bracketTopRight.y], + [x + bracketW * p.beltTop.x, y + h * p.beltTop.y], + [x + w, y + h * p.beltTop.y], + [x + w, y + h * p.beltBottom.y], + [x + bracketW * p.beltBottom.x, y + h * p.beltBottom.y], + [x + bracketW * p.bracketBottomRight.x, y + h * p.bracketBottomRight.y], + [x + bracketW * p.bracketBottomLeft.x, y + h * p.bracketBottomLeft.y], + [x + bracketW * p.notchBottom.x, y + h * p.notchBottom.y], + [x, y + h * p.farLeftBottom.y], + [x, y + h * p.farLeftTop.y], + [x + bracketW * p.tabTopLeft.x, y + h * p.tabTopLeft.y], ]; const d = `M ${pts[0][0]},${pts[0][1]} ` + pts.slice(1).map(p => `L ${p[0]},${p[1]}`).join(' ') + ' Z'; lines.push(` `); - lines.push(` `); + lines.push(` `); emitConveyanceLabelInner(lines, sym as PlacedSymbol); lines.push(` `); } else if (isPhotoeyeType(sym.symbolId)) { @@ -301,7 +294,7 @@ async function buildSvgString(): Promise { [x + w - rightCap, y + h * 0.42], ]; const d = `M ${pts[0][0]},${pts[0][1]} ` + pts.slice(1).map(p => `L ${p[0]},${p[1]}`).join(' ') + ' Z'; - lines.push(` `); + lines.push(` `); } else { // Regular SVG symbol try { diff --git a/svelte-app/src/lib/label-utils.ts b/svelte-app/src/lib/label-utils.ts new file mode 100644 index 0000000..7db9902 --- /dev/null +++ b/svelte-app/src/lib/label-utils.ts @@ -0,0 +1,21 @@ +/** Shared conveyance label parsing — used by both canvas renderer and SVG export */ + +/** 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. */ +export function parseConveyanceLabel(label: string): { lines: string[]; stripped: string[] } { + // Strip known device suffixes + let core = label.replace(/_?VFD\d*$/i, ''); + + // 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] }; +} diff --git a/svelte-app/src/lib/symbol-config.ts b/svelte-app/src/lib/symbol-config.ts index 9037577..90813f4 100644 --- a/svelte-app/src/lib/symbol-config.ts +++ b/svelte-app/src/lib/symbol-config.ts @@ -33,6 +33,40 @@ export const PHOTOEYE_CONFIG = { defaultWidth: 56, // original PE width } as const; +export const EXTENDO_CONFIG = { + bracketWidthRatio: 10.6 / 31.07, + defaultWidth: 73, + points: { + tabTopRight: { x: 0.44, y: 0.085 }, + bracketTopRight: { x: 1, y: 0.085 }, + beltTop: { x: 1, y: 0.222 }, + beltBottom: { x: 1, y: 0.780 }, + bracketBottomRight: { x: 1, y: 0.917 }, + bracketBottomLeft: { x: 0.44, y: 0.916 }, + notchBottom: { x: 0.34, y: 0.985 }, + farLeftBottom: { x: 0, y: 0.980 }, + farLeftTop: { x: 0, y: 0.017 }, + tabTopLeft: { x: 0.34, y: 0.016 }, + }, +} as const; + +export const LABEL_CONFIG = { + fontSize: 14, + minFontSize: 4, + fontSizeStep: 0.5, + padding: 2, + font: 'bold 14px Arial', + fontFamily: 'Arial', + baselineOffset: 0.35, + color: '#000000', +} as const; + +export const CONVEYANCE_STYLE = { + fillColor: '#ffffff', + strokeColor: '#000000', + lineWidth: 1, +} as const; + export const CURVE_CONFIG = { // Fractions of display size, derived from SVG viewBox "-2 -2 104 104" // All curved SVGs have arc center at viewBox (0,100), outerR=95 diff --git a/svelte-app/src/lib/symbols.ts b/svelte-app/src/lib/symbols.ts index f8484f0..5d0b5ed 100644 --- a/svelte-app/src/lib/symbols.ts +++ b/svelte-app/src/lib/symbols.ts @@ -1,6 +1,6 @@ import type { SymbolDef } from './types.js'; -import { EPC_CONFIG, INDUCTION_CONFIG, CURVE_CONFIG, PHOTOEYE_CONFIG } from './symbol-config.js'; -export { EPC_CONFIG, INDUCTION_CONFIG, CURVE_CONFIG, PHOTOEYE_CONFIG }; +import { EPC_CONFIG, INDUCTION_CONFIG, CURVE_CONFIG, PHOTOEYE_CONFIG, EXTENDO_CONFIG, LABEL_CONFIG, CONVEYANCE_STYLE } from './symbol-config.js'; +export { EPC_CONFIG, INDUCTION_CONFIG, CURVE_CONFIG, PHOTOEYE_CONFIG, EXTENDO_CONFIG, LABEL_CONFIG, CONVEYANCE_STYLE }; export const SYMBOLS: SymbolDef[] = [ // --- Conveyance > Conveyor ---