Refactor: extract shared code, add constants, clean up dispatch
- 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) <noreply@anthropic.com>
This commit is contained in:
parent
8a52449dfe
commit
3f122d9177
@ -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,
|
||||
},
|
||||
|
||||
@ -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++) {
|
||||
|
||||
@ -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 <g> — 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(` <text x="${labelCx}" y="${y}" text-anchor="middle" style="font-family:Arial;font-weight:bold;font-size:${fontSize}px;fill:#000000"${rotAttr}>${textLines[i]}</text>`);
|
||||
const y = labelCy + dy + fontSize * LABEL_CONFIG.baselineOffset;
|
||||
lines.push(` <text x="${labelCx}" y="${y}" text-anchor="middle" style="font-family:${LABEL_CONFIG.fontFamily};font-weight:bold;font-size:${fontSize}px;fill:${LABEL_CONFIG.color}"${rotAttr}>${textLines[i]}</text>`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<string> {
|
||||
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(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
|
||||
lines.push(` <path d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
|
||||
lines.push(` <path d="${d}" fill="${CONVEYANCE_STYLE.fillColor}" stroke="${CONVEYANCE_STYLE.strokeColor}" stroke-width="${CONVEYANCE_STYLE.lineWidth}" />`);
|
||||
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
|
||||
lines.push(` </g>`);
|
||||
} else if (isCurvedType(sym.symbolId)) {
|
||||
@ -246,41 +238,42 @@ async function buildSvgString(): Promise<string> {
|
||||
'Z',
|
||||
].join(' ');
|
||||
lines.push(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
|
||||
lines.push(` <path d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
|
||||
lines.push(` <path d="${d}" fill="${CONVEYANCE_STYLE.fillColor}" stroke="${CONVEYANCE_STYLE.strokeColor}" stroke-width="${CONVEYANCE_STYLE.lineWidth}" />`);
|
||||
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
|
||||
lines.push(` </g>`);
|
||||
} 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(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
|
||||
lines.push(` <path d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
|
||||
lines.push(` <path d="${d}" fill="${CONVEYANCE_STYLE.fillColor}" stroke="${CONVEYANCE_STYLE.strokeColor}" stroke-width="${CONVEYANCE_STYLE.lineWidth}" />`);
|
||||
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
|
||||
lines.push(` </g>`);
|
||||
} else if (isRectConveyanceType(sym.symbolId)) {
|
||||
lines.push(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
|
||||
lines.push(` <rect x="${sym.x}" y="${sym.y}" width="${sym.w}" height="${sym.h}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
|
||||
lines.push(` <rect x="${sym.x}" y="${sym.y}" width="${sym.w}" height="${sym.h}" fill="${CONVEYANCE_STYLE.fillColor}" stroke="${CONVEYANCE_STYLE.strokeColor}" stroke-width="${CONVEYANCE_STYLE.lineWidth}" />`);
|
||||
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
|
||||
lines.push(` </g>`);
|
||||
} 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(` <g ${idAttr}${outerTransform ? ` transform="${outerTransform}"` : ''}>`);
|
||||
lines.push(` <path d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1" />`);
|
||||
lines.push(` <path d="${d}" fill="${CONVEYANCE_STYLE.fillColor}" stroke="${CONVEYANCE_STYLE.strokeColor}" stroke-width="${CONVEYANCE_STYLE.lineWidth}" />`);
|
||||
emitConveyanceLabelInner(lines, sym as PlacedSymbol);
|
||||
lines.push(` </g>`);
|
||||
} else if (isPhotoeyeType(sym.symbolId)) {
|
||||
@ -301,7 +294,7 @@ async function buildSvgString(): Promise<string> {
|
||||
[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(` <path ${idAttr} d="${d}" fill="#ffffff" stroke="#000000" stroke-width="1"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
||||
lines.push(` <path ${idAttr} d="${d}" fill="${CONVEYANCE_STYLE.fillColor}" stroke="${CONVEYANCE_STYLE.strokeColor}" stroke-width="${CONVEYANCE_STYLE.lineWidth}"${outerTransform ? ` transform="${outerTransform}"` : ''} />`);
|
||||
} else {
|
||||
// Regular SVG symbol
|
||||
try {
|
||||
|
||||
21
svelte-app/src/lib/label-utils.ts
Normal file
21
svelte-app/src/lib/label-utils.ts
Normal file
@ -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] };
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 ---
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user