feat(editor): give each Stock a unit

A Stock can carry a display unit (°C, people, $, …): a new optional field, an
inspector text input, validation + round-trip in io, and a readout beside the
live value on the canvas. Equip the demonstrative samples (Water L, Balance $,
Coffee °C, Population/Epidemic people, Yeast cells). Also bumps the fill gauge
opacity for a more vivid level.
This commit is contained in:
Julien Calixte
2026-06-20 14:47:08 +02:00
parent ed6f011e69
commit 382fdddc68
6 changed files with 57 additions and 13 deletions

View File

@@ -89,6 +89,9 @@ function nodeError(value: unknown, index: number): string | null {
if (kind === "stock" && value.initialValue !== undefined && !isFiniteNumber(value.initialValue)) {
return `${at}.initialValue must be a finite number`
}
if (kind === "stock" && value.unit !== undefined && typeof value.unit !== "string") {
return `${at}.unit must be a string`
}
if (
(kind === "flow" || kind === "converter") &&
value.rule !== undefined &&

View File

@@ -77,6 +77,7 @@ function bathtub(): Model {
const source = makeCloud({ x: -280, y: 0 })
const water = makeStock({ x: 0, y: 0 }, "Water")
water.initialValue = 20
water.unit = "L"
const sink = makeCloud({ x: 280, y: 0 })
const filling = makeFlow(
midpoint(source.position, water.position),
@@ -108,6 +109,7 @@ function savings(): Model {
const source = makeCloud({ x: -240, y: -80 })
const balance = makeStock({ x: 120, y: 40 }, "Balance")
balance.initialValue = 1000
balance.unit = "$"
const interest = makeFlow(
midpoint(source.position, balance.position),
"interest",
@@ -133,6 +135,7 @@ function savings(): Model {
function coffee(): Model {
const coffee = makeStock({ x: -200, y: 0 }, "Coffee")
coffee.initialValue = 90
coffee.unit = "°C"
const sink = makeCloud({ x: 200, y: 0 })
const cooling = makeFlow(midpoint(coffee.position, sink.position), "cooling", coffee.id, sink.id)
// cooling = 0.1 × (Coffee room): the `+` input is the level, the `` the target.
@@ -169,6 +172,7 @@ function population(): Model {
lifeExpectancy.rule = { kind: "constant", value: 70 }
const people = makeStock({ x: 0, y: 0 }, "Population")
people.initialValue = 100
people.unit = "people"
const births = makeFlow({ x: -160, y: -160 }, "births", source.id, people.id)
// births = fertility × Population (both `+` inputs): more people and higher
// fertility, more births — the Reinforcing engine.
@@ -206,6 +210,7 @@ function limitsToGrowth(): Model {
const source = makeCloud({ x: -280, y: 0 })
const yeast = makeStock({ x: 40, y: 0 }, "Yeast")
yeast.initialValue = 20
yeast.unit = "cells"
const growth = makeFlow(midpoint(source.position, yeast.position), "growth", source.id, yeast.id)
// growth = 30% of Yeast (its `+` input): the Reinforcing engine.
growth.rule = { kind: "proportional", factor: 0.3 }
@@ -321,10 +326,13 @@ function predatorPrey(): Model {
function epidemic(): Model {
const susceptible = makeStock({ x: -280, y: 0 }, "Susceptible")
susceptible.initialValue = 990
susceptible.unit = "people"
const infected = makeStock({ x: 0, y: 0 }, "Infected")
infected.initialValue = 10
infected.unit = "people"
const recovered = makeStock({ x: 280, y: 0 }, "Recovered")
recovered.initialValue = 0
recovered.unit = "people"
const infection = makeFlow(
midpoint(susceptible.position, infected.position),
"infection",

View File

@@ -76,6 +76,8 @@ export interface StockNode extends BaseNode {
name: string
/** Initial accumulated quantity. Optional in the diagram phase; the simulator reads it. */
initialValue?: number
/** Unit of the quantity, e.g. "°C", "people", "$" — shown beside the value (display only). */
unit?: string
}
/**