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:
parent
37f3700a18
commit
1e67c3de47
@ -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}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user