feat(editor): add a simulation panel and an element inspector

- ResultsPanel: a Simulate panel that charts every Stock over time (hand-built
  SVG), with start/stop/dt run controls and a divergence warning
- Inspector: edit a selected Stock's initial value or a Flow/Converter's rule
- store: undoable setInitialValue / setRule / setSimSpec actions
This commit is contained in:
Julien Calixte
2026-06-20 13:56:19 +02:00
parent 40dc9fba43
commit 34df540e4a
4 changed files with 390 additions and 1 deletions

View File

@@ -24,7 +24,15 @@ import {
nextName,
} from "@/model/factory"
import { detectLoops } from "@/model/loops"
import type { ConverterNode, Model, ModelNode, Position, StockNode } from "@/model/types"
import type {
ConverterNode,
Model,
ModelNode,
Position,
Rule,
SimSpec,
StockNode,
} from "@/model/types"
import { canConnect, intentFor } from "@/model/validation"
/** Ring-buffer depth for undo (F9 target: ≥50 steps). */
@@ -96,6 +104,47 @@ export const useModelStore = defineStore("model", () => {
node.name = name
}
/**
* Set (or clear) a Stock's initial value — the quantity the simulator starts
* from (ADR-0004). `undefined` un-equips it, sending the Model back to "not yet
* simulatable". No-op when unchanged, so a blur with no edit doesn't burn undo.
*/
function setInitialValue(id: string, value: number | undefined): void {
const node = findNode(id)
if (!node || node.kind !== "stock" || node.initialValue === value) return
record()
if (value === undefined) delete node.initialValue
else node.initialValue = value
}
/**
* Set (or clear) how a Flow's rate or a Converter's value is computed (ADR-0004:
* one of the fixed rules, never a formula). No-op when unchanged.
*/
function setRule(id: string, rule: Rule | undefined): void {
const node = findNode(id)
if (!node || (node.kind !== "flow" && node.kind !== "converter")) return
if (JSON.stringify(node.rule) === JSON.stringify(rule)) return
record()
if (rule === undefined) delete node.rule
else node.rule = rule
}
/** Set the run parameters (start / stop / dt). No-op when unchanged. */
function setSimSpec(spec: SimSpec): void {
const current = model.value.sim
if (
current &&
current.start === spec.start &&
current.stop === spec.stop &&
current.dt === spec.dt
) {
return
}
record()
model.value.sim = spec
}
/**
* Create whatever source→target means under the structure guard: an
* Information Link (default `+` polarity, F6) onto a Flow/Converter, or a Flow
@@ -229,6 +278,9 @@ export const useModelStore = defineStore("model", () => {
beginInteraction,
moveNode,
renameNode,
setInitialValue,
setRule,
setSimSpec,
removeNode,
removeInfoLink,
toggleLinkPolarity,