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:
parent
2c38950cb7
commit
a0ceb56309
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(),
|
||||
],
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user