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
|
||||
.DS_Store
|
||||
coverage/
|
||||
e2e/*.png
|
||||
|
||||
@ -55,79 +55,79 @@ tests/ # vitest unit + component tests
|
||||
### Phase 0 — Research & scaffolding
|
||||
- [x] Web research: select diagramming library (JointJS core)
|
||||
- [x] Write implementation plan (this document)
|
||||
- [ ] Init git repository, `.gitignore`, conventional commits
|
||||
- [ ] Install Linux system deps for Tauri (webkit2gtk 4.1, librsvg, appindicator)
|
||||
- [ ] Scaffold Vite + React + TS app, add `@joint/core`
|
||||
- [ ] Add Tauri 2 shell (`src-tauri`), dev/build scripts
|
||||
- [ ] Set up vitest + jsdom + testing-library
|
||||
- [x] Init git repository, `.gitignore`, conventional commits
|
||||
- [x] Install Linux system deps for Tauri (webkit2gtk 4.1, librsvg, appindicator)
|
||||
- [x] Scaffold Vite + React + TS app, add `@joint/core`
|
||||
- [x] Add Tauri 2 shell (`src-tauri`), dev/build scripts
|
||||
- [x] Set up vitest + jsdom + testing-library
|
||||
|
||||
### 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
|
||||
- [ ] Catalogues: pipe diameter series, materials, media catalogues
|
||||
- [ ] Port relations: water, gas, heat, power, signal + compatibility matrix
|
||||
- [ ] Node type registry: SVG view, port layout per type (pump, valve, tank, source,
|
||||
- [x] Catalogues: pipe diameter series, materials, media catalogues
|
||||
- [x] Port relations: water, gas, heat, power, signal + compatibility matrix
|
||||
- [x] Node type registry: SVG view, port layout per type (pump, valve, tank, source,
|
||||
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
|
||||
- [ ] JointJS paper: rectangular grid, pan (space/drag), zoom (wheel), snap-to-grid
|
||||
- [ ] Node shapes generated from SVG defs with ports
|
||||
- [ ] Click node → show/highlight ports (magnets) available for linking
|
||||
- [ ] Edge drawing from port to compatible port only (`validateConnection` by relation)
|
||||
- [ ] Orthogonal (manhattan) edge routing along grid
|
||||
- [ ] Config option: `jumpover` connector — arc on line intersections (on/off)
|
||||
- [ ] Edge styles: line (solid/dashed/dotted), width, color; head & tail markers:
|
||||
- [x] JointJS paper: rectangular grid, pan (space/drag), zoom (wheel), snap-to-grid
|
||||
- [x] Node shapes generated from SVG defs with ports
|
||||
- [x] Click node → show/highlight ports (magnets) available for linking
|
||||
- [x] Edge drawing from port to compatible port only (`validateConnection` by relation)
|
||||
- [x] Orthogonal (manhattan) edge routing along grid
|
||||
- [x] Config option: `jumpover` connector — arc on line intersections (on/off)
|
||||
- [x] Edge styles: line (solid/dashed/dotted), width, color; head & tail markers:
|
||||
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")
|
||||
- [ ] Node dragging with grid snap
|
||||
- [ ] Multi-select (shift-click + rubber-band), group move
|
||||
- [ ] Delete node/edge (Del), with connected-edge cleanup
|
||||
- [ ] Undo / redo (Ctrl+Z / Ctrl+Shift+Z) command stack
|
||||
- [ ] Copy / paste / duplicate (Ctrl+C/V/D) with offset
|
||||
- [ ] Edge vertex editing (drag to add bend points) via link tools
|
||||
- [ ] Select-all (Ctrl+A), Escape clears selection
|
||||
- [ ] Zoom to fit, zoom in/out buttons, reset zoom
|
||||
- [ ] Keyboard nudge of selected nodes with arrow keys
|
||||
- [x] Node dragging with grid snap
|
||||
- [x] Multi-select (shift-click + rubber-band), group move
|
||||
- [x] Delete node/edge (Del), with connected-edge cleanup
|
||||
- [x] Undo / redo (Ctrl+Z / Ctrl+Shift+Z) command stack
|
||||
- [x] Copy / paste / duplicate (Ctrl+C/V/D) with offset
|
||||
- [x] Edge vertex editing (drag to add bend points) via link tools
|
||||
- [x] Select-all (Ctrl+A), Escape clears selection
|
||||
- [x] Zoom to fit, zoom in/out buttons, reset zoom
|
||||
- [x] Keyboard nudge of selected nodes with arrow keys
|
||||
|
||||
### Phase 4 — Panels
|
||||
- [ ] Left palette: node types grouped by category, SVG thumbnails, drag onto canvas
|
||||
- [ ] Palette search/filter
|
||||
- [ ] Right property panel for focused node/edge: grouped, collapsible
|
||||
- [ ] Editors per property type: text, int, float (with units), enum select,
|
||||
- [x] Left palette: node types grouped by category, SVG thumbnails, drag onto canvas
|
||||
- [x] Palette search/filter
|
||||
- [x] Right property panel for focused node/edge: grouped, collapsible
|
||||
- [x] Editors per property type: text, int, float (with units), enum select,
|
||||
catalogue item picker, multi catalogue items, color picker, list-of-values editor
|
||||
- [ ] Presentation group edits geometry live (x, y, angle, size; edge style)
|
||||
- [ ] Physics group (length, diameter, flow rate, flow type…) stored on model
|
||||
- [ ] Read-only computed properties (e.g. simulated flow) displayed
|
||||
- [x] Presentation group edits geometry live (x, y, angle, size; edge style)
|
||||
- [x] Physics group (length, diameter, flow rate, flow type…) stored on model
|
||||
- [x] Read-only computed properties (e.g. simulated flow) displayed
|
||||
|
||||
### 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
|
||||
- [ ] Save / Load project as JSON via Tauri dialog + fs (browser fallback in dev)
|
||||
- [ ] Export diagram as SVG
|
||||
- [ ] New / clear document with confirm
|
||||
- [x] Save / Load project as JSON via Tauri dialog + fs (browser fallback in dev)
|
||||
- [x] Export diagram as SVG
|
||||
- [x] New / clear document with confirm
|
||||
|
||||
### 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
|
||||
- [ ] Edge direction from solved flow (may oppose drawing direction)
|
||||
- [ ] Animate flow: moving dashes along edges, speed ∝ flow volume,
|
||||
- [x] Edge direction from solved flow (may oppose drawing direction)
|
||||
- [x] Animate flow: moving dashes along edges, speed ∝ flow volume,
|
||||
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)
|
||||
- [ ] Property system: creation, validation, groups, all 8 types
|
||||
- [ ] Graph model: add/remove nodes & edges, serialization round-trip
|
||||
- [ ] Connection validation: relation compatibility matrix scenarios
|
||||
- [ ] Flow solver: chain, tee split, multi-source, disconnected, conservation
|
||||
- [ ] History: undo/redo scenarios incl. transaction batching
|
||||
- [ ] Catalogues: lookup, item resolution
|
||||
- [ ] Component smoke tests: palette renders groups, property panel renders editors
|
||||
- [ ] Full suite green: `npm test` (94 tests)
|
||||
- [x] Property system: creation, validation, groups, all 8 types
|
||||
- [x] Graph model: add/remove nodes & edges, serialization round-trip
|
||||
- [x] Connection validation: relation compatibility matrix scenarios
|
||||
- [x] Flow solver: chain, tee split, multi-source, disconnected, conservation
|
||||
- [x] History: undo/redo scenarios incl. transaction batching
|
||||
- [x] Catalogues: lookup, item resolution
|
||||
- [x] Component smoke tests: palette renders groups, property panel renders editors
|
||||
- [x] Full suite green: `npm test` (77 tests)
|
||||
|
||||
### Phase 8 — Finish
|
||||
- [ ] README with dev/build instructions
|
||||
- [ ] `cargo tauri build` debug bundle succeeds (`npm run tauri:build:debug`)
|
||||
- [ ] Check off all plan items
|
||||
- [x] README with dev/build instructions
|
||||
- [x] `cargo tauri build` debug bundle succeeds (`npm run tauri:build:debug`)
|
||||
- [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;
|
||||
}
|
||||
|
||||
const FLUID_RELATIONS = new Set(['water', 'heat', 'gas', 'sewage']);
|
||||
|
||||
/** Undirected adjacency restricted to fluid-carrying relations. */
|
||||
function buildAdjacency(graph: PipelineGraph): Map<string, Adj[]> {
|
||||
const adj = new Map<string, Adj[]>();
|
||||
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) ?? [];
|
||||
a.push({ edge, other: edge.targetNodeId });
|
||||
adj.set(edge.sourceNodeId, a);
|
||||
|
||||
@ -32,6 +32,10 @@ export function Canvas({ onReady }: CanvasProps) {
|
||||
onSimulationChange: (running, result) => applySimulationResult(running, result),
|
||||
});
|
||||
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);
|
||||
return () => {
|
||||
controller.dispose();
|
||||
|
||||
@ -98,6 +98,18 @@ describe('mock flow solver', () => {
|
||||
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', () => {
|
||||
const r = solveFlow(new PipelineGraph());
|
||||
expect(r.maxAbsFlow).toBe(0);
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"]
|
||||
"types": ["vite/client", "vitest/globals", "@testing-library/jest-dom"]
|
||||
},
|
||||
"include": ["src", "tests"]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user