import { layout } from './stores/layout.svelte.js'; import { isEpcType, isInductionType, isSpurType, isCurvedType, getSymbolGroup, EPC_CONFIG, INDUCTION_CONFIG, getCurveGeometry } from './symbols.js'; import { deserializeSymbol } from './serialization.js'; import type { PlacedSymbol } from './types.js'; 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[] = [ '', `', ` `, ]; for (const sym of layout.symbols) { if (sym.hidden || layout.hiddenGroups.has(getSymbolGroup(sym.symbolId))) continue; const rot = sym.rotation || 0; const mirrored = sym.mirrored || false; const cx = sym.x + sym.w / 2; const cy = sym.y + sym.h / 2; const label = sym.label || sym.name; const idAttr = `id="${label}" inkscape:label="${label}"`; // Build outer transform (rotation + mirror) const outerParts: string[] = []; if (rot) outerParts.push(`rotate(${rot},${cx},${cy})`); if (mirrored) outerParts.push(`translate(${cx},0) scale(-1,1) translate(${-cx},0)`); const outerTransform = outerParts.join(' '); if (isEpcType(sym.symbolId)) { await emitEpc(lines, sym as PlacedSymbol, label, outerTransform); } else if (isInductionType(sym.symbolId)) { const hw = INDUCTION_CONFIG.headWidth; const stripTopY = sym.y + sym.h * INDUCTION_CONFIG.stripTopFrac; const stripBottomY = sym.y + sym.h * INDUCTION_CONFIG.stripBottomFrac; 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(` `); } 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); const sweepRad = (angle * Math.PI) / 180; const outerEndX = arcCx + outerR * Math.cos(sweepRad); const outerEndY = arcCy - outerR * Math.sin(sweepRad); const innerEndX = arcCx + innerR * Math.cos(sweepRad); const innerEndY = arcCy - innerR * Math.sin(sweepRad); const largeArc = angle > 180 ? 1 : 0; const d = [ `M ${arcCx + outerR},${arcCy}`, `A ${outerR},${outerR} 0 ${largeArc},0 ${outerEndX},${outerEndY}`, `L ${innerEndX},${innerEndY}`, `A ${innerR},${innerR} 0 ${largeArc},1 ${arcCx + innerR},${arcCy}`, 'Z', ].join(' '); 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(` `); } else { // Regular SVG symbol try { const svgText = await (await fetch(sym.file)).text(); const doc = new DOMParser().parseFromString(svgText, 'image/svg+xml'); const svgEl = doc.documentElement; if (svgEl.querySelector('parsererror')) { console.error('SVG parse error for:', sym.file); continue; } const vb = svgEl.getAttribute('viewBox'); const [vbX, vbY, vbW, vbH] = vb ? vb.split(/[\s,]+/).map(Number) : [0, 0, sym.w, sym.h]; const sx = sym.w / vbW; const sy = sym.h / vbH; // Base positioning transform let baseTransform = `translate(${sym.x},${sym.y}) scale(${sx.toFixed(6)},${sy.toFixed(6)}) translate(${-vbX},${-vbY})`; if (mirrored) baseTransform = `translate(${cx},0) scale(-1,1) translate(${-cx},0) ${baseTransform}`; if (rot) baseTransform = `rotate(${rot},${cx},${cy}) ${baseTransform}`; const children = Array.from(svgEl.children); if (children.length === 1 && children[0].tagName === 'g') { // SVG has a wrapper — keep it, put id/label on it, compose transforms const g = children[0]; const gTransform = g.getAttribute('transform') || ''; const fullTransform = gTransform ? `${baseTransform} ${gTransform}` : baseTransform; const innerContent = serializeChildren(g); lines.push(` `); lines.push(` ${innerContent}`); lines.push(' '); } else if (children.length === 1) { // Single element, no group — put id/label/transform directly on it const el = children[0].cloneNode(true) as Element; const elTransform = el.getAttribute('transform'); el.setAttribute('transform', elTransform ? `${baseTransform} ${elTransform}` : baseTransform); el.setAttribute('id', label); let s = new XMLSerializer().serializeToString(el) .replace(/ xmlns="http:\/\/www\.w3\.org\/2000\/svg"/g, ''); // Inject inkscape:label after the tag name const firstSpace = s.indexOf(' '); s = s.slice(0, firstSpace) + ` inkscape:label="${label}"` + s.slice(firstSpace); lines.push(` ${s}`); } else { // Multiple children without a group — wrap in with id/label const innerContent = serializeChildren(svgEl); lines.push(` `); lines.push(` ${innerContent}`); lines.push(' '); } } catch (err) { console.error('Failed to embed symbol:', sym.name, err); } } } lines.push(''); downloadBlob(new Blob([lines.join('\n')], { type: 'image/svg+xml' }), 'test_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); }); }