feat(model): add a gallery of sample models
This commit is contained in:
155
src/model/samples.ts
Normal file
155
src/model/samples.ts
Normal file
@@ -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,
|
||||||
|
},
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user