Add SVG import, JSON export, and embed layout data in SVG export

- SVG export now embeds layout JSON as HTML comment for re-import
- New loadLayoutSVG() extracts embedded data from exported SVGs
- Import accepts both .json and .svg files
- New exportJSON() saves layout as MCM_layout.json
- JSON export button added to toolbar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-30 20:45:05 +04:00
parent 37f3700a18
commit 1e67c3de47
2 changed files with 61 additions and 4 deletions

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { layout } from '$lib/stores/layout.svelte.js'; import { layout } from '$lib/stores/layout.svelte.js';
import { exportSVG, loadLayoutJSON } from '$lib/export.js'; import { exportSVG, exportJSON, loadLayoutJSON, loadLayoutSVG } from '$lib/export.js';
import { loadPdfFile, loadPdfFromPath, pdfZoomIn, pdfZoomOut, removePdf, toggleEditBackground, restorePdf } from '$lib/pdf.js'; import { loadPdfFile, loadPdfFromPath, pdfZoomIn, pdfZoomOut, removePdf, toggleEditBackground, restorePdf } from '$lib/pdf.js';
import { discoverProjects } from '$lib/projects.js'; import { discoverProjects } from '$lib/projects.js';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
@ -93,7 +93,11 @@
const file = (e.target as HTMLInputElement).files?.[0]; const file = (e.target as HTMLInputElement).files?.[0];
if (!file) return; if (!file) return;
try { try {
await loadLayoutJSON(file); if (file.name.endsWith('.svg')) {
await loadLayoutSVG(file);
} else {
await loadLayoutJSON(file);
}
} catch (err) { } catch (err) {
alert('Invalid layout file: ' + (err instanceof Error ? err.message : String(err))); alert('Invalid layout file: ' + (err instanceof Error ? err.message : String(err)));
} }
@ -239,9 +243,10 @@
</div> </div>
<div class="setting btn-row"> <div class="setting btn-row">
<button onclick={exportSVG}>Save SVG</button> <button onclick={exportSVG}>Save SVG</button>
<button onclick={exportJSON}>Save JSON</button>
<button onclick={() => importFileEl.click()}>Load JSON</button> <button onclick={() => importFileEl.click()}>Load JSON</button>
<button onclick={clearCanvas}>Clear</button> <button onclick={clearCanvas}>Clear</button>
<input bind:this={importFileEl} type="file" accept=".json" style="display:none" onchange={onImportFile}> <input bind:this={importFileEl} type="file" accept=".json,.svg" style="display:none" onchange={onImportFile}>
</div> </div>
</div> </div>
{/if} {/if}

View File

@ -1,6 +1,6 @@
import { layout } from './stores/layout.svelte.js'; 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, getCurveGeometry } from './symbols.js';
import { deserializeSymbol } from './serialization.js'; import { serializeSymbol, deserializeSymbol } from './serialization.js';
import type { PlacedSymbol } from './types.js'; import type { PlacedSymbol } from './types.js';
/** Parse conveyance label into display lines — same logic as renderer */ /** Parse conveyance label into display lines — same logic as renderer */
@ -70,6 +70,18 @@ function downloadBlob(blob: Blob, filename: string) {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
export function exportJSON() {
const data = {
symbols: layout.symbols.map(s => serializeSymbol(s)),
gridSize: layout.gridSize,
minSpacing: layout.minSpacing,
canvasW: layout.canvasW,
canvasH: layout.canvasH,
};
const mcmName = layout.currentMcm || 'export';
downloadBlob(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }), `${mcmName}_layout.json`);
}
/** Serialize child elements of an SVG, stripping xmlns added by XMLSerializer */ /** Serialize child elements of an SVG, stripping xmlns added by XMLSerializer */
function serializeChildren(parent: Element): string { function serializeChildren(parent: Element): string {
return Array.from(parent.children) return Array.from(parent.children)
@ -247,6 +259,16 @@ export async function exportSVG() {
} }
} }
// Embed layout data as metadata for re-import
const layoutData = {
symbols: layout.symbols.filter(s => !s.hidden && !layout.hiddenGroups.has(getSymbolGroup(s.symbolId))).map(s => serializeSymbol(s)),
gridSize: layout.gridSize,
minSpacing: layout.minSpacing,
canvasW: layout.canvasW,
canvasH: layout.canvasH,
};
lines.push(` <!-- LAYOUT_DATA:${JSON.stringify(layoutData)}:END_LAYOUT_DATA -->`);
lines.push('</svg>'); lines.push('</svg>');
const mcmName = layout.currentMcm || 'export'; const mcmName = layout.currentMcm || 'export';
downloadBlob(new Blob([lines.join('\n')], { type: 'image/svg+xml' }), `${mcmName}_Detailed_View.svg`); downloadBlob(new Blob([lines.join('\n')], { type: 'image/svg+xml' }), `${mcmName}_Detailed_View.svg`);
@ -315,6 +337,36 @@ async function emitEpc(lines: string[], sym: PlacedSymbol, label: string, outerT
lines.push(' </g>'); lines.push(' </g>');
} }
export function loadLayoutSVG(file: File): Promise<void> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (ev) => {
try {
const svgText = ev.target!.result as string;
const match = svgText.match(/<!-- LAYOUT_DATA:(.*?):END_LAYOUT_DATA -->/);
if (!match) throw new Error('No layout data found in SVG. Only SVGs exported from this tool can be imported.');
const data = JSON.parse(match[1]);
layout.pushUndo();
if (data.gridSize) layout.gridSize = data.gridSize;
if (data.minSpacing) layout.minSpacing = data.minSpacing;
if (data.canvasW) layout.canvasW = data.canvasW;
if (data.canvasH) layout.canvasH = data.canvasH;
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);
});
}
export function loadLayoutJSON(file: File): Promise<void> { export function loadLayoutJSON(file: File): Promise<void> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();