Add Ignition SCADA JSON export (ia.shapes.svg format)
New "SCADA" button generates Ignition-compatible JSON that can be
directly pasted into Ignition Perspective views. Converts SVG elements
to Ignition's ia.shapes.svg JSON format with:
- Proper element types (group, rect, path, text, polyline, circle)
- Fill/stroke as {paint, width} objects
- Text style as {fontFamily, fontWeight, fontSize} objects
- color, state, priority, tagpaths as top-level element properties
- Correct viewBox, meta, position structure
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
48bb43f471
commit
2c38950cb7
@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { layout } from '$lib/stores/layout.svelte.js';
|
||||
import { exportSVG, exportJSON, loadLayoutJSON, loadLayoutSVG } from '$lib/export.js';
|
||||
import { exportSVG, exportJSON, exportIgnitionJSON, loadLayoutJSON, loadLayoutSVG } from '$lib/export.js';
|
||||
import { loadPdfFile, loadPdfFromPath, pdfZoomIn, pdfZoomOut, removePdf, toggleEditBackground, restorePdf } from '$lib/pdf.js';
|
||||
import { discoverProjects } from '$lib/projects.js';
|
||||
import { onMount } from 'svelte';
|
||||
@ -125,7 +125,8 @@
|
||||
<!-- Action bar — always visible -->
|
||||
<div class="action-bar">
|
||||
<button onclick={exportSVG} title="Export SVG">SVG</button>
|
||||
<button onclick={exportJSON} title="Export JSON">JSON</button>
|
||||
<button onclick={exportJSON} title="Export layout JSON">JSON</button>
|
||||
<button onclick={exportIgnitionJSON} title="Export Ignition SCADA JSON">SCADA</button>
|
||||
<button onclick={() => importFileEl.click()} title="Import layout file">Import</button>
|
||||
<button class="btn-danger" onclick={clearCanvas} title="Clear canvas">Clear</button>
|
||||
<input bind:this={importFileEl} type="file" accept=".json,.svg" style="display:none" onchange={onImportFile}>
|
||||
|
||||
@ -164,7 +164,7 @@ function serializeChildren(parent: Element): string {
|
||||
.join('\n ');
|
||||
}
|
||||
|
||||
export async function exportSVG() {
|
||||
async function buildSvgString(): Promise<string> {
|
||||
const lines: string[] = [
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
|
||||
`<svg width="${layout.canvasW}" height="${layout.canvasH}" viewBox="0 0 ${layout.canvasW} ${layout.canvasH}" version="1.1"`,
|
||||
@ -355,8 +355,189 @@ export async function exportSVG() {
|
||||
lines.push(` <!-- LAYOUT_DATA:${JSON.stringify(layoutData)}:END_LAYOUT_DATA -->`);
|
||||
|
||||
lines.push('</svg>');
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export async function exportSVG() {
|
||||
const svgStr = await buildSvgString();
|
||||
const mcmName = layout.currentMcm || 'export';
|
||||
downloadBlob(new Blob([lines.join('\n')], { type: 'image/svg+xml' }), `${mcmName}_Detailed_View.svg`);
|
||||
downloadBlob(new Blob([svgStr], { type: 'image/svg+xml' }), `${mcmName}_Detailed_View.svg`);
|
||||
}
|
||||
|
||||
/** Convert SVG element to Ignition JSON format */
|
||||
function svgElementToIgnition(el: Element): Record<string, any> | null {
|
||||
const tag = el.tagName.toLowerCase();
|
||||
if (tag === 'defs' || tag === 'sodipodi:namedview') return null;
|
||||
|
||||
const obj: Record<string, any> = {};
|
||||
|
||||
if (tag === 'g') {
|
||||
obj.type = 'group';
|
||||
obj.name = el.getAttribute('id') || el.getAttribute('inkscape:label') || 'group';
|
||||
if (el.getAttribute('id')) obj.id = el.getAttribute('id');
|
||||
if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform');
|
||||
|
||||
// Convert children
|
||||
const children: Record<string, any>[] = [];
|
||||
for (const child of Array.from(el.children)) {
|
||||
const c = svgElementToIgnition(child);
|
||||
if (c) children.push(c);
|
||||
}
|
||||
if (children.length) obj.elements = children;
|
||||
|
||||
// Ignition metadata from data-* attributes
|
||||
if (el.getAttribute('data-color')) obj.color = el.getAttribute('data-color');
|
||||
if (el.getAttribute('data-state')) obj.state = el.getAttribute('data-state');
|
||||
if (el.getAttribute('data-priority')) obj.priority = el.getAttribute('data-priority');
|
||||
if (el.getAttribute('data-tagpath')) obj.tagpaths = [el.getAttribute('data-tagpath')];
|
||||
} else if (tag === 'rect') {
|
||||
obj.type = 'rect';
|
||||
obj.name = el.getAttribute('id') || 'rect';
|
||||
if (el.getAttribute('id')) obj.id = el.getAttribute('id');
|
||||
for (const attr of ['x', 'y', 'width', 'height', 'rx', 'ry']) {
|
||||
if (el.getAttribute(attr)) obj[attr] = el.getAttribute(attr);
|
||||
}
|
||||
if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform');
|
||||
addFillStroke(obj, el);
|
||||
// Ignition metadata
|
||||
if (el.getAttribute('data-color')) obj.color = el.getAttribute('data-color');
|
||||
if (el.getAttribute('data-state')) obj.state = el.getAttribute('data-state');
|
||||
if (el.getAttribute('data-priority')) obj.priority = el.getAttribute('data-priority');
|
||||
if (el.getAttribute('data-tagpath')) obj.tagpaths = [el.getAttribute('data-tagpath')];
|
||||
} else if (tag === 'path') {
|
||||
obj.type = 'path';
|
||||
obj.name = el.getAttribute('id') || 'path';
|
||||
if (el.getAttribute('id')) obj.id = el.getAttribute('id');
|
||||
obj.d = el.getAttribute('d') || '';
|
||||
if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform');
|
||||
addFillStroke(obj, el);
|
||||
if (el.getAttribute('data-color')) obj.color = el.getAttribute('data-color');
|
||||
if (el.getAttribute('data-state')) obj.state = el.getAttribute('data-state');
|
||||
if (el.getAttribute('data-priority')) obj.priority = el.getAttribute('data-priority');
|
||||
if (el.getAttribute('data-tagpath')) obj.tagpaths = [el.getAttribute('data-tagpath')];
|
||||
} else if (tag === 'text') {
|
||||
obj.type = 'text';
|
||||
obj.name = el.getAttribute('id') || 'text';
|
||||
obj.text = el.textContent || '';
|
||||
for (const attr of ['x', 'y']) {
|
||||
if (el.getAttribute(attr)) obj[attr] = el.getAttribute(attr);
|
||||
}
|
||||
if (el.getAttribute('text-anchor')) obj.textAnchor = el.getAttribute('text-anchor');
|
||||
if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform');
|
||||
// Parse inline style to Ignition style object
|
||||
const styleStr = el.getAttribute('style');
|
||||
if (styleStr) {
|
||||
const styleObj: Record<string, string> = {};
|
||||
let fillColor = '#000000';
|
||||
for (const part of styleStr.split(';')) {
|
||||
const [k, v] = part.split(':').map(s => s.trim());
|
||||
if (k === 'font-family') styleObj.fontFamily = v;
|
||||
else if (k === 'font-weight') styleObj.fontWeight = v;
|
||||
else if (k === 'font-size') styleObj.fontSize = v;
|
||||
else if (k === 'fill') fillColor = v;
|
||||
}
|
||||
if (Object.keys(styleObj).length) obj.style = styleObj;
|
||||
obj.fill = { paint: fillColor };
|
||||
} else {
|
||||
if (el.getAttribute('fill')) obj.fill = { paint: el.getAttribute('fill') };
|
||||
if (el.getAttribute('font-size')) obj.fontSize = el.getAttribute('font-size');
|
||||
}
|
||||
} else if (tag === 'polyline') {
|
||||
obj.type = 'polyline';
|
||||
obj.name = el.getAttribute('id') || 'polyline';
|
||||
if (el.getAttribute('points')) obj.points = el.getAttribute('points');
|
||||
addFillStroke(obj, el);
|
||||
if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform');
|
||||
} else if (tag === 'circle') {
|
||||
obj.type = 'circle';
|
||||
obj.name = el.getAttribute('id') || 'circle';
|
||||
for (const attr of ['cx', 'cy', 'r']) {
|
||||
if (el.getAttribute(attr)) obj[attr] = el.getAttribute(attr);
|
||||
}
|
||||
addFillStroke(obj, el);
|
||||
if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform');
|
||||
} else if (tag === 'polygon') {
|
||||
obj.type = 'polygon';
|
||||
obj.name = el.getAttribute('id') || 'polygon';
|
||||
if (el.getAttribute('points')) obj.points = el.getAttribute('points');
|
||||
addFillStroke(obj, el);
|
||||
if (el.getAttribute('transform')) obj.transform = el.getAttribute('transform');
|
||||
} else {
|
||||
return null; // skip unknown elements
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/** Extract fill/stroke from SVG element (handles both attributes and style) */
|
||||
function addFillStroke(obj: Record<string, any>, el: Element) {
|
||||
const styleStr = el.getAttribute('style');
|
||||
if (styleStr) {
|
||||
const styles: Record<string, string> = {};
|
||||
for (const part of styleStr.split(';')) {
|
||||
const [k, v] = part.split(':').map(s => s.trim());
|
||||
if (k && v) styles[k] = v;
|
||||
}
|
||||
const fill: Record<string, string> = {};
|
||||
if (styles.fill) fill.paint = styles.fill;
|
||||
else if (el.getAttribute('fill')) fill.paint = el.getAttribute('fill')!;
|
||||
if (styles['fill-opacity']) fill.opacity = styles['fill-opacity'];
|
||||
if (fill.paint) obj.fill = fill;
|
||||
|
||||
const stroke: Record<string, string> = {};
|
||||
if (styles.stroke) stroke.paint = styles.stroke;
|
||||
if (styles['stroke-width']) stroke.width = styles['stroke-width'];
|
||||
if (styles['stroke-dasharray']) stroke.dasharray = styles['stroke-dasharray'];
|
||||
if (styles['stroke-opacity']) stroke.opacity = styles['stroke-opacity'];
|
||||
if (stroke.paint) obj.stroke = stroke;
|
||||
} else {
|
||||
if (el.getAttribute('fill')) {
|
||||
const fill: Record<string, string> = { paint: el.getAttribute('fill')! };
|
||||
if (el.getAttribute('fill-opacity')) fill.opacity = el.getAttribute('fill-opacity')!;
|
||||
obj.fill = fill;
|
||||
}
|
||||
if (el.getAttribute('stroke')) {
|
||||
const stroke: Record<string, string> = { paint: el.getAttribute('stroke')! };
|
||||
if (el.getAttribute('stroke-width')) stroke.width = el.getAttribute('stroke-width')!;
|
||||
obj.stroke = stroke;
|
||||
}
|
||||
}
|
||||
// fill="none" → paint: "transparent"
|
||||
if (obj.fill?.paint === 'none') obj.fill.paint = 'transparent';
|
||||
if (obj.stroke?.paint === 'none') obj.stroke.paint = 'transparent';
|
||||
}
|
||||
|
||||
/** Export as Ignition SCADA JSON (ia.shapes.svg format) */
|
||||
export async function exportIgnitionJSON() {
|
||||
const svgStr = await buildSvgString();
|
||||
const doc = new DOMParser().parseFromString(svgStr, 'image/svg+xml');
|
||||
const svgEl = doc.documentElement;
|
||||
|
||||
const elements: Record<string, any>[] = [];
|
||||
for (const child of Array.from(svgEl.children)) {
|
||||
// Skip comments
|
||||
if (child.nodeType === 8) continue;
|
||||
const converted = svgElementToIgnition(child);
|
||||
if (converted) elements.push(converted);
|
||||
}
|
||||
|
||||
const mcmName = layout.currentMcm || 'export';
|
||||
const ignitionData = [{
|
||||
type: 'ia.shapes.svg',
|
||||
version: 0,
|
||||
props: {
|
||||
viewBox: svgEl.getAttribute('viewBox') || `0 0 ${layout.canvasW} ${layout.canvasH}`,
|
||||
elements,
|
||||
},
|
||||
meta: { name: `${mcmName}_Detailed_View` },
|
||||
position: { width: 1, height: 1 },
|
||||
custom: {},
|
||||
}];
|
||||
|
||||
downloadBlob(
|
||||
new Blob([JSON.stringify(ignitionData, null, 2)], { type: 'application/json' }),
|
||||
`${mcmName}_Detailed_View.json`
|
||||
);
|
||||
}
|
||||
|
||||
/** Emit EPC symbol — polyline + icon + right box, wrapped in <g> with id/label */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user