Add Ignition deploy: write view.json directly to Ignition project dir

- New "Ignition" section in toolbar with Project and View name fields
- View name defaults to current MCM
- "Deploy to Ignition" button writes view.json + resource.json to:
  C:/Program Files/Inductive Automation/Ignition/data/projects/{Project}/
  com.inductiveautomation.perspective/views/{ViewName}/
- Vite dev server plugin handles file writing via /api/deploy-ignition
- view.json wraps ia.shapes.svg component in proper Perspective view structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
igurielidze 2026-03-30 22:44:11 +04:00
parent 2c38950cb7
commit a0ceb56309
4 changed files with 115 additions and 8 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, exportJSON, exportIgnitionJSON, loadLayoutJSON, loadLayoutSVG } from '$lib/export.js'; import { exportSVG, exportJSON, exportIgnitionJSON, deployToIgnition, 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';
@ -12,6 +12,7 @@
let pdfOpen = $state(false); let pdfOpen = $state(false);
let settingsOpen = $state(false); let settingsOpen = $state(false);
let ignitionOpen = $state(false);
onMount(async () => { onMount(async () => {
const projects = await discoverProjects(); const projects = await discoverProjects();
@ -197,6 +198,27 @@
</div> </div>
{/if} {/if}
<!-- Ignition Deploy -->
<button class="section-toggle" onclick={() => ignitionOpen = !ignitionOpen}>
<span class="chevron" class:open={ignitionOpen}></span>
Ignition
</button>
{#if ignitionOpen}
<div class="section-body">
<div class="inline-row">
<label>Project</label>
<input type="text" bind:value={layout.ignitionProject} placeholder="Testing_Project">
</div>
<div class="inline-row">
<label>View</label>
<input type="text" bind:value={layout.ignitionViewName} placeholder={layout.currentMcm || 'MCM01'}>
</div>
<div class="btn-row">
<button onclick={deployToIgnition}>Deploy to Ignition</button>
</div>
</div>
{/if}
<div class="divider"></div> <div class="divider"></div>
<!-- Symbols --> <!-- Symbols -->
@ -435,7 +457,8 @@
min-width: 14px; min-width: 14px;
} }
.inline-row input[type="number"] { .inline-row input[type="number"],
.inline-row input[type="text"] {
flex: 1; flex: 1;
padding: 3px 4px; padding: 3px 4px;
background: #1f2937; background: #1f2937;

View File

@ -507,22 +507,21 @@ function addFillStroke(obj: Record<string, any>, el: Element) {
if (obj.stroke?.paint === 'none') obj.stroke.paint = 'transparent'; if (obj.stroke?.paint === 'none') obj.stroke.paint = 'transparent';
} }
/** Export as Ignition SCADA JSON (ia.shapes.svg format) */ /** Build Ignition ia.shapes.svg component data */
export async function exportIgnitionJSON() { async function buildIgnitionComponent(): Promise<Record<string, any>> {
const svgStr = await buildSvgString(); const svgStr = await buildSvgString();
const doc = new DOMParser().parseFromString(svgStr, 'image/svg+xml'); const doc = new DOMParser().parseFromString(svgStr, 'image/svg+xml');
const svgEl = doc.documentElement; const svgEl = doc.documentElement;
const elements: Record<string, any>[] = []; const elements: Record<string, any>[] = [];
for (const child of Array.from(svgEl.children)) { for (const child of Array.from(svgEl.children)) {
// Skip comments
if (child.nodeType === 8) continue; if (child.nodeType === 8) continue;
const converted = svgElementToIgnition(child); const converted = svgElementToIgnition(child);
if (converted) elements.push(converted); if (converted) elements.push(converted);
} }
const mcmName = layout.currentMcm || 'export'; const mcmName = layout.currentMcm || 'export';
const ignitionData = [{ return {
type: 'ia.shapes.svg', type: 'ia.shapes.svg',
version: 0, version: 0,
props: { props: {
@ -532,14 +531,70 @@ export async function exportIgnitionJSON() {
meta: { name: `${mcmName}_Detailed_View` }, meta: { name: `${mcmName}_Detailed_View` },
position: { width: 1, height: 1 }, position: { width: 1, height: 1 },
custom: {}, custom: {},
}]; };
}
/** Export as Ignition SCADA JSON file (download) */
export async function exportIgnitionJSON() {
const component = await buildIgnitionComponent();
const mcmName = layout.currentMcm || 'export';
downloadBlob( downloadBlob(
new Blob([JSON.stringify(ignitionData, null, 2)], { type: 'application/json' }), new Blob([JSON.stringify([component], null, 2)], { type: 'application/json' }),
`${mcmName}_Detailed_View.json` `${mcmName}_Detailed_View.json`
); );
} }
/** Deploy directly to Ignition project directory */
export async function deployToIgnition() {
const component = await buildIgnitionComponent();
const projectName = layout.ignitionProject || 'Testing_Project';
const viewName = layout.ignitionViewName || layout.currentMcm || 'MCM01';
const viewJson = JSON.stringify({
custom: {},
params: {},
props: {
defaultSize: { height: layout.canvasH, width: layout.canvasW },
},
root: {
children: [component],
meta: { name: 'root' },
props: { direction: 'column' },
type: 'ia.container.coord',
},
}, null, 2);
const resourceJson = JSON.stringify({
scope: 'G',
version: 1,
restricted: false,
overridable: true,
files: ['view.json'],
attributes: {
lastModification: {
actor: 'scada-layout-tool',
timestamp: new Date().toISOString(),
},
},
}, null, 2);
try {
const resp = await fetch('/api/deploy-ignition', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ projectName, viewName, viewJson, resourceJson }),
});
const result = await resp.json();
if (result.ok) {
alert(`Deployed to Ignition!\n${result.path}`);
} else {
alert(`Deploy failed: ${result.error}`);
}
} catch (err) {
alert(`Deploy failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
/** Emit EPC symbol — polyline + icon + right box, wrapped in <g> with id/label */ /** Emit EPC symbol — polyline + icon + right box, wrapped in <g> with id/label */
async function emitEpc(lines: string[], sym: PlacedSymbol, label: string, outerTransform: string) { async function emitEpc(lines: string[], sym: PlacedSymbol, label: string, outerTransform: string) {
const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints; const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints;

View File

@ -25,6 +25,10 @@ class LayoutStore {
currentProject = $state<string>(''); currentProject = $state<string>('');
currentMcm = $state<string>(''); currentMcm = $state<string>('');
// Ignition export settings
ignitionProject = $state<string>('Testing_Project');
ignitionViewName = $state<string>('');
// PDF state // PDF state
pdfScale = $state(1.0); pdfScale = $state(1.0);
pdfOffsetX = $state(0); pdfOffsetX = $state(0);

View File

@ -254,6 +254,31 @@ export default defineConfig({
}); });
}, },
}, },
{
name: 'ignition-deploy',
configureServer(server) {
server.middlewares.use('/api/deploy-ignition', async (req, res) => {
if (req.method !== 'POST') { res.statusCode = 405; res.end('Method not allowed'); return; }
let body = '';
req.on('data', (chunk: Buffer) => { body += chunk.toString(); });
req.on('end', () => {
try {
const { projectName, viewName, viewJson, resourceJson } = JSON.parse(body);
const ignitionBase = 'C:/Program Files/Inductive Automation/Ignition/data/projects';
const viewDir = path.join(ignitionBase, projectName, 'com.inductiveautomation.perspective/views', viewName);
fs.mkdirSync(viewDir, { recursive: true });
fs.writeFileSync(path.join(viewDir, 'view.json'), viewJson);
fs.writeFileSync(path.join(viewDir, 'resource.json'), resourceJson);
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ ok: true, path: viewDir }));
} catch (err: any) {
res.statusCode = 500;
res.end(JSON.stringify({ error: err.message }));
}
});
});
},
},
sveltekit(), sveltekit(),
], ],
}); });