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">
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 { discoverProjects } from '$lib/projects.js';
import { onMount } from 'svelte';
@ -12,6 +12,7 @@
let pdfOpen = $state(false);
let settingsOpen = $state(false);
let ignitionOpen = $state(false);
onMount(async () => {
const projects = await discoverProjects();
@ -197,6 +198,27 @@
</div>
{/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>
<!-- Symbols -->
@ -435,7 +457,8 @@
min-width: 14px;
}
.inline-row input[type="number"] {
.inline-row input[type="number"],
.inline-row input[type="text"] {
flex: 1;
padding: 3px 4px;
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';
}
/** Export as Ignition SCADA JSON (ia.shapes.svg format) */
export async function exportIgnitionJSON() {
/** Build Ignition ia.shapes.svg component data */
async function buildIgnitionComponent(): Promise<Record<string, any>> {
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 = [{
return {
type: 'ia.shapes.svg',
version: 0,
props: {
@ -532,14 +531,70 @@ export async function exportIgnitionJSON() {
meta: { name: `${mcmName}_Detailed_View` },
position: { width: 1, height: 1 },
custom: {},
}];
};
}
/** Export as Ignition SCADA JSON file (download) */
export async function exportIgnitionJSON() {
const component = await buildIgnitionComponent();
const mcmName = layout.currentMcm || 'export';
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`
);
}
/** 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 */
async function emitEpc(lines: string[], sym: PlacedSymbol, label: string, outerTransform: string) {
const waypoints = sym.epcWaypoints || EPC_CONFIG.defaultWaypoints;

View File

@ -25,6 +25,10 @@ class LayoutStore {
currentProject = $state<string>('');
currentMcm = $state<string>('');
// Ignition export settings
ignitionProject = $state<string>('Testing_Project');
ignitionViewName = $state<string>('');
// PDF state
pdfScale = $state(1.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(),
],
});