@classytic/stage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/assets/index.d.mts +3 -0
  4. package/dist/assets/index.mjs +5 -0
  5. package/dist/assets/kit/glyphs.d.mts +61 -0
  6. package/dist/assets/kit/glyphs.mjs +256 -0
  7. package/dist/assets/kit/index.d.mts +2 -0
  8. package/dist/assets/kit/index.mjs +4 -0
  9. package/dist/assets/kit/svg-defs.d.mts +6 -0
  10. package/dist/assets/kit/svg-defs.mjs +137 -0
  11. package/dist/builder/Palette.d.mts +30 -0
  12. package/dist/builder/Palette.mjs +100 -0
  13. package/dist/builder/SceneBuilder.d.mts +26 -0
  14. package/dist/builder/SceneBuilder.mjs +340 -0
  15. package/dist/builder/controls.d.mts +18 -0
  16. package/dist/builder/controls.mjs +29 -0
  17. package/dist/builder/editor.d.mts +46 -0
  18. package/dist/builder/editor.mjs +130 -0
  19. package/dist/builder/icons.d.mts +14 -0
  20. package/dist/builder/icons.mjs +143 -0
  21. package/dist/builder/index.d.mts +6 -0
  22. package/dist/builder/index.mjs +7 -0
  23. package/dist/builder/tools.d.mts +47 -0
  24. package/dist/builder/tools.mjs +303 -0
  25. package/dist/chem/index.d.mts +118 -0
  26. package/dist/chem/index.mjs +131 -0
  27. package/dist/core/clock.d.mts +41 -0
  28. package/dist/core/clock.mjs +62 -0
  29. package/dist/core/context.d.mts +13 -0
  30. package/dist/core/context.mjs +29 -0
  31. package/dist/core/control.d.mts +58 -0
  32. package/dist/core/control.mjs +113 -0
  33. package/dist/core/coords.d.mts +51 -0
  34. package/dist/core/coords.mjs +44 -0
  35. package/dist/core/index.d.mts +8 -0
  36. package/dist/core/index.mjs +9 -0
  37. package/dist/core/learner.d.mts +44 -0
  38. package/dist/core/learner.mjs +23 -0
  39. package/dist/core/motion.d.mts +73 -0
  40. package/dist/core/motion.mjs +103 -0
  41. package/dist/core/richText.d.mts +28 -0
  42. package/dist/core/richText.mjs +52 -0
  43. package/dist/core/vec.d.mts +47 -0
  44. package/dist/core/vec.mjs +144 -0
  45. package/dist/field/index.d.mts +58 -0
  46. package/dist/field/index.mjs +156 -0
  47. package/dist/index.d.mts +54 -0
  48. package/dist/index.mjs +55 -0
  49. package/dist/interaction/MovableDot.d.mts +33 -0
  50. package/dist/interaction/MovableDot.mjs +66 -0
  51. package/dist/interaction/hitTest.d.mts +16 -0
  52. package/dist/interaction/hitTest.mjs +39 -0
  53. package/dist/interaction/index.d.mts +5 -0
  54. package/dist/interaction/index.mjs +6 -0
  55. package/dist/interaction/useDraggable.d.mts +51 -0
  56. package/dist/interaction/useDraggable.mjs +116 -0
  57. package/dist/interaction/useScreenToMath.d.mts +12 -0
  58. package/dist/interaction/useScreenToMath.mjs +25 -0
  59. package/dist/logic/ast.d.mts +29 -0
  60. package/dist/logic/ast.mjs +37 -0
  61. package/dist/logic/index.d.mts +25 -0
  62. package/dist/logic/index.mjs +36 -0
  63. package/dist/logic/latex.d.mts +6 -0
  64. package/dist/logic/latex.mjs +33 -0
  65. package/dist/logic/minimize.d.mts +37 -0
  66. package/dist/logic/minimize.mjs +175 -0
  67. package/dist/logic/parse.d.mts +6 -0
  68. package/dist/logic/parse.mjs +68 -0
  69. package/dist/logic/table.d.mts +24 -0
  70. package/dist/logic/table.mjs +65 -0
  71. package/dist/logic/tokenize.mjs +258 -0
  72. package/dist/math/ast.d.mts +35 -0
  73. package/dist/math/ast.mjs +94 -0
  74. package/dist/math/calculus.d.mts +9 -0
  75. package/dist/math/calculus.mjs +188 -0
  76. package/dist/math/defs.mjs +51 -0
  77. package/dist/math/index.d.mts +26 -0
  78. package/dist/math/index.mjs +42 -0
  79. package/dist/math/latex.d.mts +7 -0
  80. package/dist/math/latex.mjs +115 -0
  81. package/dist/math/parse.d.mts +6 -0
  82. package/dist/math/parse.mjs +91 -0
  83. package/dist/math/tokenize.mjs +112 -0
  84. package/dist/primitives/CanvasLayer.d.mts +26 -0
  85. package/dist/primitives/CanvasLayer.mjs +87 -0
  86. package/dist/primitives/Dot.d.mts +27 -0
  87. package/dist/primitives/Dot.mjs +40 -0
  88. package/dist/primitives/Grid.d.mts +30 -0
  89. package/dist/primitives/Grid.mjs +122 -0
  90. package/dist/primitives/Label.d.mts +33 -0
  91. package/dist/primitives/Label.mjs +45 -0
  92. package/dist/primitives/Lines.d.mts +50 -0
  93. package/dist/primitives/Lines.mjs +82 -0
  94. package/dist/primitives/Plot.d.mts +50 -0
  95. package/dist/primitives/Plot.mjs +87 -0
  96. package/dist/primitives/Shapes.d.mts +76 -0
  97. package/dist/primitives/Shapes.mjs +79 -0
  98. package/dist/primitives/Tex.d.mts +27 -0
  99. package/dist/primitives/Tex.mjs +54 -0
  100. package/dist/primitives/index.d.mts +10 -0
  101. package/dist/primitives/index.mjs +10 -0
  102. package/dist/primitives/props.d.mts +15 -0
  103. package/dist/scene/Scene.d.mts +31 -0
  104. package/dist/scene/Scene.mjs +168 -0
  105. package/dist/scene/assets.d.mts +37 -0
  106. package/dist/scene/assets.mjs +31 -0
  107. package/dist/scene/commands.d.mts +47 -0
  108. package/dist/scene/commands.mjs +162 -0
  109. package/dist/scene/evaluators.d.mts +17 -0
  110. package/dist/scene/evaluators.mjs +210 -0
  111. package/dist/scene/index.d.mts +13 -0
  112. package/dist/scene/index.mjs +13 -0
  113. package/dist/scene/migrate.d.mts +11 -0
  114. package/dist/scene/migrate.mjs +35 -0
  115. package/dist/scene/render.d.mts +14 -0
  116. package/dist/scene/render.mjs +126 -0
  117. package/dist/scene/resolve.d.mts +12 -0
  118. package/dist/scene/resolve.mjs +95 -0
  119. package/dist/scene/schema.d.mts +9 -0
  120. package/dist/scene/schema.mjs +51 -0
  121. package/dist/scene/sims.d.mts +18 -0
  122. package/dist/scene/sims.mjs +54 -0
  123. package/dist/scene/store.d.mts +22 -0
  124. package/dist/scene/store.mjs +37 -0
  125. package/dist/scene/types.d.mts +228 -0
  126. package/dist/scene/types.mjs +27 -0
  127. package/dist/sim/equilibrium.d.mts +36 -0
  128. package/dist/sim/equilibrium.mjs +54 -0
  129. package/dist/sim/index.d.mts +9 -0
  130. package/dist/sim/index.mjs +9 -0
  131. package/dist/sim/particles.d.mts +43 -0
  132. package/dist/sim/particles.mjs +136 -0
  133. package/dist/sim/rate.d.mts +36 -0
  134. package/dist/sim/rate.mjs +39 -0
  135. package/dist/sim/registry.d.mts +10 -0
  136. package/dist/sim/registry.mjs +27 -0
  137. package/dist/sim/sampler.d.mts +47 -0
  138. package/dist/sim/sampler.mjs +79 -0
  139. package/dist/sim/thermal.d.mts +78 -0
  140. package/dist/sim/thermal.mjs +172 -0
  141. package/dist/sim/types.d.mts +24 -0
  142. package/dist/sim/wave.d.mts +34 -0
  143. package/dist/sim/wave.mjs +61 -0
  144. package/dist/steps/index.d.mts +62 -0
  145. package/dist/steps/index.mjs +133 -0
  146. package/dist/thermo/index.d.mts +82 -0
  147. package/dist/thermo/index.mjs +133 -0
  148. package/dist/view/Stage.d.mts +39 -0
  149. package/dist/view/Stage.mjs +91 -0
  150. package/dist/view/index.d.mts +4 -0
  151. package/dist/view/index.mjs +5 -0
  152. package/dist/view/useElementSize.d.mts +13 -0
  153. package/dist/view/useElementSize.mjs +40 -0
  154. package/dist/view/useInView.d.mts +9 -0
  155. package/dist/view/useInView.mjs +33 -0
  156. package/package.json +141 -0
  157. package/styles.css +70 -0
