Replace the cramped Samples dropdown with a modal that lays the gallery
out as a card grid, sectioned by tier, so the growing set stays scannable
and the learning order is visible at once.
The results panel floated over the canvas and hid the lower part of the
diagram. Dock it beneath the canvas instead, and refit the view when it
opens or closes so the whole diagram stays framed in the height that
remains. The refit fires on Vue Flow's `dimensions` (post-measure), not
on the toggle, so it frames against the settled size.
A simulation store owns one run and one playhead, so the chart and the canvas
read off the same instant. While the panel is open, Stocks show a live value and
a fill gauge, Flows and Converters their current value; a play/pause + scrubber
drives the playhead (a wall-clock-paced clock in usePlayback), and a marker
tracks it on the chart.
- 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
Swap the filled-triangle arrowhead for an open arrow on flow pipes and
information links, and route flow pipes through a custom cubic edge whose
horizontal flatten arm scales with the vertical drop, not just the
horizontal gap. Vue Flow's default arm collapses toward zero when the
valve sits nearly above the stock, so a steep pipe stayed flat only in
its last pixels and the arrowhead detached from the visible line. The
pipe now keeps a real horizontal run into the stock's left edge at any
approach angle.
Dragging a Flow back onto a Stock it already touches is refused (an
Information Link never targets a Stock), but that gesture is almost
always someone trying to "close the loop" by hand. The Flow's own pipe
already is the link back, so surface a self-dismissing hint saying so
instead of letting the drop die silently (F4: guide, don't nag).
Restoring on reload fit the view synchronously in onRestore, framing
against unmeasured 0×0 nodes, so the model landed jammed top-left. Arm
the same post-measure onNodesInitialized fit the sample/import loads use.