From 6d03d706a0a5c389e3da5efdab05ac50e5393d1b Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Sat, 20 Jun 2026 09:37:32 +0200 Subject: [PATCH] feat(model): add a gallery of sample models --- src/model/samples.ts | 155 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/model/samples.ts diff --git a/src/model/samples.ts b/src/model/samples.ts new file mode 100644 index 0000000..a4480f2 --- /dev/null +++ b/src/model/samples.ts @@ -0,0 +1,155 @@ +/** + * Sample Models — a small, curated gallery the user can load to learn the + * language by example (CONTEXT.md) and to have something on the canvas in one + * click. + * + * The set is deliberately "simple yet exhaustive": each Model is the smallest + * thing that makes its point, but read top to bottom they introduce the whole + * vocabulary and both loop kinds, one new idea at a time: + * + * 1. Bathtub — Stock, Flow (in/out), Source, Sink. No feedback. + * 2. Savings account — + Information Link, `+` polarity → a Reinforcing loop. + * 3. Coffee cooling — + Converter, `−` polarity → a Balancing loop. + * 4. Population — all of the above at once: Reinforcing *and* Balancing. + * + * These are plain data built from the same tested constructors the store uses + * (factory.ts), so every sample is a valid Model by construction. `build()` + * mints fresh ids on each call, so loading a sample twice never collides. + */ +import { makeCloud, makeConverter, makeFlow, makeStock, midpoint, newId } from "./factory" +import { + type InformationLink, + type Model, + MODEL_VERSION, + type ModelNode, + type Polarity, +} from "./types" + +/** A loadable example: a title and one-line blurb for the menu, plus a builder. */ +export interface Sample { + title: string + blurb: string + build: () => Model +} + +/** Information Link between two already-built nodes, with an explicit polarity. */ +function link(source: ModelNode, target: ModelNode, polarity: Polarity): InformationLink { + return { id: newId("link"), source: source.id, target: target.id, polarity } +} + +function model(name: string, nodes: ModelNode[], infoLinks: InformationLink[]): Model { + return { version: MODEL_VERSION, id: newId("model"), name, nodes, infoLinks } +} + +/** + * Bathtub — the canonical first model. A Stock filled by one Flow and drained by + * another, each open end resting on a Cloud. No Information Links, so no + * feedback: it just shows the substrate (Stock, Flow, Source, Sink). + */ +function bathtub(): Model { + const source = makeCloud({ x: -278, y: -18 }) + const water = makeStock({ x: -48, y: -20 }, "Water") + const sink = makeCloud({ x: 242, y: -18 }) + const filling = makeFlow( + midpoint(source.position, water.position), + "filling", + source.id, + water.id, + ) + const emptying = makeFlow(midpoint(water.position, sink.position), "emptying", water.id, sink.id) + return model("Bathtub", [source, water, sink, filling, emptying], []) +} + +/** + * Savings account — the simplest Reinforcing loop. Interest flows in from outside + * (a Source), and the bigger the Balance the larger the interest: Balance → [+] + * → interest → (inflow) → Balance. Even number of `−` (zero) → Reinforcing. + */ +function savings(): Model { + // Source up-left, Balance down-right: the interest valve lands at their + // midpoint (above Balance), so the `Balance → interest` link arcs back up as a + // visible Reinforcing loop instead of overlapping the inflow pipe. + const source = makeCloud({ x: -250, y: -70 }) + const balance = makeStock({ x: 110, y: 30 }, "Balance") + const interest = makeFlow( + midpoint(source.position, balance.position), + "interest", + source.id, + balance.id, + ) + return model("Savings account", [source, balance, interest], [link(balance, interest, "+")]) +} + +/** + * Coffee cooling — the simplest Balancing loop, plus a Converter. Heat leaves the + * cup faster the hotter it is (Coffee → [+] → heat loss) but slower the warmer + * the room (room temperature → [−] → heat loss). The loop Coffee → heat loss → + * (outflow) → Coffee has one `−` → Balancing: it settles toward room temperature. + */ +function coffee(): Model { + const coffee = makeStock({ x: -200, y: -10 }, "Coffee") + const sink = makeCloud({ x: 190, y: -8 }) + const heatLoss = makeFlow( + midpoint(coffee.position, sink.position), + "heat loss", + coffee.id, + sink.id, + ) + const room = makeConverter({ x: -29, y: -150 }, "room temperature") + return model( + "Coffee cooling", + [coffee, sink, heatLoss, room], + [link(coffee, heatLoss, "+"), link(room, heatLoss, "-")], + ) +} + +/** + * Population — the whole language in one model. Births add to Population and more + * people means more births (Reinforcing); deaths remove from it and more people + * means more deaths (Balancing). Converters feed each rate: fertility raises + * births (`+`), life expectancy lowers deaths (`−`). + */ +function population(): Model { + const source = makeCloud({ x: -360, y: -8 }) + const people = makeStock({ x: -48, y: -10 }, "Population") + const sink = makeCloud({ x: 250, y: -8 }) + const births = makeFlow( + midpoint(source.position, people.position), + "births", + source.id, + people.id, + ) + const deaths = makeFlow(midpoint(people.position, sink.position), "deaths", people.id, sink.id) + const fertility = makeConverter({ x: -204, y: -150 }, "fertility") + const lifeExpectancy = makeConverter({ x: 101, y: -150 }, "life expectancy") + return model( + "Population", + [source, people, sink, births, deaths, fertility, lifeExpectancy], + [ + link(people, births, "+"), + link(fertility, births, "+"), + link(people, deaths, "+"), + link(lifeExpectancy, deaths, "-"), + ], + ) +} + +/** The gallery, ordered simplest first. */ +export const SAMPLES: Sample[] = [ + { title: "Bathtub", blurb: "A stock filled and drained — no feedback yet.", build: bathtub }, + { + title: "Savings account", + blurb: "Interest on a balance: a Reinforcing loop.", + build: savings, + }, + { + title: "Coffee cooling", + blurb: "Settling toward room temperature: a Balancing loop.", + build: coffee, + }, + { + title: "Population", + blurb: "Births and deaths: Reinforcing and Balancing together.", + build: population, + }, +]