|
1 |
| -<!-- src/components/layouts/MainLayout.vue --> |
2 | 1 | <script setup lang="ts">
|
3 | 2 | import { ref, computed, onUnmounted, watch, onMounted, nextTick } from 'vue';
|
4 | 3 | import type { StyleValue } from 'vue';
|
@@ -123,11 +122,13 @@ onMounted(async () => {
|
123 | 122 |
|
124 | 123 | watch(
|
125 | 124 | () => graphStore.currentGraphId,
|
126 |
| - (newId, oldId) => { |
127 |
| - if (newId && newId !== oldId) { |
| 125 | + (newId) => { |
| 126 | + if (newId) { |
128 | 127 | nextTick(() => {
|
129 | 128 | setTimeout(() => {
|
130 |
| - handleGraphLayout('dagre'); |
| 129 | + const graphContent = graphStore.graphContents.get(newId); |
| 130 | + const layoutToApply = graphContent?.lastLayout || 'dagre'; |
| 131 | + handleGraphLayout(layoutToApply); |
131 | 132 | }, 100);
|
132 | 133 | });
|
133 | 134 | }
|
@@ -242,20 +243,50 @@ const handleDeleteElement = (elementId: string) => {
|
242 | 243 | deleteElement(elementId);
|
243 | 244 | };
|
244 | 245 |
|
| 246 | +const handleLayoutUpdated = (layoutName: string) => { |
| 247 | + if (graphStore.currentGraphId) { |
| 248 | + graphStore.updateGraphLayout(graphStore.currentGraphId, layoutName); |
| 249 | + } |
| 250 | +}; |
| 251 | +
|
245 | 252 | const handleGraphLayout = (layoutName: string) => {
|
246 |
| - const cy = getCyInstance(); |
247 |
| - if (!cy) return; |
248 |
| - /* eslint-disable @typescript-eslint/no-explicit-any */ |
249 |
| - const layoutOptionsMap: Record<string, LayoutOptions> = { |
250 |
| - dagre: { name: 'dagre', animate: true, animationDuration: 500, fit: true, padding: 30 } as any, |
251 |
| - fcose: { name: 'fcose', animate: true, animationDuration: 500, fit: true, padding: 30, randomize: false, quality: 'proof' } as any, |
252 |
| - cola: { name: 'cola', animate: true, fit: true, padding: 30, refresh: 1, avoidOverlap: true, infinite: false, centerGraph: true, flow: { axis: 'y', minSeparation: 30 }, handleDisconnected: false, randomize: false } as any, |
253 |
| - klay: { name: 'klay', animate: true, animationDuration: 500, fit: true, padding: 30, klay: { direction: 'RIGHT', edgeRouting: 'SPLINES', nodePlacement: 'LINEAR_SEGMENTS' } } as any, |
254 |
| - preset: { name: 'preset' } |
255 |
| - }; |
256 |
| - /* eslint-enable @typescript-eslint/no-explicit-any */ |
257 |
| - const options = layoutOptionsMap[layoutName] || layoutOptionsMap.preset; |
258 |
| - cy.layout(options).run(); |
| 253 | + const cy = getCyInstance(); |
| 254 | + if (!cy) return; |
| 255 | +
|
| 256 | + const shouldUpdatePositions = layoutName !== 'preset'; |
| 257 | +
|
| 258 | + /* eslint-disable @typescript-eslint/no-explicit-any */ |
| 259 | + const layoutOptionsMap: Record<string, LayoutOptions> = { |
| 260 | + dagre: { name: 'dagre', animate: true, animationDuration: 500, fit: true, padding: 30 } as any, |
| 261 | + fcose: { name: 'fcose', animate: true, animationDuration: 500, fit: true, padding: 30, randomize: false, quality: 'proof' } as any, |
| 262 | + cola: { name: 'cola', animate: true, fit: true, padding: 30, refresh: 1, avoidOverlap: true, infinite: false, centerGraph: true, flow: { axis: 'y', minSeparation: 30 }, handleDisconnected: false, randomize: false } as any, |
| 263 | + klay: { name: 'klay', animate: true, animationDuration: 500, fit: true, padding: 30, klay: { direction: 'RIGHT', edgeRouting: 'SPLINES', nodePlacement: 'LINEAR_SEGMENTS' } } as any, |
| 264 | + preset: { name: 'preset' } |
| 265 | + }; |
| 266 | + /* eslint-enable @typescript-eslint/no-explicit-any */ |
| 267 | + const options = layoutOptionsMap[layoutName] || layoutOptionsMap.preset; |
| 268 | + const layout = cy.layout(options); |
| 269 | +
|
| 270 | + if (shouldUpdatePositions) { |
| 271 | + layout.one('layoutstop', () => { |
| 272 | + const updatedElements = graphStore.currentGraphElements.map(el => { |
| 273 | + if (el.type === 'node') { |
| 274 | + const cyNode = cy.getElementById(el.id); |
| 275 | + if (cyNode.length > 0) { |
| 276 | + const newPos = cyNode.position(); |
| 277 | + return { ...el, position: { x: newPos.x, y: newPos.y } }; |
| 278 | + } |
| 279 | + } |
| 280 | + return el; |
| 281 | + }); |
| 282 | + if (graphStore.currentGraphId) { |
| 283 | + graphStore.updateGraphElements(graphStore.currentGraphId, updatedElements); |
| 284 | + } |
| 285 | + }); |
| 286 | + } |
| 287 | +
|
| 288 | + layout.run(); |
| 289 | + handleLayoutUpdated(layoutName); |
259 | 290 | };
|
260 | 291 |
|
261 | 292 | const handlePaletteSelection = (itemType: PaletteItemType) => {
|
@@ -524,14 +555,15 @@ const runModel = async () => {
|
524 | 555 | }
|
525 | 556 | });
|
526 | 557 | const frontendStandaloneFile: GeneratedFile = { name: 'standalone.jl', content: frontendStandaloneScript };
|
527 |
| - const backendFiles = (result.files ?? []) |
528 |
| - .filter(file => file.name !== 'standalone.jl') |
529 |
| - .map(file => ({ |
530 |
| - name: file.name, |
531 |
| - content: typeof (file as any).content === 'string' |
532 |
| - ? (file as any).content |
533 |
| - : ((file as any).content == null ? '' : JSON.stringify((file as any).content)), |
534 |
| - })); |
| 558 | + const backendFiles = (result.files ?? []).filter(file => file.name !== 'standalone.jl').map(file => { |
| 559 | + const content = (file as GeneratedFile & { content: unknown }).content; |
| 560 | + return { |
| 561 | + name: file.name, |
| 562 | + content: typeof content === 'string' |
| 563 | + ? content |
| 564 | + : (content == null ? '' : JSON.stringify(content)), |
| 565 | + }; |
| 566 | + }); |
535 | 567 | // Put standalone first so users see a guaranteed non-empty file
|
536 | 568 | executionStore.generatedFiles = [frontendStandaloneFile, ...backendFiles];
|
537 | 569 | // Debug log: show file sizes to help diagnose empty content
|
@@ -569,37 +601,6 @@ const runModel = async () => {
|
569 | 601 | }
|
570 | 602 | };
|
571 | 603 |
|
572 |
| -// Convert a Julia NamedTuple-like string to a JSON-friendly object (best-effort) |
573 |
| -function juliaTupleToJsonObject(juliaString: string): Record<string, unknown> { |
574 |
| - try { |
575 |
| - // Very conservative: if it's already JSON, return parsed |
576 |
| - try { return JSON.parse(juliaString); } catch { /* ignore */ } |
577 |
| - // Minimal parser for patterns like: (a = 1, b = [1,2], c = [ [..]; [..] ]) |
578 |
| - // We will transform into JSON by replacing Julia syntax tokens carefully. |
579 |
| - let s = juliaString.trim(); |
580 |
| - if (!s || s === '()') return {}; |
581 |
| - // Replace tuple parens with braces |
582 |
| - s = s.replace(/^\(/, '{').replace(/\)$/, '}'); |
583 |
| - // Replace "key =" with ""key":" |
584 |
| - s = s.replace(/(\w+)\s*=\s*/g, '"$1": '); |
585 |
| - // Replace Julia matrix [a b; c d] with array of arrays [[a,b],[c,d]] (best-effort) |
586 |
| - s = s.replace(/\[\s*([\s\S]*?)\s*\]/g, (match, content) => { |
587 |
| - // If it contains semicolons, treat as rows |
588 |
| - if (content.includes(';')) { |
589 |
| - const rows = content.split(';').map((row: string) => `[${row.trim().replace(/\s+/g, ', ')}]`); |
590 |
| - return `[${rows.join(',')}]`; |
591 |
| - } |
592 |
| - // Otherwise keep commas |
593 |
| - return `[${content.replace(/\s+/g, ' ')}]`; |
594 |
| - }); |
595 |
| - // Remove trailing commas if any |
596 |
| - s = s.replace(/,\s*}/g, '}'); |
597 |
| - return JSON.parse(s); |
598 |
| - } catch { |
599 |
| - return {}; |
600 |
| - } |
601 |
| -} |
602 |
| -
|
603 | 604 | const jsonToJulia = (jsonString: string): string => {
|
604 | 605 | try {
|
605 | 606 | const obj = JSON.parse(jsonString);
|
@@ -711,7 +712,7 @@ const handleGenerateStandalone = () => {
|
711 | 712 | <GraphEditor :is-grid-enabled="isGridEnabled" :grid-size="gridSize" :current-mode="currentMode"
|
712 | 713 | :elements="elements" :current-node-type="currentNodeType" :validation-errors="validationErrors"
|
713 | 714 | @update:current-mode="currentMode = $event" @update:current-node-type="currentNodeType = $event"
|
714 |
| - @element-selected="handleElementSelected" /> |
| 715 | + @element-selected="handleElementSelected" @layout-updated="handleLayoutUpdated" /> |
715 | 716 | </main>
|
716 | 717 |
|
717 | 718 | <div class="resizer resizer-right" @mousedown.prevent="startResizeRight"></div>
|
|
0 commit comments