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 { 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 — absolute coordinates, no transforms (Ignition compatible) */
function emitConveyanceLabel(lines: string[], sym: PlacedSymbol, _outerTransform: string) {
if (!sym.label) return;
const { lines: textLines } = parseConveyanceLabel(sym.label);
// Compute label center position based on shape type
let labelCx: number, labelCy: number, availH: number;
if (isCurvedType(sym.symbolId)) {
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;
labelCx = arcCx + midR * Math.cos(midAngleRad);
labelCy = arcCy - midR * Math.sin(midAngleRad);
availH = bandW - 4;
} else if (isSpurType(sym.symbolId)) {
const w2 = sym.w2 ?? sym.w;
labelCx = sym.x + (w2 + sym.w) / 4;
labelCy = sym.y + sym.h / 2;
availH = sym.h - 4;
} else if (isInductionType(sym.symbolId)) {
const stripTopY = sym.y + sym.h * INDUCTION_CONFIG.stripTopFrac;
const stripBottomY = sym.y + sym.h * INDUCTION_CONFIG.stripBottomFrac;
labelCx = sym.x + (INDUCTION_CONFIG.headWidth + sym.w) / 2;
labelCy = (stripTopY + stripBottomY) / 2;
availH = stripBottomY - stripTopY - 4;
} else {
labelCx = sym.x + sym.w / 2;
labelCy = sym.y + sym.h / 2;
availH = sym.h - 4;
}
const fontSize = Math.min(14, availH / textLines.length);
if (fontSize < 4) return;
const lineH = fontSize;
// Emit each line at absolute position — y is baseline, offset by 0.35*fontSize to center visually
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]}`);
}
}
function downloadBlob(blob: Blob, filename: string) {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
/** Serialize child elements of an SVG, stripping xmlns added by XMLSerializer */
function serializeChildren(parent: Element): string {
return Array.from(parent.children)
.map(el => new XMLSerializer().serializeToString(el)
.replace(/ xmlns="http:\/\/www\.w3\.org\/2000\/svg"/g, ''))
.join('\n ');
}
export async function exportSVG() {
const lines: string[] = [
'',
`');
const mcmName = layout.currentMcm || 'export';
downloadBlob(new Blob([lines.join('\n')], { type: 'image/svg+xml' }), `${mcmName}_Detailed_View.svg`);
}
/** Emit EPC symbol — polyline + icon + right box, wrapped in with id/label */
async function emitEpc(lines: string[], sym: PlacedSymbol, label: string, outerTransform: string) {
const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints;
const ox = sym.x;
const oy = sym.y;
const parts: string[] = [];
const tAttr = outerTransform ? ` transform="${outerTransform}"` : '';
// Polyline
if (waypoints.length >= 2) {
const points = waypoints.map(wp => `${ox + wp.x},${oy + wp.y}`).join(' ');
parts.push(` `);
}
if (waypoints.length >= 2) {
// Left icon
const lb = EPC_CONFIG.leftBox;
const p0x = ox + waypoints[0].x, p0y = oy + waypoints[0].y;
const p1x = ox + waypoints[1].x, p1y = oy + waypoints[1].y;
const lAngle = Math.atan2(p1y - p0y, p1x - p0x) * 180 / Math.PI;
try {
const svgText = await (await fetch(EPC_CONFIG.iconFile)).text();
const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml');
const svgEl = doc.documentElement;
const vb = svgEl.getAttribute('viewBox');
const [vbX, vbY, vbW, vbH] = vb ? vb.split(/[\s,]+/).map(Number) : [0, 0, lb.w, lb.h];
const sx = lb.w / vbW;
const sy = lb.h / vbH;
const iconTransform = `translate(${p0x},${p0y}) rotate(${lAngle.toFixed(2)}) translate(${-lb.w},${-lb.h / 2}) scale(${sx.toFixed(6)},${sy.toFixed(6)}) translate(${-vbX},${-vbY})`;
const children = Array.from(svgEl.children);
if (children.length === 1 && children[0].tagName === 'g') {
const g = children[0];
const gT = g.getAttribute('transform') || '';
const fullT = gT ? `${iconTransform} ${gT}` : iconTransform;
parts.push(` `);
parts.push(` ${serializeChildren(g)}`);
parts.push(` `);
} else {
parts.push(` `);
parts.push(` ${serializeChildren(svgEl)}`);
parts.push(` `);
}
} catch {
parts.push(` `);
}
// Right box
const last = waypoints[waypoints.length - 1];
const prev = waypoints[waypoints.length - 2];
const plx = ox + last.x, ply = oy + last.y;
const ppx = ox + prev.x, ppy = oy + prev.y;
const rAngle = Math.atan2(ply - ppy, plx - ppx) * 180 / Math.PI;
const rb = EPC_CONFIG.rightBox;
parts.push(` `);
}
lines.push(` `);
lines.push(parts.join('\n'));
lines.push(' ');
}
export function loadLayoutJSON(file: File): Promise {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (ev) => {
try {
const data = JSON.parse(ev.target!.result as string);
layout.pushUndo();
if (data.gridSize) layout.gridSize = data.gridSize;
if (data.minSpacing) layout.minSpacing = data.minSpacing;
layout.symbols = [];
layout.nextId = 1;
for (const s of data.symbols) {
layout.symbols.push(deserializeSymbol(s, layout.nextId++));
}
layout.markDirty();
layout.saveMcmState();
resolve();
} catch (err) {
reject(err);
}
};
reader.readAsText(file);
});
}