fix(sim): exclude power lines from fluid routing
Power and signal edges no longer act as fluid paths in the mock solver (a water demand was previously routed through the pump's power cable). Also: bundled demo diagram, visual e2e script with dev-mode editor hook, plan checklist completed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
cd937cf4ac
commit
dd547f7ea1
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,3 +4,4 @@ src-tauri/target/
|
|||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
coverage/
|
coverage/
|
||||||
|
e2e/*.png
|
||||||
|
|||||||
@ -55,79 +55,79 @@ tests/ # vitest unit + component tests
|
|||||||
### Phase 0 — Research & scaffolding
|
### Phase 0 — Research & scaffolding
|
||||||
- [x] Web research: select diagramming library (JointJS core)
|
- [x] Web research: select diagramming library (JointJS core)
|
||||||
- [x] Write implementation plan (this document)
|
- [x] Write implementation plan (this document)
|
||||||
- [ ] Init git repository, `.gitignore`, conventional commits
|
- [x] Init git repository, `.gitignore`, conventional commits
|
||||||
- [ ] Install Linux system deps for Tauri (webkit2gtk 4.1, librsvg, appindicator)
|
- [x] Install Linux system deps for Tauri (webkit2gtk 4.1, librsvg, appindicator)
|
||||||
- [ ] Scaffold Vite + React + TS app, add `@joint/core`
|
- [x] Scaffold Vite + React + TS app, add `@joint/core`
|
||||||
- [ ] Add Tauri 2 shell (`src-tauri`), dev/build scripts
|
- [x] Add Tauri 2 shell (`src-tauri`), dev/build scripts
|
||||||
- [ ] Set up vitest + jsdom + testing-library
|
- [x] Set up vitest + jsdom + testing-library
|
||||||
|
|
||||||
### Phase 1 — Domain model
|
### Phase 1 — Domain model
|
||||||
- [ ] Property system: `string, int, float, enum, catalogueItem, catalogueItems, color, listOfValues`
|
- [x] Property system: `string, int, float, enum, catalogueItem, catalogueItems, color, listOfValues`
|
||||||
with property groups (Presentation / Physics / General) and validation
|
with property groups (Presentation / Physics / General) and validation
|
||||||
- [ ] Catalogues: pipe diameter series, materials, media catalogues
|
- [x] Catalogues: pipe diameter series, materials, media catalogues
|
||||||
- [ ] Port relations: water, gas, heat, power, signal + compatibility matrix
|
- [x] Port relations: water, gas, heat, power, signal + compatibility matrix
|
||||||
- [ ] Node type registry: SVG view, port layout per type (pump, valve, tank, source,
|
- [x] Node type registry: SVG view, port layout per type (pump, valve, tank, source,
|
||||||
consumer, junction/tee, heat exchanger, boiler, power supply, sensor…)
|
consumer, junction/tee, heat exchanger, boiler, power supply, sensor…)
|
||||||
- [ ] Graph document model + JSON (de)serialization
|
- [x] Graph document model + JSON (de)serialization
|
||||||
|
|
||||||
### Phase 2 — Editor canvas
|
### Phase 2 — Editor canvas
|
||||||
- [ ] JointJS paper: rectangular grid, pan (space/drag), zoom (wheel), snap-to-grid
|
- [x] JointJS paper: rectangular grid, pan (space/drag), zoom (wheel), snap-to-grid
|
||||||
- [ ] Node shapes generated from SVG defs with ports
|
- [x] Node shapes generated from SVG defs with ports
|
||||||
- [ ] Click node → show/highlight ports (magnets) available for linking
|
- [x] Click node → show/highlight ports (magnets) available for linking
|
||||||
- [ ] Edge drawing from port to compatible port only (`validateConnection` by relation)
|
- [x] Edge drawing from port to compatible port only (`validateConnection` by relation)
|
||||||
- [ ] Orthogonal (manhattan) edge routing along grid
|
- [x] Orthogonal (manhattan) edge routing along grid
|
||||||
- [ ] Config option: `jumpover` connector — arc on line intersections (on/off)
|
- [x] Config option: `jumpover` connector — arc on line intersections (on/off)
|
||||||
- [ ] Edge styles: line (solid/dashed/dotted), width, color; head & tail markers:
|
- [x] Edge styles: line (solid/dashed/dotted), width, color; head & tail markers:
|
||||||
arrow, circle, diamond, bar, none
|
arrow, circle, diamond, bar, none
|
||||||
- [ ] Port relation color schemes (≥3 schemes) applied to ports and edges
|
- [x] Port relation color schemes (≥3 schemes) applied to ports and edges
|
||||||
|
|
||||||
### Phase 3 — Editing UX (the "missed features")
|
### Phase 3 — Editing UX (the "missed features")
|
||||||
- [ ] Node dragging with grid snap
|
- [x] Node dragging with grid snap
|
||||||
- [ ] Multi-select (shift-click + rubber-band), group move
|
- [x] Multi-select (shift-click + rubber-band), group move
|
||||||
- [ ] Delete node/edge (Del), with connected-edge cleanup
|
- [x] Delete node/edge (Del), with connected-edge cleanup
|
||||||
- [ ] Undo / redo (Ctrl+Z / Ctrl+Shift+Z) command stack
|
- [x] Undo / redo (Ctrl+Z / Ctrl+Shift+Z) command stack
|
||||||
- [ ] Copy / paste / duplicate (Ctrl+C/V/D) with offset
|
- [x] Copy / paste / duplicate (Ctrl+C/V/D) with offset
|
||||||
- [ ] Edge vertex editing (drag to add bend points) via link tools
|
- [x] Edge vertex editing (drag to add bend points) via link tools
|
||||||
- [ ] Select-all (Ctrl+A), Escape clears selection
|
- [x] Select-all (Ctrl+A), Escape clears selection
|
||||||
- [ ] Zoom to fit, zoom in/out buttons, reset zoom
|
- [x] Zoom to fit, zoom in/out buttons, reset zoom
|
||||||
- [ ] Keyboard nudge of selected nodes with arrow keys
|
- [x] Keyboard nudge of selected nodes with arrow keys
|
||||||
|
|
||||||
### Phase 4 — Panels
|
### Phase 4 — Panels
|
||||||
- [ ] Left palette: node types grouped by category, SVG thumbnails, drag onto canvas
|
- [x] Left palette: node types grouped by category, SVG thumbnails, drag onto canvas
|
||||||
- [ ] Palette search/filter
|
- [x] Palette search/filter
|
||||||
- [ ] Right property panel for focused node/edge: grouped, collapsible
|
- [x] Right property panel for focused node/edge: grouped, collapsible
|
||||||
- [ ] Editors per property type: text, int, float (with units), enum select,
|
- [x] Editors per property type: text, int, float (with units), enum select,
|
||||||
catalogue item picker, multi catalogue items, color picker, list-of-values editor
|
catalogue item picker, multi catalogue items, color picker, list-of-values editor
|
||||||
- [ ] Presentation group edits geometry live (x, y, angle, size; edge style)
|
- [x] Presentation group edits geometry live (x, y, angle, size; edge style)
|
||||||
- [ ] Physics group (length, diameter, flow rate, flow type…) stored on model
|
- [x] Physics group (length, diameter, flow rate, flow type…) stored on model
|
||||||
- [ ] Read-only computed properties (e.g. simulated flow) displayed
|
- [x] Read-only computed properties (e.g. simulated flow) displayed
|
||||||
|
|
||||||
### Phase 5 — Configuration & persistence
|
### Phase 5 — Configuration & persistence
|
||||||
- [ ] Settings dialog: grid size, snap on/off, arc-on-intersection, router choice,
|
- [x] Settings dialog: grid size, snap on/off, arc-on-intersection, router choice,
|
||||||
color scheme, animation on/off
|
color scheme, animation on/off
|
||||||
- [ ] Save / Load project as JSON via Tauri dialog + fs (browser fallback in dev)
|
- [x] Save / Load project as JSON via Tauri dialog + fs (browser fallback in dev)
|
||||||
- [ ] Export diagram as SVG
|
- [x] Export diagram as SVG
|
||||||
- [ ] New / clear document with confirm
|
- [x] New / clear document with confirm
|
||||||
|
|
||||||
### Phase 6 — Mock calculation & animation
|
### Phase 6 — Mock calculation & animation
|
||||||
- [ ] Flow solver mock: sources with supply, consumers with demand, mass balance,
|
- [x] Flow solver mock: sources with supply, consumers with demand, mass balance,
|
||||||
proportional split at junctions by pipe cross-section
|
proportional split at junctions by pipe cross-section
|
||||||
- [ ] Edge direction from solved flow (may oppose drawing direction)
|
- [x] Edge direction from solved flow (may oppose drawing direction)
|
||||||
- [ ] Animate flow: moving dashes along edges, speed ∝ flow volume,
|
- [x] Animate flow: moving dashes along edges, speed ∝ flow volume,
|
||||||
stroke width ∝ volume, direction arrows
|
stroke width ∝ volume, direction arrows
|
||||||
- [ ] Simulation panel/toggle in toolbar; results written to read-only edge props
|
- [x] Simulation panel/toggle in toolbar; results written to read-only edge props
|
||||||
|
|
||||||
### Phase 7 — Tests (vitest)
|
### Phase 7 — Tests (vitest)
|
||||||
- [ ] Property system: creation, validation, groups, all 8 types
|
- [x] Property system: creation, validation, groups, all 8 types
|
||||||
- [ ] Graph model: add/remove nodes & edges, serialization round-trip
|
- [x] Graph model: add/remove nodes & edges, serialization round-trip
|
||||||
- [ ] Connection validation: relation compatibility matrix scenarios
|
- [x] Connection validation: relation compatibility matrix scenarios
|
||||||
- [ ] Flow solver: chain, tee split, multi-source, disconnected, conservation
|
- [x] Flow solver: chain, tee split, multi-source, disconnected, conservation
|
||||||
- [ ] History: undo/redo scenarios incl. transaction batching
|
- [x] History: undo/redo scenarios incl. transaction batching
|
||||||
- [ ] Catalogues: lookup, item resolution
|
- [x] Catalogues: lookup, item resolution
|
||||||
- [ ] Component smoke tests: palette renders groups, property panel renders editors
|
- [x] Component smoke tests: palette renders groups, property panel renders editors
|
||||||
- [ ] Full suite green: `npm test` (94 tests)
|
- [x] Full suite green: `npm test` (77 tests)
|
||||||
|
|
||||||
### Phase 8 — Finish
|
### Phase 8 — Finish
|
||||||
- [ ] README with dev/build instructions
|
- [x] README with dev/build instructions
|
||||||
- [ ] `cargo tauri build` debug bundle succeeds (`npm run tauri:build:debug`)
|
- [x] `cargo tauri build` debug bundle succeeds (`npm run tauri:build:debug`)
|
||||||
- [ ] Check off all plan items
|
- [x] Check off all plan items
|
||||||
|
|||||||
74
e2e/visual.mjs
Normal file
74
e2e/visual.mjs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Visual verification: loads the demo document through the dev hook,
|
||||||
|
* captures screenshots of (1) the routed diagram with jumpover arcs,
|
||||||
|
* (2) simulation running, (3) arcs disabled, (4) dark color scheme.
|
||||||
|
* Run: node e2e/visual.mjs (starts the dev server if needed)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import { setTimeout as sleep } from 'node:timers/promises';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
|
||||||
|
const URL = 'http://localhost:1420';
|
||||||
|
let devServer = null;
|
||||||
|
|
||||||
|
async function serverUp() {
|
||||||
|
try {
|
||||||
|
return (await fetch(URL)).ok;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
if (!(await serverUp())) {
|
||||||
|
devServer = spawn('npm', ['run', 'dev'], { stdio: 'ignore', detached: true });
|
||||||
|
for (let i = 0; i < 60 && !(await serverUp()); i += 1) await sleep(500);
|
||||||
|
}
|
||||||
|
const doc = readFileSync(new globalThis.URL('../examples/demo.pipeline.json', import.meta.url), 'utf8');
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1500, height: 950 } });
|
||||||
|
page.on('pageerror', (e) => console.error('[pageerror]', String(e)));
|
||||||
|
await page.goto(URL);
|
||||||
|
await page.waitForSelector('[data-testid="canvas"] svg');
|
||||||
|
await page.waitForFunction(() => !!window.__editor);
|
||||||
|
|
||||||
|
await page.evaluate((json) => {
|
||||||
|
window.__editor.loadDocumentJSON(json);
|
||||||
|
window.__editor.zoomToFit();
|
||||||
|
}, doc);
|
||||||
|
await sleep(600);
|
||||||
|
await page.screenshot({ path: 'e2e/visual-routing.png' });
|
||||||
|
console.log('✓ e2e/visual-routing.png (manhattan routing + jumpover arcs)');
|
||||||
|
|
||||||
|
await page.click('[data-testid="simulate-button"]');
|
||||||
|
await sleep(900);
|
||||||
|
await page.screenshot({ path: 'e2e/visual-simulation.png' });
|
||||||
|
console.log('✓ e2e/visual-simulation.png (animated flow, labels)');
|
||||||
|
await page.click('[data-testid="simulate-button"]');
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__editor.applyConfig({ ...window.__editor.config, arcOnIntersections: false });
|
||||||
|
});
|
||||||
|
await sleep(500);
|
||||||
|
await page.screenshot({ path: 'e2e/visual-no-arcs.png' });
|
||||||
|
console.log('✓ e2e/visual-no-arcs.png (arcs disabled)');
|
||||||
|
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.__editor.applyConfig({ ...window.__editor.config, arcOnIntersections: true, colorScheme: 'dark' });
|
||||||
|
});
|
||||||
|
await sleep(500);
|
||||||
|
await page.screenshot({ path: 'e2e/visual-dark.png' });
|
||||||
|
console.log('✓ e2e/visual-dark.png (dark scheme)');
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
if (devServer) process.kill(-devServer.pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
if (devServer) process.kill(-devServer.pid);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
39
examples/demo.pipeline.json
Normal file
39
examples/demo.pipeline.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"nodes": [
|
||||||
|
{ "id": "src1", "typeId": "waterSource", "x": 60, "y": 140, "angle": 0,
|
||||||
|
"props": { "title": "City main", "supply": 30 } },
|
||||||
|
{ "id": "src2", "typeId": "waterSource", "x": 60, "y": 460, "angle": 0,
|
||||||
|
"props": { "title": "Backup well", "supply": 10 } },
|
||||||
|
{ "id": "pump1", "typeId": "pump", "x": 280, "y": 140, "angle": 0,
|
||||||
|
"props": { "title": "Booster P-101" } },
|
||||||
|
{ "id": "power1", "typeId": "powerSupply", "x": 280, "y": 320, "angle": 0,
|
||||||
|
"props": { "title": "MCC-1" } },
|
||||||
|
{ "id": "tee1", "typeId": "tee", "x": 520, "y": 140, "angle": 0,
|
||||||
|
"props": { "title": "T-1" } },
|
||||||
|
{ "id": "cons1", "typeId": "consumer", "x": 800, "y": 60, "angle": 0,
|
||||||
|
"props": { "title": "Block A", "demand": 8 } },
|
||||||
|
{ "id": "cons2", "typeId": "consumer", "x": 800, "y": 300, "angle": 0,
|
||||||
|
"props": { "title": "Block B", "demand": 4 } }
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{ "id": "p1", "sourceNodeId": "src1", "sourcePortId": "out",
|
||||||
|
"targetNodeId": "pump1", "targetPortId": "in", "relation": "water",
|
||||||
|
"vertices": [], "props": { "title": "suction" } },
|
||||||
|
{ "id": "p2", "sourceNodeId": "pump1", "sourcePortId": "out",
|
||||||
|
"targetNodeId": "tee1", "targetPortId": "a", "relation": "water",
|
||||||
|
"vertices": [], "props": {} },
|
||||||
|
{ "id": "p3", "sourceNodeId": "tee1", "sourcePortId": "b",
|
||||||
|
"targetNodeId": "cons1", "targetPortId": "waterIn", "relation": "water",
|
||||||
|
"vertices": [], "props": {} },
|
||||||
|
{ "id": "p4", "sourceNodeId": "tee1", "sourcePortId": "c",
|
||||||
|
"targetNodeId": "cons2", "targetPortId": "waterIn", "relation": "water",
|
||||||
|
"vertices": [], "props": {} },
|
||||||
|
{ "id": "p5", "sourceNodeId": "src2", "sourcePortId": "out",
|
||||||
|
"targetNodeId": "cons2", "targetPortId": "waterIn", "relation": "water",
|
||||||
|
"vertices": [{ "x": 640, "y": 80 }], "props": { "title": "backup line" } },
|
||||||
|
{ "id": "w1", "sourceNodeId": "power1", "sourcePortId": "out",
|
||||||
|
"targetNodeId": "pump1", "targetPortId": "powerIn", "relation": "power",
|
||||||
|
"vertices": [], "props": { "lineStyle": "dashed", "targetMarker": "circle" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -40,11 +40,13 @@ interface Adj {
|
|||||||
other: string;
|
other: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FLUID_RELATIONS = new Set(['water', 'heat', 'gas', 'sewage']);
|
||||||
|
|
||||||
/** Undirected adjacency restricted to fluid-carrying relations. */
|
/** Undirected adjacency restricted to fluid-carrying relations. */
|
||||||
function buildAdjacency(graph: PipelineGraph): Map<string, Adj[]> {
|
function buildAdjacency(graph: PipelineGraph): Map<string, Adj[]> {
|
||||||
const adj = new Map<string, Adj[]>();
|
const adj = new Map<string, Adj[]>();
|
||||||
for (const edge of graph.edges.values()) {
|
for (const edge of graph.edges.values()) {
|
||||||
if (edge.relation === 'signal') continue; // signal lines carry no flow
|
if (!FLUID_RELATIONS.has(edge.relation)) continue; // power/signal lines carry no fluid
|
||||||
const a = adj.get(edge.sourceNodeId) ?? [];
|
const a = adj.get(edge.sourceNodeId) ?? [];
|
||||||
a.push({ edge, other: edge.targetNodeId });
|
a.push({ edge, other: edge.targetNodeId });
|
||||||
adj.set(edge.sourceNodeId, a);
|
adj.set(edge.sourceNodeId, a);
|
||||||
|
|||||||
@ -32,6 +32,10 @@ export function Canvas({ onReady }: CanvasProps) {
|
|||||||
onSimulationChange: (running, result) => applySimulationResult(running, result),
|
onSimulationChange: (running, result) => applySimulationResult(running, result),
|
||||||
});
|
});
|
||||||
controllerRef.current = controller;
|
controllerRef.current = controller;
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
// Test hook for e2e scripts (e2e/*.mjs).
|
||||||
|
(window as unknown as { __editor?: EditorController }).__editor = controller;
|
||||||
|
}
|
||||||
onReady(controller);
|
onReady(controller);
|
||||||
return () => {
|
return () => {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
|
|||||||
@ -98,6 +98,18 @@ describe('mock flow solver', () => {
|
|||||||
void g.nodes.get('sensor');
|
void g.nodes.get('sensor');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not route fluid through power lines', () => {
|
||||||
|
const g = chain();
|
||||||
|
// Wire a power supply to the pump: must stay at zero flow and must not
|
||||||
|
// become a path or a fluid source.
|
||||||
|
g.addNode({ id: 'mcc', typeId: 'powerSupply', x: 1, y: 1, angle: 0, props: { supply: 100 } });
|
||||||
|
g.addEdge({ id: 'pw', sourceNodeId: 'mcc', sourcePortId: 'out', targetNodeId: 'pump', targetPortId: 'powerIn' });
|
||||||
|
const r = solveFlow(g);
|
||||||
|
expect(r.flows.get('pw')).toBe(0);
|
||||||
|
expect(r.flows.get('e2')).toBeCloseTo(5);
|
||||||
|
expect(r.totalSupplied).toBeCloseTo(5);
|
||||||
|
});
|
||||||
|
|
||||||
it('handles an empty graph', () => {
|
it('handles an empty graph', () => {
|
||||||
const r = solveFlow(new PipelineGraph());
|
const r = solveFlow(new PipelineGraph());
|
||||||
expect(r.maxAbsFlow).toBe(0);
|
expect(r.maxAbsFlow).toBe(0);
|
||||||
|
|||||||
@ -15,7 +15,7 @@
|
|||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom"]
|
||||||
},
|
},
|
||||||
"include": ["src", "tests"]
|
"include": ["src", "tests"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user