@@ -0,0 +1,95 @@
1
+ import { isDerived, isVec2 } from "./types.mjs";
2
+ import { parentIds } from "./schema.mjs";
3
+ import { EVALUATORS } from "./evaluators.mjs";
4
+
5
+ //#region src/scene/resolve.ts
6
+ function bindingParents(bindings) {
7
+ const m = /* @__PURE__ */ new Map();
8
+ for (const b of bindings) {
9
+ const list = m.get(b.from.ref) ?? [];
10
+ list.push(b.to.ref);
11
+ m.set(b.from.ref, list);
12
+ }
13
+ return m;
14
+ }
15
+ function resolve(doc, overlay, prev, simStates) {
16
+ const byId = /* @__PURE__ */ new Map();
17
+ for (const el of doc.elements) byId.set(el.id, el);
18
+ const bindEdges = bindingParents(doc.bindings ?? []);
19
+ const parentsOf = (el) => {
20
+ const refParents = parentIds(el);
21
+ const binds = bindEdges.get(el.id) ?? [];
22
+ return [...refParents, ...binds].filter((id) => byId.has(id));
23
+ };
24
+ const indeg = /* @__PURE__ */ new Map();
25
+ const children = /* @__PURE__ */ new Map();
26
+ for (const el of doc.elements) {
27
+ const ps = parentsOf(el);
28
+ indeg.set(el.id, ps.length);
29
+ for (const p of ps) {
30
+ const cs = children.get(p) ?? [];
31
+ cs.push(el.id);
32
+ children.set(p, cs);
33
+ }
34
+ }
35
+ const queue = [];
36
+ for (const el of doc.elements) if ((indeg.get(el.id) ?? 0) === 0) queue.push(el.id);
37
+ const order = [];
38
+ while (queue.length) {
39
+ const id = queue.shift();
40
+ order.push(id);
41
+ for (const c of children.get(id) ?? []) {
42
+ const n = (indeg.get(c) ?? 0) - 1;
43
+ indeg.set(c, n);
44
+ if (n === 0) queue.push(c);
45
+ }
46
+ }
47
+ const values = /* @__PURE__ */ new Map();
48
+ const errors = {};
49
+ const inOrder = new Set(order);
50
+ for (const el of doc.elements) if (!inOrder.has(el.id)) errors[el.id] = "cycle";
51
+ const ctx = {
52
+ get: (r) => values.get(r.ref),
53
+ vec: (r) => {
54
+ const v = values.get(r.ref);
55
+ return isVec2(v) ? v : void 0;
56
+ },
57
+ num: (x) => typeof x === "number" ? x : numOfVal(values.get(x.ref)),
58
+ prevVal: (id) => prev?.values.get(id),
59
+ simField: (sim, field) => simStates?.get(sim)?.[field],
60
+ selfId: ""
61
+ };
62
+ for (const id of order) {
63
+ const el = byId.get(id);
64
+ if (!el) continue;
65
+ try {
66
+ if (isDerived(el)) {
67
+ const evalFn = EVALUATORS[el.def.op];
68
+ if (!evalFn) throw new Error(`no evaluator for op '${el.def.op}'`);
69
+ ctx.selfId = id;
70
+ values.set(id, evalFn(el.def, ctx));
71
+ } else if (el.kind === "point") {
72
+ const o = overlay?.get(id);
73
+ values.set(id, isVec2(o) ? o : el.free.at);
74
+ } else if (el.kind === "scalar") {
75
+ const o = overlay?.get(id);
76
+ values.set(id, typeof o === "number" ? o : el.free.value);
77
+ } else if (el.kind === "note") values.set(id, el.free.at);
78
+ } catch (e) {
79
+ errors[id] = e instanceof Error ? e.message : String(e);
80
+ }
81
+ }
82
+ return {
83
+ values,
84
+ order,
85
+ errors
86
+ };
87
+ }
88
+ function numOfVal(v) {
89
+ if (typeof v === "number") return v;
90
+ if (typeof v === "boolean") return v ? 1 : 0;
91
+ return NaN;
92
+ }
93
+
94
+ //#endregion
95
+ export { resolve };
@@ -0,0 +1,9 @@
1
+ import { DerivedDef, Id, Op, Ref, SceneElement } from "./types.mjs";
2
+
3
+ //#region src/scene/schema.d.ts
4
+ declare function isRef(x: unknown): x is Ref;
5
+ declare const OP_REFS: Record<Op, (def: DerivedDef) => Ref[]>;
6
+ /** Parent element ids of `el` from its def's declared Refs (free elements have none). */
7
+ declare function parentIds(el: SceneElement): Id[];
8
+ //#endregion
9
+ export { OP_REFS, isRef, parentIds };
@@ -0,0 +1,51 @@
1
+ import { isDerived } from "./types.mjs";
2
+
3
+ //#region src/scene/schema.ts
4
+ function isRef(x) {
5
+ return !!x && typeof x === "object" && "ref" in x && typeof x.ref === "string";
6
+ }
7
+ const refsOnly = (...xs) => xs.filter(isRef);
8
+ const OP_REFS = {
9
+ midpoint: (d) => d.of,
10
+ intersect: (d) => d.of,
11
+ circle: (d) => {
12
+ const x = d;
13
+ return refsOnly(x.center, x.through);
14
+ },
15
+ line: (d) => d.through,
16
+ segment: (d) => {
17
+ const x = d;
18
+ return [x.from, x.to];
19
+ },
20
+ polygon: (d) => d.vertices,
21
+ distance: (d) => d.of,
22
+ angle: (d) => d.of,
23
+ linop: (d) => d.terms.map((t) => t.in),
24
+ clamp: (d) => [d.in],
25
+ lerp: (d) => {
26
+ const x = d;
27
+ return refsOnly(x.a, x.b, x.t);
28
+ },
29
+ reduce: (d) => d.inputs,
30
+ product: (d) => d.of,
31
+ compare: (d) => {
32
+ const x = d;
33
+ return refsOnly(x.a, x.b);
34
+ },
35
+ select: (d) => {
36
+ const x = d;
37
+ return refsOnly(x.cond, x.then, x.else);
38
+ },
39
+ expr: (d) => Object.values(d.inputs),
40
+ asset: (d) => Object.values(d.bind)
41
+ };
42
+ /** Parent element ids of `el` from its def's declared Refs (free elements have none). */
43
+ function parentIds(el) {
44
+ if (!isDerived(el)) return [];
45
+ const extract = OP_REFS[el.def.op];
46
+ if (!extract) return [];
47
+ return extract(el.def).map((r) => r.ref);
48
+ }
49
+
50
+ //#endregion
51
+ export { OP_REFS, isRef, parentIds };
@@ -0,0 +1,18 @@
1
+ import { Id, SimDecl } from "./types.mjs";
2
+
3
+ //#region src/scene/sims.d.ts
4
+ /** Live per-instance core state, keyed by SimDecl.id. */
5
+ type SimStates = Map<Id, Record<string, unknown>>;
6
+ /** Seed each declared core from its params. */
7
+ declare function initSims(decls: SimDecl[]): SimStates;
8
+ /**
9
+ * Advance every core by `dtSec` and collect the scalar overlay its `drives` map
10
+ * produces. Returns fresh states (treat the input as immutable) + the overlay.
11
+ */
12
+ declare function stepSims(decls: SimDecl[], prev: SimStates, dtSec: number): {
13
+ states: SimStates;
14
+ scalars: Map<Id, number>;
15
+ done: boolean;
16
+ };
17
+ //#endregion
18
+ export { SimStates, initSims, stepSims };
@@ -0,0 +1,54 @@
1
+ import { getSim } from "../sim/registry.mjs";
2
+
3
+ //#region src/scene/sims.ts
4
+ /**
5
+ * Sim runtime for a SceneDoc. A `SimDecl` (in doc.meta.sims) names a registered
6
+ * core + its params, and maps the core's output fields → `scalar` element ids.
7
+ * Each frame the runtime steps every core and produces a scalar overlay; `<Scene>`
8
+ * feeds that overlay into `resolve()` exactly like a drag override, so the pure
9
+ * resolver re-evaluates the DAG with the live values. Time lives here; the
10
+ * resolver stays a pure function of its inputs.
11
+ *
12
+ * Non-finite outputs (NaN/Inf from a diverging core) are dropped, not propagated
13
+ * (graceful failure — a bad sim never blanks the canvas).
14
+ */
15
+ /** Seed each declared core from its params. */
16
+ function initSims(decls) {
17
+ const states = /* @__PURE__ */ new Map();
18
+ for (const d of decls) {
19
+ const core = getSim(d.core);
20
+ if (core) states.set(d.id, core.reset(d.params ?? {}));
21
+ }
22
+ return states;
23
+ }
24
+ /**
25
+ * Advance every core by `dtSec` and collect the scalar overlay its `drives` map
26
+ * produces. Returns fresh states (treat the input as immutable) + the overlay.
27
+ */
28
+ function stepSims(decls, prev, dtSec) {
29
+ const states = /* @__PURE__ */ new Map();
30
+ const scalars = /* @__PURE__ */ new Map();
31
+ for (const d of decls) {
32
+ const core = getSim(d.core);
33
+ const before = prev.get(d.id);
34
+ if (!core || !before) continue;
35
+ const merged = d.params ? {
36
+ ...before,
37
+ ...d.params
38
+ } : before;
39
+ const after = core.step(merged, dtSec);
40
+ states.set(d.id, after);
41
+ for (const [elId, field] of Object.entries(d.drives)) {
42
+ const v = after[field];
43
+ if (typeof v === "number" && Number.isFinite(v)) scalars.set(elId, v);
44
+ }
45
+ }
46
+ return {
47
+ states,
48
+ scalars,
49
+ done: decls.length > 0 && decls.every((d) => states.get(d.id)?.done === true)
50
+ };
51
+ }
52
+
53
+ //#endregion
54
+ export { initSims, stepSims };
@@ -0,0 +1,22 @@
1
+ import { Vec2 } from "../core/vec.mjs";
2
+ import { Id, SceneDoc } from "./types.mjs";
3
+ import { Command } from "./commands.mjs";
4
+ import { Resolved } from "./resolve.mjs";
5
+
6
+ //#region src/scene/store.d.ts
7
+ interface InstanceState {
8
+ selection: ReadonlySet<Id>;
9
+ hover: Id | null;
10
+ dragOverlay: ReadonlyMap<Id, Vec2>;
11
+ }
12
+ declare class SceneStore {
13
+ readonly doc: SceneDoc;
14
+ readonly instance: InstanceState;
15
+ constructor(doc: SceneDoc, instance?: InstanceState);
16
+ static from(raw: unknown): SceneStore;
17
+ apply(cmd: Command): SceneStore;
18
+ withInstance(patch: Partial<InstanceState>): SceneStore;
19
+ resolve(prev?: Resolved): Resolved;
20
+ }
21
+ //#endregion
22
+ export { InstanceState, SceneStore };
@@ -0,0 +1,37 @@
1
+ import { resolve } from "./resolve.mjs";
2
+ import { applyCommand } from "./commands.mjs";
3
+ import { migrate } from "./migrate.mjs";
4
+
5
+ //#region src/scene/store.ts
6
+ const EMPTY_INSTANCE = {
7
+ selection: /* @__PURE__ */ new Set(),
8
+ hover: null,
9
+ dragOverlay: /* @__PURE__ */ new Map()
10
+ };
11
+ var SceneStore = class SceneStore {
12
+ doc;
13
+ instance;
14
+ constructor(doc, instance = EMPTY_INSTANCE) {
15
+ this.doc = doc;
16
+ this.instance = instance;
17
+ }
18
+ static from(raw) {
19
+ return new SceneStore(migrate(raw));
20
+ }
21
+ apply(cmd) {
22
+ return new SceneStore(applyCommand(this.doc, cmd), this.instance);
23
+ }
24
+ withInstance(patch) {
25
+ return new SceneStore(this.doc, {
26
+ ...this.instance,
27
+ ...patch
28
+ });
29
+ }
30
+ resolve(prev) {
31
+ const overlay = this.instance.dragOverlay.size ? this.instance.dragOverlay : void 0;
32
+ return resolve(this.doc, overlay, prev);
33
+ }
34
+ };
35
+
36
+ //#endregion
37
+ export { SceneStore };
@@ -0,0 +1,228 @@
1
+ import { Vec2 } from "../core/vec.mjs";
2
+ import { ViewBox } from "../core/coords.mjs";
3
+
4
+ //#region src/scene/types.d.ts
5
+ type Id = string;
6
+ /** Explicit parent-edge wrapper. A bare string is never an edge. */
7
+ interface Ref {
8
+ ref: Id;
9
+ }
10
+ type NumOrRef = number | Ref;
11
+ /** Structured geometry value an asset resolver computes (single source of truth). */
12
+ interface AssetGeometry {
13
+ kind: 'asset-geom';
14
+ parts: Record<string, Vec2 | Vec2[] | number>;
15
+ meta?: Record<string, unknown>;
16
+ }
17
+ interface CircleVal {
18
+ kind: 'circle';
19
+ center: Vec2;
20
+ r: number;
21
+ }
22
+ interface ShapeLineVal {
23
+ kind: 'line' | 'segment';
24
+ a: Vec2;
25
+ b: Vec2;
26
+ }
27
+ /** A resolved element's value. */
28
+ type Val = number | boolean | string | Vec2 | Vec2[] | CircleVal | ShapeLineVal | AssetGeometry;
29
+ interface ElementStyle {
30
+ color?: string;
31
+ fill?: string;
32
+ opacity?: number;
33
+ weight?: number;
34
+ dashed?: boolean;
35
+ hidden?: boolean;
36
+ layer?: number;
37
+ }
38
+ interface ElementBase {
39
+ id: Id;
40
+ label?: string;
41
+ style?: ElementStyle;
42
+ a11y?: {
43
+ role?: string;
44
+ label?: string;
45
+ };
46
+ meta?: Record<string, unknown>;
47
+ }
48
+ interface FreePoint extends ElementBase {
49
+ kind: 'point';
50
+ free: {
51
+ at: Vec2;
52
+ draggable?: boolean;
53
+ constrain?: 'horizontal' | 'vertical' | {
54
+ onLine: Ref;
55
+ };
56
+ };
57
+ }
58
+ interface FreeScalar extends ElementBase {
59
+ kind: 'scalar';
60
+ free: {
61
+ value: number;
62
+ min?: number;
63
+ max?: number;
64
+ step?: number;
65
+ };
66
+ }
67
+ interface FreeNote extends ElementBase {
68
+ kind: 'note';
69
+ free: {
70
+ at: Vec2;
71
+ text: string;
72
+ };
73
+ }
74
+ type FreeElement = FreePoint | FreeScalar | FreeNote;
75
+ type DerivedDef = {
76
+ op: 'midpoint';
77
+ of: [Ref, Ref];
78
+ } | {
79
+ op: 'intersect';
80
+ of: [Ref, Ref];
81
+ pick?: 0 | 1;
82
+ } | {
83
+ op: 'circle';
84
+ center: Ref;
85
+ radius?: number;
86
+ through?: Ref;
87
+ } | {
88
+ op: 'line';
89
+ through: [Ref, Ref];
90
+ } | {
91
+ op: 'segment';
92
+ from: Ref;
93
+ to: Ref;
94
+ } | {
95
+ op: 'polygon';
96
+ vertices: Ref[];
97
+ } | {
98
+ op: 'distance';
99
+ of: [Ref, Ref];
100
+ } | {
101
+ op: 'angle';
102
+ of: [Ref, Ref, Ref];
103
+ } | {
104
+ op: 'linop';
105
+ terms: {
106
+ coef: number;
107
+ in: Ref;
108
+ }[];
109
+ const?: number;
110
+ } | {
111
+ op: 'clamp';
112
+ in: Ref;
113
+ min: number;
114
+ max: number;
115
+ } | {
116
+ op: 'lerp';
117
+ a: Ref;
118
+ b: Ref;
119
+ t: NumOrRef;
120
+ } | {
121
+ op: 'reduce';
122
+ inputs: Ref[];
123
+ reducer: 'sum' | 'mean' | 'max' | 'min';
124
+ } | {
125
+ op: 'product';
126
+ of: [Ref, Ref];
127
+ } | {
128
+ op: 'compare';
129
+ a: Ref;
130
+ b: NumOrRef;
131
+ cmp: 'lt' | 'le' | 'eq' | 'ge' | 'gt';
132
+ eps?: number;
133
+ } | {
134
+ op: 'select';
135
+ cond: Ref;
136
+ then: NumOrRef;
137
+ else: NumOrRef;
138
+ } | {
139
+ op: 'expr';
140
+ inputs: Record<string, Ref>;
141
+ fn: string;
142
+ } | {
143
+ op: 'asset';
144
+ asset: string;
145
+ params: Record<string, number | Vec2>;
146
+ bind: Record<string, Ref>;
147
+ simBind?: Record<string, {
148
+ sim: Id;
149
+ field: string;
150
+ }>;
151
+ };
152
+ type Op = DerivedDef['op'];
153
+ type DerivedKind = 'point' | 'circle' | 'line' | 'segment' | 'polygon' | 'measure' | 'scalar' | 'boolean' | 'asset';
154
+ interface DerivedElement extends ElementBase {
155
+ kind: DerivedKind;
156
+ def: DerivedDef;
157
+ }
158
+ type SceneElement = FreeElement | DerivedElement;
159
+ /** First-class relation record (ids only, never literals). */
160
+ interface Binding {
161
+ id: Id;
162
+ from: Ref;
163
+ to: Ref;
164
+ rel: string;
165
+ props?: Record<string, unknown>;
166
+ }
167
+ /**
168
+ * Structured pedagogy for a lab — first-class learning data, NOT prose buried in
169
+ * UI text. Authored alongside the scene, surfaced to the learner seam + xAPI.
170
+ */
171
+ interface LabMeta {
172
+ /** Learning objectives this lab evidences (ids or short statements). */
173
+ objectives?: string[];
174
+ /** Known misconceptions: an optional trigger (a state that exposes it) + the note. */
175
+ misconceptions?: {
176
+ trigger?: string;
177
+ note: string;
178
+ }[];
179
+ /** Progressive hints, revealed in order. */
180
+ hints?: string[];
181
+ /** 1 (intro) … 5 (challenge). */
182
+ difficulty?: 1 | 2 | 3 | 4 | 5;
183
+ /** Prerequisite lab/objective ids. */
184
+ prerequisites?: string[];
185
+ /** Human-readable success criterion (what "solved" means). */
186
+ successCriteria?: string;
187
+ }
188
+ /**
189
+ * A time-stepped sim wired into a scene: a registered core advances each frame and
190
+ * its named output fields drive `scalar` elements (by id) as external inputs — so
191
+ * the pure resolver re-evaluates with live values without knowing about time.
192
+ */
193
+ interface SimDecl {
194
+ /** Instance id (unique within the doc). */
195
+ id: Id;
196
+ /** Registered sim-core name (e.g. 'wave'). */
197
+ core: string;
198
+ /** Seed params for the core's reset() (e.g. a sampler's `weights` array, a
199
+ * particle box `{w,h}`). */
200
+ params?: Record<string, number | string | boolean | number[] | Record<string, number>>;
201
+ /** Map: scalar-element id → core-state field that feeds it. */
202
+ drives: Record<Id, string>;
203
+ }
204
+ /** SceneDoc.meta — `pedagogy` + `sims` are typed; the rest stays open for domain use. */
205
+ type SceneMeta = {
206
+ pedagogy?: LabMeta;
207
+ sims?: SimDecl[];
208
+ } & Record<string, unknown>;
209
+ /** Canonical persisted form. `commands[]` is NOT stored — history lives in the Editor. */
210
+ interface SceneDoc {
211
+ schemaVersion: 2;
212
+ type: 'stage-scene';
213
+ view?: ViewBox;
214
+ width?: number;
215
+ height?: number;
216
+ elements: SceneElement[];
217
+ bindings: Binding[];
218
+ meta?: SceneMeta;
219
+ }
220
+ declare function isFree(el: SceneElement): el is FreeElement;
221
+ declare function isDerived(el: SceneElement): el is DerivedElement;
222
+ declare function isVec2(v: Val | undefined): v is Vec2;
223
+ declare function isCircleVal(v: Val | undefined): v is CircleVal;
224
+ declare function isLineVal(v: Val | undefined): v is ShapeLineVal;
225
+ declare function isAssetGeom(v: Val | undefined): v is AssetGeometry;
226
+ declare function numOf(v: Val | undefined): number;
227
+ //#endregion
228
+ export { AssetGeometry, Binding, CircleVal, DerivedDef, DerivedElement, DerivedKind, ElementBase, ElementStyle, FreeElement, FreeNote, FreePoint, FreeScalar, Id, LabMeta, NumOrRef, Op, Ref, SceneDoc, SceneElement, SceneMeta, ShapeLineVal, SimDecl, Val, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isVec2, numOf };
@@ -0,0 +1,27 @@
1
+ //#region src/scene/types.ts
2
+ function isFree(el) {
3
+ return "free" in el;
4
+ }
5
+ function isDerived(el) {
6
+ return "def" in el;
7
+ }
8
+ function isVec2(v) {
9
+ return !!v && typeof v === "object" && "x" in v && "y" in v && !("kind" in v);
10
+ }
11
+ function isCircleVal(v) {
12
+ return !!v && typeof v === "object" && "kind" in v && v.kind === "circle";
13
+ }
14
+ function isLineVal(v) {
15
+ return !!v && typeof v === "object" && "kind" in v && (v.kind === "line" || v.kind === "segment");
16
+ }
17
+ function isAssetGeom(v) {
18
+ return !!v && typeof v === "object" && "kind" in v && v.kind === "asset-geom";
19
+ }
20
+ function numOf(v) {
21
+ if (typeof v === "number") return v;
22
+ if (typeof v === "boolean") return v ? 1 : 0;
23
+ return NaN;
24
+ }
25
+
26
+ //#endregion
27
+ export { isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isVec2, numOf };
@@ -0,0 +1,36 @@
1
+ import { SimCore } from "./types.mjs";
2
+
3
+ //#region src/sim/equilibrium.d.ts
4
+ interface RxnSpecies {
5
+ name: string;
6
+ /** Stoichiometric coefficient (positive). */
7
+ coeff: number;
8
+ /** Which side of the equation. */
9
+ side: 'reactant' | 'product';
10
+ /** Optional colour token (for a coloured-reaction skin). */
11
+ color?: string;
12
+ }
13
+ interface EquilibriumParams {
14
+ species: RxnSpecies[];
15
+ /** Initial concentrations aligned to `species`. Named conc0 (NOT conc) so the
16
+ * runtime's per-frame param-merge can't reset the evolving concentrations. */
17
+ conc0: number[];
18
+ /** Forward / reverse rate constants. Live-mergeable (raise/lower to model a
19
+ * temperature change, which shifts K = k_f/k_r). */
20
+ kf: number;
21
+ kr: number;
22
+ }
23
+ interface EquilibriumState {
24
+ spec: RxnSpecies[];
25
+ conc: number[];
26
+ kf: number;
27
+ kr: number;
28
+ tSec: number;
29
+ /** Reaction quotient Π[product]^b / Π[reactant]^a (→ K at equilibrium). */
30
+ Q: number;
31
+ /** Equilibrium constant k_f/k_r. */
32
+ K: number;
33
+ }
34
+ declare const EquilibriumCore: SimCore<EquilibriumState, EquilibriumParams>;
35
+ //#endregion
36
+ export { EquilibriumCore, EquilibriumParams, EquilibriumState, RxnSpecies };
@@ -0,0 +1,54 @@
1
+ //#region src/sim/equilibrium.ts
2
+ const prod = (state, side) => {
3
+ let p = 1;
4
+ for (let i = 0; i < state.spec.length; i++) if (state.spec[i].side === side) p *= Math.max(0, state.conc[i]) ** state.spec[i].coeff;
5
+ return p;
6
+ };
7
+ const quotient = (state) => {
8
+ const denom = prod(state, "reactant");
9
+ return denom <= 1e-9 ? Infinity : prod(state, "product") / denom;
10
+ };
11
+ const EquilibriumCore = {
12
+ reset(params) {
13
+ const conc = params.conc0.map((c) => Math.max(0, c));
14
+ const base = {
15
+ spec: params.species,
16
+ conc
17
+ };
18
+ return {
19
+ spec: params.species,
20
+ conc,
21
+ kf: params.kf,
22
+ kr: params.kr,
23
+ tSec: 0,
24
+ Q: quotient(base),
25
+ K: params.kr > 0 ? params.kf / params.kr : Infinity
26
+ };
27
+ },
28
+ step(state, dtSec) {
29
+ const dt = Math.max(0, dtSec);
30
+ const dxiRaw = (state.kf * prod(state, "reactant") - state.kr * prod(state, "product")) * dt;
31
+ let maxXi = Math.abs(dxiRaw);
32
+ for (let i = 0; i < state.spec.length; i++) {
33
+ const s = state.spec[i];
34
+ if (s.side === "reactant" && dxiRaw > 0 || s.side === "product" && dxiRaw < 0) maxXi = Math.min(maxXi, state.conc[i] / s.coeff);
35
+ }
36
+ const dxi = Math.sign(dxiRaw) * Math.min(Math.abs(dxiRaw), maxXi);
37
+ const conc = state.conc.map((c, i) => {
38
+ const s = state.spec[i];
39
+ const delta = (s.side === "reactant" ? -1 : 1) * s.coeff * dxi;
40
+ return Math.max(0, c + delta);
41
+ });
42
+ const next = {
43
+ ...state,
44
+ conc,
45
+ tSec: state.tSec + dt
46
+ };
47
+ next.Q = quotient(next);
48
+ next.K = state.kr > 0 ? state.kf / state.kr : Infinity;
49
+ return next;
50
+ }
51
+ };
52
+
53
+ //#endregion
54
+ export { EquilibriumCore };
@@ -0,0 +1,9 @@
1
+ import { SimCore } from "./types.mjs";
2
+ import { WaveCore, WaveMode, WaveParams, WaveState } from "./wave.mjs";
3
+ import { SamplerCore, SamplerParams, SamplerState } from "./sampler.mjs";
4
+ import { RateCore, RateParams, RateState, halfLifeToTau, tauToHalfLife } from "./rate.mjs";
5
+ import { Box, ParticlesCore, ParticlesParams, ParticlesState } from "./particles.mjs";
6
+ import { ETHANOL, Phase, Substance, ThermalCore, ThermalParams, ThermalSeg, ThermalState, WATER, segLabel } from "./thermal.mjs";
7
+ import { EquilibriumCore, EquilibriumParams, EquilibriumState, RxnSpecies } from "./equilibrium.mjs";
8
+ import { getSim, listSims, registerSim } from "./registry.mjs";
9
+ export { type Box, ETHANOL, EquilibriumCore, type EquilibriumParams, type EquilibriumState, ParticlesCore, type ParticlesParams, type ParticlesState, type Phase, RateCore, type RateParams, type RateState, type RxnSpecies, SamplerCore, type SamplerParams, type SamplerState, type SimCore, type Substance, ThermalCore, type ThermalParams, type ThermalSeg, type ThermalState, WATER, WaveCore, type WaveMode, type WaveParams, type WaveState, getSim, halfLifeToTau, listSims, registerSim, segLabel, tauToHalfLife };
@@ -0,0 +1,9 @@
1
+ import { WaveCore } from "./wave.mjs";
2
+ import { SamplerCore } from "./sampler.mjs";
3
+ import { RateCore, halfLifeToTau, tauToHalfLife } from "./rate.mjs";
4
+ import { ParticlesCore } from "./particles.mjs";
5
+ import { ETHANOL, ThermalCore, WATER, segLabel } from "./thermal.mjs";
6
+ import { EquilibriumCore } from "./equilibrium.mjs";
7
+ import { getSim, listSims, registerSim } from "./registry.mjs";
8
+
9
+ export { ETHANOL, EquilibriumCore, ParticlesCore, RateCore, SamplerCore, ThermalCore, WATER, WaveCore, getSim, halfLifeToTau, listSims, registerSim, segLabel, tauToHalfLife };