@classytic/stage 0.1.0 → 0.3.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 (110) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +7 -3
  3. package/dist/assets/index.mjs +0 -1
  4. package/dist/assets/kit/glyphs.d.mts +5 -5
  5. package/dist/assets/kit/glyphs.mjs +4 -4
  6. package/dist/builder/Palette.mjs +50 -89
  7. package/dist/builder/SceneBuilder.mjs +18 -76
  8. package/dist/builder/editor.d.mts +2 -1
  9. package/dist/builder/editor.mjs +14 -4
  10. package/dist/builder/tools.d.mts +1 -1
  11. package/dist/builder/tools.mjs +1 -1
  12. package/dist/chem/index.d.mts +5 -5
  13. package/dist/chem/index.mjs +4 -4
  14. package/dist/circuit/index.d.mts +92 -0
  15. package/dist/circuit/index.mjs +333 -0
  16. package/dist/core/clock.d.mts +1 -1
  17. package/dist/core/clock.mjs +3 -3
  18. package/dist/core/control.d.mts +1 -1
  19. package/dist/core/control.mjs +2 -2
  20. package/dist/core/coords.d.mts +4 -4
  21. package/dist/core/coords.mjs +1 -1
  22. package/dist/core/index.d.mts +2 -1
  23. package/dist/core/index.mjs +2 -1
  24. package/dist/core/learner.d.mts +1 -1
  25. package/dist/core/learner.mjs +1 -1
  26. package/dist/core/math.d.mts +26 -0
  27. package/dist/core/math.mjs +37 -0
  28. package/dist/core/motion.d.mts +6 -6
  29. package/dist/core/motion.mjs +6 -6
  30. package/dist/core/richText.d.mts +2 -2
  31. package/dist/core/vec.d.mts +1 -1
  32. package/dist/field/index.d.mts +11 -3
  33. package/dist/field/index.mjs +20 -3
  34. package/dist/finance/bizsim.d.mts +93 -0
  35. package/dist/finance/bizsim.mjs +117 -0
  36. package/dist/finance/index.d.mts +118 -0
  37. package/dist/finance/index.mjs +203 -0
  38. package/dist/index.d.mts +6 -3
  39. package/dist/index.mjs +6 -4
  40. package/dist/interaction/MovableDot.d.mts +17 -1
  41. package/dist/interaction/MovableDot.mjs +160 -43
  42. package/dist/interaction/index.d.mts +2 -1
  43. package/dist/interaction/index.mjs +2 -1
  44. package/dist/interaction/useDraggable.d.mts +1 -1
  45. package/dist/interaction/useDraggable.mjs +55 -5
  46. package/dist/interaction/usePressSpring.d.mts +24 -0
  47. package/dist/interaction/usePressSpring.mjs +67 -0
  48. package/dist/logic/ast.d.mts +1 -1
  49. package/dist/logic/index.mjs +8 -3
  50. package/dist/logic/minimize.d.mts +2 -2
  51. package/dist/logic/minimize.mjs +56 -18
  52. package/dist/logic/table.mjs +1 -1
  53. package/dist/math/ast.d.mts +1 -1
  54. package/dist/math/ast.mjs +1 -1
  55. package/dist/math/calculus.d.mts +22 -1
  56. package/dist/math/calculus.mjs +224 -11
  57. package/dist/math/compile.d.mts +8 -0
  58. package/dist/math/compile.mjs +40 -0
  59. package/dist/math/defs.mjs +1 -1
  60. package/dist/math/index.d.mts +5 -4
  61. package/dist/math/index.mjs +5 -4
  62. package/dist/math/latex.mjs +8 -0
  63. package/dist/math/parse.mjs +2 -2
  64. package/dist/math/tokenize.mjs +1 -1
  65. package/dist/primitives/CanvasLayer.mjs +3 -3
  66. package/dist/primitives/Dot.d.mts +2 -15
  67. package/dist/primitives/Dot.mjs +6 -4
  68. package/dist/primitives/Grid.d.mts +41 -14
  69. package/dist/primitives/Grid.mjs +100 -22
  70. package/dist/primitives/Label.d.mts +2 -15
  71. package/dist/primitives/Label.mjs +4 -3
  72. package/dist/primitives/Lines.d.mts +4 -32
  73. package/dist/primitives/Lines.mjs +10 -8
  74. package/dist/primitives/Shapes.d.mts +5 -43
  75. package/dist/primitives/Shapes.mjs +12 -10
  76. package/dist/primitives/Tex.mjs +2 -2
  77. package/dist/primitives/index.d.mts +2 -2
  78. package/dist/primitives/index.mjs +2 -2
  79. package/dist/primitives/props.mjs +31 -0
  80. package/dist/scene/Scene.d.mts +6 -1
  81. package/dist/scene/Scene.mjs +16 -41
  82. package/dist/scene/assets.d.mts +2 -2
  83. package/dist/scene/assets.mjs +2 -2
  84. package/dist/scene/commands.d.mts +10 -1
  85. package/dist/scene/commands.mjs +42 -2
  86. package/dist/scene/evaluators.mjs +2 -2
  87. package/dist/scene/index.d.mts +2 -2
  88. package/dist/scene/index.mjs +2 -2
  89. package/dist/scene/migrate.mjs +1 -1
  90. package/dist/scene/sims.mjs +1 -1
  91. package/dist/scene/types.d.mts +4 -4
  92. package/dist/sim/particles.d.mts +2 -2
  93. package/dist/sim/rate.d.mts +4 -4
  94. package/dist/sim/rate.mjs +1 -1
  95. package/dist/sim/registry.d.mts +1 -1
  96. package/dist/sim/sampler.d.mts +5 -5
  97. package/dist/sim/thermal.d.mts +3 -3
  98. package/dist/sim/thermal.mjs +2 -2
  99. package/dist/sim/types.d.mts +2 -2
  100. package/dist/sim/wave.d.mts +2 -2
  101. package/dist/sim/wave.mjs +2 -2
  102. package/dist/steps/index.d.mts +1 -1
  103. package/dist/steps/index.mjs +3 -3
  104. package/dist/thermo/index.d.mts +1 -1
  105. package/dist/thermo/index.mjs +1 -1
  106. package/dist/view/Stage.mjs +6 -13
  107. package/dist/view/useInView.mjs +1 -1
  108. package/package.json +37 -25
  109. package/styles.css +141 -1
  110. package/dist/assets/kit/index.mjs +0 -4
@@ -15,6 +15,9 @@ interface SceneProps {
15
15
  /** Author opt-in: start a sim running immediately (ambient motion) instead of
16
16
  * the default "press Play". A SceneDoc with no `meta.sims` is always static. */
17
17
  autoPlay?: boolean;
18
+ /** Optional controlled playback for hosts that provide their own transport. */
19
+ playing?: boolean;
20
+ onPlayingChange?: (playing: boolean) => void;
18
21
  }
19
22
  declare function Scene({
20
23
  doc,
@@ -25,7 +28,9 @@ declare function Scene({
25
28
  height,
26
29
  view,
27
30
  ariaLabel,
28
- autoPlay
31
+ autoPlay,
32
+ playing: playingProp,
33
+ onPlayingChange
29
34
  }: SceneProps): ReactNode;
30
35
  //#endregion
31
36
  export { Scene, SceneProps };
@@ -14,7 +14,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
14
14
 
15
15
  //#region src/scene/Scene.tsx
16
16
  /**
17
- * <Scene> renders a SceneDoc (read/play): resolve() → shared element renderer.
17
+ * <Scene>, renders a SceneDoc (read/play): resolve() → shared element renderer.
18
18
  * Free draggable points commit one `mutate` via onChange on pointer-up; live drag
19
19
  * uses an ephemeral overlay (descendants re-resolve without mutating the doc).
20
20
  */
@@ -45,7 +45,7 @@ function autoView(resolved) {
45
45
  yMax: Math.max(...ys) + pad
46
46
  };
47
47
  }
48
- function Scene({ doc, onChange, interactive = true, showGrid = true, showAxes = true, height, view, ariaLabel = "Interactive figure", autoPlay = false }) {
48
+ function Scene({ doc, onChange, interactive = true, showGrid = true, showAxes = true, height, view, ariaLabel = "Interactive figure", autoPlay = false, playing: playingProp, onPlayingChange }) {
49
49
  const [overlay, setOverlay] = useState(/* @__PURE__ */ new Map());
50
50
  const prevRef = useRef(void 0);
51
51
  const sims = doc.meta?.sims;
@@ -55,14 +55,22 @@ function Scene({ doc, onChange, interactive = true, showGrid = true, showAxes =
55
55
  const [frame, setFrame] = useState(0);
56
56
  const { ref: rootRef, inView } = useInView();
57
57
  const [done, setDone] = useState(false);
58
- const [playing, setPlaying] = useState(autoPlay);
58
+ const [internalPlaying, setInternalPlaying] = useState(autoPlay);
59
+ const playing = playingProp ?? internalPlaying;
60
+ const setPlaying = (next) => {
61
+ if (playingProp === void 0) setInternalPlaying(next);
62
+ onPlayingChange?.(next);
63
+ };
59
64
  useFrameLoop((f) => {
60
65
  if (!sims?.length || !simStates.current) return;
61
66
  const next = stepSims(sims, simStates.current, Math.min(.05, f.dtMs / 1e3));
62
67
  simStates.current = next.states;
63
68
  simScalars.current = next.scalars;
64
69
  setFrame((n) => (n + 1) % 1e6);
65
- if (next.done) setDone(true);
70
+ if (next.done) {
71
+ setDone(true);
72
+ setPlaying(false);
73
+ }
66
74
  }, { running: !!sims?.length && inView && playing && !done });
67
75
  const merged = useMemo(() => {
68
76
  const scalars = simScalars.current;
@@ -95,7 +103,7 @@ function Scene({ doc, onChange, interactive = true, showGrid = true, showAxes =
95
103
  const hasSims = !!sims?.length;
96
104
  return /* @__PURE__ */ jsxs("div", {
97
105
  ref: rootRef,
98
- style: { position: "relative" },
106
+ className: "stage-scene",
99
107
  children: [
100
108
  /* @__PURE__ */ jsxs(Stage, {
101
109
  view: v,
@@ -114,30 +122,9 @@ function Scene({ doc, onChange, interactive = true, showGrid = true, showAxes =
114
122
  type: "button",
115
123
  onClick: () => setPlaying(true),
116
124
  "aria-label": "Play simulation",
117
- style: {
118
- position: "absolute",
119
- inset: 0,
120
- display: "flex",
121
- alignItems: "center",
122
- justifyContent: "center",
123
- background: "color-mix(in oklab, var(--stage-bg) 38%, transparent)",
124
- border: "none",
125
- borderRadius: 12,
126
- cursor: "pointer"
127
- },
125
+ className: "stage-scene__play-overlay",
128
126
  children: /* @__PURE__ */ jsx("span", {
129
- style: {
130
- display: "inline-flex",
131
- alignItems: "center",
132
- gap: 8,
133
- padding: "11px 26px",
134
- borderRadius: 999,
135
- background: "var(--stage-accent)",
136
- color: "#fff",
137
- fontWeight: 800,
138
- fontSize: 16,
139
- boxShadow: "0 6px 18px rgba(0,0,0,.28)"
140
- },
127
+ className: "stage-scene__play-label",
141
128
  children: "▶ Play"
142
129
  })
143
130
  }),
@@ -145,19 +132,7 @@ function Scene({ doc, onChange, interactive = true, showGrid = true, showAxes =
145
132
  type: "button",
146
133
  onClick: () => setPlaying(false),
147
134
  "aria-label": "Pause simulation",
148
- style: {
149
- position: "absolute",
150
- top: 10,
151
- right: 10,
152
- width: 34,
153
- height: 34,
154
- borderRadius: 999,
155
- border: "1px solid var(--stage-grid)",
156
- background: "color-mix(in oklab, var(--stage-bg) 80%, transparent)",
157
- color: "var(--stage-fg)",
158
- cursor: "pointer",
159
- fontSize: 13
160
- },
135
+ className: "stage-scene__pause",
161
136
  children: "⏸"
162
137
  })
163
138
  ]
@@ -21,13 +21,13 @@ declare function registerAsset(name: string, spec: AssetSpec): void;
21
21
  declare function getAsset(name: string): AssetSpec | undefined;
22
22
  declare function listAssets(): string[];
23
23
  /**
24
- * Typed read of an asset's `meta` write `const m = assetMeta<MyMeta>(geom)` in a
24
+ * Typed read of an asset's `meta`, write `const m = assetMeta<MyMeta>(geom)` in a
25
25
  * Component instead of an inline `(geom.meta ?? {}) as MyMeta` cast everywhere.
26
26
  *
27
27
  * We deliberately keep `AssetGeometry` NON-generic: `parts` must stay
28
28
  * `Vec2 | Vec2[] | number` so it remains a clean member of the resolver's `Val`
29
29
  * union, and a fully generic `AssetGeometry<TParts, TMeta>` would force `AssetSpec`,
30
- * `registerAsset`, `resolve()`, and the render path to be generic too and a
30
+ * `registerAsset`, `resolve()`, and the render path to be generic too, and a
31
31
  * specific `TMeta` interface isn't assignable to `Record<string, unknown>` without
32
32
  * index-signature friction. This helper gives the type-safety at the read site
33
33
  * with none of that ripple. (Asset domain data lives in `meta`; `parts` is geometry.)
@@ -12,13 +12,13 @@ function listAssets() {
12
12
  return [...ASSETS.keys()];
13
13
  }
14
14
  /**
15
- * Typed read of an asset's `meta` write `const m = assetMeta<MyMeta>(geom)` in a
15
+ * Typed read of an asset's `meta`, write `const m = assetMeta<MyMeta>(geom)` in a
16
16
  * Component instead of an inline `(geom.meta ?? {}) as MyMeta` cast everywhere.
17
17
  *
18
18
  * We deliberately keep `AssetGeometry` NON-generic: `parts` must stay
19
19
  * `Vec2 | Vec2[] | number` so it remains a clean member of the resolver's `Val`
20
20
  * union, and a fully generic `AssetGeometry<TParts, TMeta>` would force `AssetSpec`,
21
- * `registerAsset`, `resolve()`, and the render path to be generic too and a
21
+ * `registerAsset`, `resolve()`, and the render path to be generic too, and a
22
22
  * specific `TMeta` interface isn't assignable to `Record<string, unknown>` without
23
23
  * index-signature friction. This helper gives the type-safety at the read site
24
24
  * with none of that ripple. (Asset domain data lives in `meta`; `parts` is geometry.)
@@ -41,7 +41,16 @@ declare function collectRemoved(doc: SceneDoc, id: Id): {
41
41
  elements: SceneElement[];
42
42
  bindings: Binding[];
43
43
  };
44
+ /**
45
+ * Validate a command against the current doc; returns an error string, or null if it is safe to
46
+ * apply. The Editor runs this BEFORE mutating, so a bad command (from an agent or a tool) fails
47
+ * loudly with a reason instead of silently corrupting the scene: duplicate ids, missing mutate /
48
+ * remove targets, bindings to elements that do not exist, double binds, unbinding nothing.
49
+ */
50
+ declare function validateCommand(doc: SceneDoc, cmd: Command): string | null;
51
+ /** The element ids a command (or batch) creates, for `CommandResult.createdIds`. */
52
+ declare function createdIds(cmd: Command): Id[];
44
53
  declare function applyCommand(doc: SceneDoc, cmd: Command): SceneDoc;
45
54
  declare function inverse(cmd: Command, prev: SceneDoc): Command;
46
55
  //#endregion
47
- export { Command, CommandResult, MutatePatch, applyCommand, collectRemoved, inverse };
56
+ export { Command, CommandResult, MutatePatch, applyCommand, collectRemoved, createdIds, inverse, validateCommand };
@@ -3,7 +3,7 @@ import { parentIds } from "./schema.mjs";
3
3
 
4
4
  //#region src/scene/commands.ts
5
5
  /**
6
- * Commands JSON, agent-emittable mutations. ALL changes funnel through these
6
+ * Commands, JSON, agent-emittable mutations. ALL changes funnel through these
7
7
  * (builder, agent, undo, control-surface bridge). GeoGebra create-vs-mutate split.
8
8
  * `applyCommand` is a pure reducer; `inverse` produces the undo command, including
9
9
  * a concrete cascading-remove inverse that re-creates descendants + bindings in
@@ -48,6 +48,46 @@ function collectRemoved(doc, id) {
48
48
  bindings: doc.bindings.filter((b) => removed.has(b.from.ref) || removed.has(b.to.ref))
49
49
  };
50
50
  }
51
+ /**
52
+ * Validate a command against the current doc; returns an error string, or null if it is safe to
53
+ * apply. The Editor runs this BEFORE mutating, so a bad command (from an agent or a tool) fails
54
+ * loudly with a reason instead of silently corrupting the scene: duplicate ids, missing mutate /
55
+ * remove targets, bindings to elements that do not exist, double binds, unbinding nothing.
56
+ */
57
+ function validateCommand(doc, cmd) {
58
+ const hasEl = (id) => doc.elements.some((e) => e.id === id);
59
+ switch (cmd.op) {
60
+ case "create":
61
+ if (hasEl(cmd.id)) return `create: element id '${cmd.id}' already exists`;
62
+ if (cmd.element.id !== cmd.id) return `create: element.id '${cmd.element.id}' does not match command id '${cmd.id}'`;
63
+ return null;
64
+ case "mutate": return hasEl(cmd.id) ? null : `mutate: no element '${cmd.id}'`;
65
+ case "remove": return hasEl(cmd.id) ? null : `remove: no element '${cmd.id}'`;
66
+ case "bind": {
67
+ const b = cmd.binding;
68
+ if ((doc.bindings ?? []).some((x) => x.id === b.id)) return `bind: binding id '${b.id}' already exists`;
69
+ if (!hasEl(b.from.ref)) return `bind: 'from' references unknown element '${b.from.ref}'`;
70
+ if (!hasEl(b.to.ref)) return `bind: 'to' references unknown element '${b.to.ref}'`;
71
+ return null;
72
+ }
73
+ case "unbind": return (doc.bindings ?? []).some((b) => b.id === cmd.id) ? null : `unbind: no binding '${cmd.id}'`;
74
+ case "batch": {
75
+ let cur = doc;
76
+ for (const c of cmd.commands) {
77
+ const err = validateCommand(cur, c);
78
+ if (err) return `batch '${cmd.label}': ${err}`;
79
+ cur = applyCommand(cur, c);
80
+ }
81
+ return null;
82
+ }
83
+ }
84
+ }
85
+ /** The element ids a command (or batch) creates, for `CommandResult.createdIds`. */
86
+ function createdIds(cmd) {
87
+ if (cmd.op === "create") return [cmd.id];
88
+ if (cmd.op === "batch") return cmd.commands.flatMap(createdIds);
89
+ return [];
90
+ }
51
91
  function applyCommand(doc, cmd) {
52
92
  switch (cmd.op) {
53
93
  case "create": {
@@ -159,4 +199,4 @@ function inverse(cmd, prev) {
159
199
  }
160
200
 
161
201
  //#endregion
162
- export { applyCommand, collectRemoved, inverse };
202
+ export { applyCommand, collectRemoved, createdIds, inverse, validateCommand };
@@ -8,7 +8,7 @@ import { compileExpr } from "../math/index.mjs";
8
8
  * Evaluator registry, keyed by def.op. Each evaluator turns a derived def into a
9
9
  * Val, reading parent values through `ctx`. Geometry intersection math is ported
10
10
  * from the legacy labs GeometryBoard; intersect picks the solution nearest the previous
11
- * frame's resolved point (continuity never swaps P/Q on drag).
11
+ * frame's resolved point (continuity, never swaps P/Q on drag).
12
12
  */
13
13
  const exprCache = /* @__PURE__ */ new Map();
14
14
  function compiledExpr(src) {
@@ -182,7 +182,7 @@ const EVALUATORS = {
182
182
  expr: (d, ctx) => {
183
183
  const def = d;
184
184
  const compiled = compiledExpr(def.fn);
185
- if (!compiled) return NaN;
185
+ if (!compiled) throw new Error(`expr: cannot parse formula "${def.fn}"`);
186
186
  const scope = {};
187
187
  for (const [name, ref] of Object.entries(def.inputs)) scope[name] = numOf(ctx.get(ref));
188
188
  return compiled.fn(scope);
@@ -1,6 +1,6 @@
1
1
  import { Vec2 } from "../core/vec.mjs";
2
2
  import { 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 } from "./types.mjs";
3
- import { Command, CommandResult, MutatePatch, applyCommand, collectRemoved, inverse } from "./commands.mjs";
3
+ import { Command, CommandResult, MutatePatch, applyCommand, collectRemoved, createdIds, inverse, validateCommand } from "./commands.mjs";
4
4
  import { Resolved, resolve } from "./resolve.mjs";
5
5
  import { OP_REFS, isRef, parentIds } from "./schema.mjs";
6
6
  import { EVALUATORS, EvalCtx } from "./evaluators.mjs";
@@ -10,4 +10,4 @@ import { Scene, SceneProps } from "./Scene.mjs";
10
10
  import { RenderOpts, renderElements } from "./render.mjs";
11
11
  import { AssetResolveArgs, AssetSpec, assetMeta, getAsset, listAssets, registerAsset } from "./assets.mjs";
12
12
  import { SimStates, initSims, stepSims } from "./sims.mjs";
13
- export { type AssetGeometry, type AssetResolveArgs, type AssetSpec, type Binding, CURRENT_SCHEMA_VERSION, type CircleVal, type Command, type CommandResult, type DerivedDef, type DerivedElement, type DerivedKind, EVALUATORS, type ElementBase, type ElementStyle, type EvalCtx, type FreeElement, type FreeNote, type FreePoint, type FreeScalar, type Id, type InstanceState, type LabMeta, type MutatePatch, type NumOrRef, OP_REFS, type Op, type Ref, type RenderOpts, type Resolved, Scene, type SceneDoc, type SceneElement, type SceneMeta, type SceneProps, SceneStore, type Vec2 as SceneVec2, type ShapeLineVal, type SimDecl, type SimStates, type Val, applyCommand, assetMeta, collectRemoved, emptyDoc, getAsset, initSims, inverse, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isRef, isVec2, listAssets, migrate, numOf, parentIds, parse, registerAsset, renderElements, resolve, serialize, stepSims };
13
+ export { type AssetGeometry, type AssetResolveArgs, type AssetSpec, type Binding, CURRENT_SCHEMA_VERSION, type CircleVal, type Command, type CommandResult, type DerivedDef, type DerivedElement, type DerivedKind, EVALUATORS, type ElementBase, type ElementStyle, type EvalCtx, type FreeElement, type FreeNote, type FreePoint, type FreeScalar, type Id, type InstanceState, type LabMeta, type MutatePatch, type NumOrRef, OP_REFS, type Op, type Ref, type RenderOpts, type Resolved, Scene, type SceneDoc, type SceneElement, type SceneMeta, type SceneProps, SceneStore, type Vec2 as SceneVec2, type ShapeLineVal, type SimDecl, type SimStates, type Val, applyCommand, assetMeta, collectRemoved, createdIds, emptyDoc, getAsset, initSims, inverse, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isRef, isVec2, listAssets, migrate, numOf, parentIds, parse, registerAsset, renderElements, resolve, serialize, stepSims, validateCommand };
@@ -3,11 +3,11 @@ import { OP_REFS, isRef, parentIds } from "./schema.mjs";
3
3
  import { assetMeta, getAsset, listAssets, registerAsset } from "./assets.mjs";
4
4
  import { EVALUATORS } from "./evaluators.mjs";
5
5
  import { resolve } from "./resolve.mjs";
6
- import { applyCommand, collectRemoved, inverse } from "./commands.mjs";
6
+ import { applyCommand, collectRemoved, createdIds, inverse, validateCommand } from "./commands.mjs";
7
7
  import { CURRENT_SCHEMA_VERSION, emptyDoc, migrate, parse, serialize } from "./migrate.mjs";
8
8
  import { SceneStore } from "./store.mjs";
9
9
  import { renderElements } from "./render.mjs";
10
10
  import { initSims, stepSims } from "./sims.mjs";
11
11
  import { Scene } from "./Scene.mjs";
12
12
 
13
- export { CURRENT_SCHEMA_VERSION, EVALUATORS, OP_REFS, Scene, SceneStore, applyCommand, assetMeta, collectRemoved, emptyDoc, getAsset, initSims, inverse, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isRef, isVec2, listAssets, migrate, numOf, parentIds, parse, registerAsset, renderElements, resolve, serialize, stepSims };
13
+ export { CURRENT_SCHEMA_VERSION, EVALUATORS, OP_REFS, Scene, SceneStore, applyCommand, assetMeta, collectRemoved, createdIds, emptyDoc, getAsset, initSims, inverse, isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isRef, isVec2, listAssets, migrate, numOf, parentIds, parse, registerAsset, renderElements, resolve, serialize, stepSims, validateCommand };
@@ -9,7 +9,7 @@ function emptyDoc() {
9
9
  };
10
10
  }
11
11
  function migrate(raw) {
12
- if (Array.isArray(raw)) throw new Error("legacy GeoElement[] array use the geoToScene codec (Phase 6)");
12
+ if (Array.isArray(raw)) throw new Error("legacy GeoElement[] array, use the geoToScene codec (Phase 6)");
13
13
  if (!raw || typeof raw !== "object") return emptyDoc();
14
14
  const doc = raw;
15
15
  return {
@@ -10,7 +10,7 @@ import { getSim } from "../sim/registry.mjs";
10
10
  * resolver stays a pure function of its inputs.
11
11
  *
12
12
  * Non-finite outputs (NaN/Inf from a diverging core) are dropped, not propagated
13
- * (graceful failure a bad sim never blanks the canvas).
13
+ * (graceful failure, a bad sim never blanks the canvas).
14
14
  */
15
15
  /** Seed each declared core from its params. */
16
16
  function initSims(decls) {
@@ -165,7 +165,7 @@ interface Binding {
165
165
  props?: Record<string, unknown>;
166
166
  }
167
167
  /**
168
- * Structured pedagogy for a lab first-class learning data, NOT prose buried in
168
+ * Structured pedagogy for a lab, first-class learning data, NOT prose buried in
169
169
  * UI text. Authored alongside the scene, surfaced to the learner seam + xAPI.
170
170
  */
171
171
  interface LabMeta {
@@ -187,7 +187,7 @@ interface LabMeta {
187
187
  }
188
188
  /**
189
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
190
+ * its named output fields drive `scalar` elements (by id) as external inputs, so
191
191
  * the pure resolver re-evaluates with live values without knowing about time.
192
192
  */
193
193
  interface SimDecl {
@@ -201,12 +201,12 @@ interface SimDecl {
201
201
  /** Map: scalar-element id → core-state field that feeds it. */
202
202
  drives: Record<Id, string>;
203
203
  }
204
- /** SceneDoc.meta `pedagogy` + `sims` are typed; the rest stays open for domain use. */
204
+ /** SceneDoc.meta, `pedagogy` + `sims` are typed; the rest stays open for domain use. */
205
205
  type SceneMeta = {
206
206
  pedagogy?: LabMeta;
207
207
  sims?: SimDecl[];
208
208
  } & Record<string, unknown>;
209
- /** Canonical persisted form. `commands[]` is NOT stored history lives in the Editor. */
209
+ /** Canonical persisted form. `commands[]` is NOT stored, history lives in the Editor. */
210
210
  interface SceneDoc {
211
211
  schemaVersion: 2;
212
212
  type: 'stage-scene';
@@ -28,14 +28,14 @@ interface ParticlesState {
28
28
  py: number[];
29
29
  vx: number[];
30
30
  vy: number[];
31
- /** 0 or 1 two populations (for diffusion / colour). */
31
+ /** 0 or 1, two populations (for diffusion / colour). */
32
32
  group: number[];
33
33
  tSec: number;
34
34
  /** Mean kinetic energy ∝ temperature. */
35
35
  temp: number;
36
36
  /** Smoothed pressure (wall-collision momentum per unit time per unit perimeter). */
37
37
  pressure: number;
38
- /** Mixing index 0 (separated) → 1 (uniform) fraction-based, for diffusion. */
38
+ /** Mixing index 0 (separated) → 1 (uniform), fraction-based, for diffusion. */
39
39
  mixed: number;
40
40
  }
41
41
  declare const ParticlesCore: SimCore<ParticlesState, ParticlesParams>;
@@ -8,7 +8,7 @@ interface RateParams {
8
8
  value0?: number;
9
9
  /** Asymptote the value relaxes toward. Default 0 (pure decay). */
10
10
  target?: number;
11
- /** Time constant τ (seconds) the e-folding time. Default 1. */
11
+ /** Time constant τ (seconds), the e-folding time. Default 1. */
12
12
  tau?: number;
13
13
  /** Stop after this many seconds (essentially relaxed) so it doesn't run forever. Default ∞. */
14
14
  maxTime?: number;
@@ -21,15 +21,15 @@ interface RateState {
21
21
  tau: number;
22
22
  /** Seconds elapsed on this core's timeline. */
23
23
  tSec: number;
24
- /** Rolling history of `value`, oldest-first for the curve. */
24
+ /** Rolling history of `value`, oldest-first, for the curve. */
25
25
  samples: number[];
26
26
  cap: number;
27
27
  maxTime: number;
28
- /** True once tSec ≥ maxTime the runtime stops the loop. */
28
+ /** True once tSec ≥ maxTime, the runtime stops the loop. */
29
29
  done: boolean;
30
30
  }
31
31
  declare const RateCore: SimCore<RateState, RateParams>;
32
- /** τ from a half-life (and back) t½ = τ·ln2. */
32
+ /** τ from a half-life (and back), t½ = τ·ln2. */
33
33
  declare const halfLifeToTau: (halfLife: number) => number;
34
34
  declare const tauToHalfLife: (tau: number) => number;
35
35
  //#endregion
package/dist/sim/rate.mjs CHANGED
@@ -31,7 +31,7 @@ const RateCore = {
31
31
  };
32
32
  }
33
33
  };
34
- /** τ from a half-life (and back) t½ = τ·ln2. */
34
+ /** τ from a half-life (and back), t½ = τ·ln2. */
35
35
  const halfLifeToTau = (halfLife) => halfLife / Math.LN2;
36
36
  const tauToHalfLife = (tau) => tau * Math.LN2;
37
37
 
@@ -1,7 +1,7 @@
1
1
  import { SimCore } from "./types.mjs";
2
2
 
3
3
  //#region src/sim/registry.d.ts
4
- /** A core with its specifics erased the runtime reads state fields by name. */
4
+ /** A core with its specifics erased, the runtime reads state fields by name. */
5
5
  type AnySim = SimCore<Record<string, unknown>, Record<string, unknown>>;
6
6
  declare function registerSim<S extends Record<string, unknown>, P>(name: string, core: SimCore<S, P>): void;
7
7
  declare function getSim(name: string): AnySim | undefined;
@@ -8,7 +8,7 @@ interface SamplerParams {
8
8
  perStep?: number;
9
9
  /** PRNG seed. Default 1. */
10
10
  seed?: number;
11
- /** Stop drawing once `n` reaches this a converged sim shouldn't run forever. Default ∞. */
11
+ /** Stop drawing once `n` reaches this, a converged sim shouldn't run forever. Default ∞. */
12
12
  maxDraws?: number;
13
13
  /** History ring-buffer capacity for the headline estimate p[0]. Default 240. */
14
14
  trace?: number;
@@ -20,7 +20,7 @@ interface SamplerState {
20
20
  * Current PRNG state (advances every draw). Deliberately NOT named `seed`: the
21
21
  * Scene runtime merges the SimDecl `params` (which carries the initial `seed`)
22
22
  * onto the state each frame, so an evolving field sharing a param name would be
23
- * clobbered back to its initial value freezing the RNG. Keep evolving state
23
+ * clobbered back to its initial value, freezing the RNG. Keep evolving state
24
24
  * fields disjoint from param names.
25
25
  */
26
26
  rng: number;
@@ -30,16 +30,16 @@ interface SamplerState {
30
30
  counts: number[];
31
31
  /** Running frequency estimate per outcome (counts / n). */
32
32
  p: number[];
33
- /** True (normalized) probabilities for convergence comparison. */
33
+ /** True (normalized) probabilities, for convergence comparison. */
34
34
  p0: number[];
35
35
  /** Index of the most recent draw (-1 before the first). */
36
36
  last: number;
37
- /** Rolling history of p[0], oldest-first for an LLN convergence plot. */
37
+ /** Rolling history of p[0], oldest-first, for an LLN convergence plot. */
38
38
  samples: number[];
39
39
  cap: number;
40
40
  /** Draw cap; step() becomes a no-op once n reaches it. */
41
41
  maxDraws: number;
42
- /** True once n ≥ maxDraws the runtime stops the loop. */
42
+ /** True once n ≥ maxDraws, the runtime stops the loop. */
43
43
  done: boolean;
44
44
  }
45
45
  declare const SamplerCore: SimCore<SamplerState, SamplerParams>;
@@ -21,10 +21,10 @@ interface Substance {
21
21
  /** Liquid colour token (for the skin). */
22
22
  color: string;
23
23
  }
24
- /** Real water the canonical example (note: latent vaporisation ≫ everything,
24
+ /** Real water, the canonical example (note: latent vaporisation ≫ everything,
25
25
  * so the boiling plateau dwarfs the rest, which is exactly the lesson). */
26
26
  declare const WATER: Substance;
27
- /** Ethanol lower boiling point, smaller latent heats; a contrasting curve. */
27
+ /** Ethanol, lower boiling point, smaller latent heats; a contrasting curve. */
28
28
  declare const ETHANOL: Substance;
29
29
  type Phase = 'solid' | 'melting' | 'liquid' | 'boiling' | 'gas';
30
30
  /** One run of the energy ladder. `kind:'heat'` is a sloped temperature rise
@@ -58,7 +58,7 @@ interface ThermalState {
58
58
  powerW: number;
59
59
  tStart: number;
60
60
  tMax: number;
61
- /** The full energy ladder the heating curve, as data. */
61
+ /** The full energy ladder, the heating curve, as data. */
62
62
  segs: ThermalSeg[];
63
63
  /** Total heat to walk the whole ladder, J. */
64
64
  totalJ: number;
@@ -1,6 +1,6 @@
1
1
  //#region src/sim/thermal.ts
2
2
  const clamp01 = (v) => Math.max(0, Math.min(1, v));
3
- /** Real water the canonical example (note: latent vaporisation ≫ everything,
3
+ /** Real water, the canonical example (note: latent vaporisation ≫ everything,
4
4
  * so the boiling plateau dwarfs the rest, which is exactly the lesson). */
5
5
  const WATER = {
6
6
  name: "Water",
@@ -16,7 +16,7 @@ const WATER = {
16
16
  tBoil: 100,
17
17
  color: "var(--stage-accent, #3b82f6)"
18
18
  };
19
- /** Ethanol lower boiling point, smaller latent heats; a contrasting curve. */
19
+ /** Ethanol, lower boiling point, smaller latent heats; a contrasting curve. */
20
20
  const ETHANOL = {
21
21
  name: "Ethanol",
22
22
  solidName: "solid",
@@ -1,12 +1,12 @@
1
1
  //#region src/sim/types.d.ts
2
2
  /**
3
- * Sim cores the "physics engine" half of a lab: pure, time-stepped simulators
3
+ * Sim cores, the "physics engine" half of a lab: pure, time-stepped simulators
4
4
  * with NO React and NO pixels. A core is `reset(params) → state`, then
5
5
  * `step(state, dt) → state` once per frame. Because a core is a pure function of
6
6
  * (params, elapsed time), a lab built on one is deterministic: it replays
7
7
  * identically under a Remotion fixed-frame clock (→ video) and is unit-testable
8
8
  * with no DOM. Rendering (glyphs) READS the returned state; drawing never lives
9
- * in here. Live controls write their target fields onto `state` between steps
9
+ * in here. Live controls write their target fields onto `state` between steps ,
10
10
  * so a core's mutable inputs (e.g. `volts`, `mode`) live IN its state, which is
11
11
  * also why those fields are readable by name from a binding.
12
12
  */
@@ -22,9 +22,9 @@ interface WaveState {
22
22
  tSec: number;
23
23
  /** Instantaneous signed voltage; sign = current direction. */
24
24
  v: number;
25
- /** ∫v·dt accumulated charge displacement; what flow position (electrons / water) reads. */
25
+ /** ∫v·dt, accumulated charge displacement; what flow position (electrons / water) reads. */
26
26
  charge: number;
27
- /** Rolling history of `v`, oldest-first, length ≤ `cap` for the live trace. */
27
+ /** Rolling history of `v`, oldest-first, length ≤ `cap`, for the live trace. */
28
28
  samples: number[];
29
29
  /** Fixed capacity of `samples`. */
30
30
  cap: number;
package/dist/sim/wave.mjs CHANGED
@@ -2,9 +2,9 @@ import { oscillate } from "../core/motion.mjs";
2
2
 
3
3
  //#region src/sim/wave.ts
4
4
  /**
5
- * `wave` the AC/DC signal source. One core drives a whole scene: a DC source
5
+ * `wave`, the AC/DC signal source. One core drives a whole scene: a DC source
6
6
  * holds a steady voltage; an AC source oscillates. Either way it exposes the
7
- * instantaneous voltage `v` (signed sign IS current direction) plus a rolling
7
+ * instantaneous voltage `v` (signed, sign IS current direction) plus a rolling
8
8
  * `samples` buffer for a live scrolling trace. The SAME `v` can skin many ways at
9
9
  * once (a lamp's brightness, electrons in a wire, water sloshing in a pipe), which
10
10
  * is the point: sim ≠ render, so one signal feeds every visual metaphor in sync.
@@ -1,7 +1,7 @@
1
1
  import { ReactNode } from "react";
2
2
 
3
3
  //#region src/steps/index.d.ts
4
- /** One step in the sequence identity + gating only. Its TITLE, PROSE, and the
4
+ /** One step in the sequence, identity + gating only. Its TITLE, PROSE, and the
5
5
  * interactive itself live in the authored Section block, not here. */
6
6
  interface StepSpec {
7
7
  id: string;
@@ -5,14 +5,14 @@ import { createContext, createElement, useContext, useMemo, useRef, useSyncExter
5
5
 
6
6
  //#region src/steps/index.ts
7
7
  /**
8
- * Step progress + gating the runtime that turns an ordered sequence of lesson
8
+ * Step progress + gating, the runtime that turns an ordered sequence of lesson
9
9
  * STEPS into "unlock the next when this one is solved" + aggregated completion.
10
10
  *
11
- * ONE SOURCE OF TRUTH for the step sequence is the authored content itself the
11
+ * ONE SOURCE OF TRUTH for the step sequence is the authored content itself, the
12
12
  * @classytic/cms-ui `Section`/`Step` blocks a creator orders in the editor
13
13
  * (serialized in the MDX). This engine deliberately does NOT model an authored
14
14
  * "LessonDoc": that would be a SECOND, competing definition of the same steps
15
- * (and creators never hand-write one they build Section blocks). The host's
15
+ * (and creators never hand-write one, they build Section blocks). The host's
16
16
  * Section player derives a `StepSpec[]` from the section order, wraps each
17
17
  * section's interactive in `<StepScope id>`, and reads `useStepProgress` to gate
18
18
  * reveal. Each lab's `useLearner().report` flows into step completion AND
@@ -1,6 +1,6 @@
1
1
  //#region src/thermo/index.d.ts
2
2
  /**
3
- * `thermo` ideal-gas thermodynamic processes, as a PURE kernel (like `field`:
3
+ * `thermo`, ideal-gas thermodynamic processes, as a PURE kernel (like `field`:
4
4
  * no React, no pixels, no time-stepping). A lab drives it with a slider/progress
5
5
  * fraction; the kernel returns the full P–V path of a quasi-static process plus the
6
6
  * first-law bookkeeping along it (work, heat, internal energy, entropy).
@@ -1,6 +1,6 @@
1
1
  //#region src/thermo/index.ts
2
2
  /**
3
- * `thermo` ideal-gas thermodynamic processes, as a PURE kernel (like `field`:
3
+ * `thermo`, ideal-gas thermodynamic processes, as a PURE kernel (like `field`:
4
4
  * no React, no pixels, no time-stepping). A lab drives it with a slider/progress
5
5
  * fraction; the kernel returns the full P–V path of a quasi-static process plus the
6
6
  * first-law bookkeeping along it (work, heat, internal energy, entropy).
@@ -8,10 +8,10 @@ import { jsx, jsxs } from "react/jsx-runtime";
8
8
 
9
9
  //#region src/view/Stage.tsx
10
10
  /**
11
- * <Stage> the SVG root. SSR-safe: builds a CoordinateSystem from explicit
11
+ * <Stage>, the SVG root. SSR-safe: builds a CoordinateSystem from explicit
12
12
  * width/height/aspectRatio (or a default) so children render on first paint;
13
13
  * after mount a ResizeObserver swaps in the measured width. There is ONE content
14
- * group with no transform every primitive self-positions via coords.toPx().
14
+ * group with no transform, every primitive self-positions via coords.toPx().
15
15
  * A pixel-space overlay group hosts selection halos/handles (builder phase).
16
16
  */
17
17
  const DEFAULT_VIEW = {
@@ -31,7 +31,7 @@ function Stage({ view = DEFAULT_VIEW, width, height, aspectRatio, pad = 12, pres
31
31
  }), [
32
32
  w,
33
33
  h,
34
- view,
34
+ `${view.xMin} ${view.xMax} ${view.yMin} ${view.yMax}`,
35
35
  pad,
36
36
  preserveAspect
37
37
  ]);
@@ -43,8 +43,7 @@ function Stage({ view = DEFAULT_VIEW, width, height, aspectRatio, pad = 12, pres
43
43
  } : void 0;
44
44
  return /* @__PURE__ */ jsx("div", {
45
45
  ref: wrapRef,
46
- className,
47
- style: { width: "100%" },
46
+ className: ["stage-root", className].filter(Boolean).join(" "),
48
47
  children: /* @__PURE__ */ jsxs("svg", {
49
48
  ref: svgRef,
50
49
  width: w,
@@ -53,14 +52,8 @@ function Stage({ view = DEFAULT_VIEW, width, height, aspectRatio, pad = 12, pres
53
52
  role: "group",
54
53
  "aria-label": ariaLabel,
55
54
  onPointerDown: handlePointer,
56
- style: {
57
- display: "block",
58
- background,
59
- borderRadius: 12,
60
- border: "1px solid var(--stage-grid)",
61
- userSelect: "none",
62
- touchAction: "none"
63
- },
55
+ className: "stage-canvas",
56
+ style: { background },
64
57
  children: [/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("marker", {
65
58
  id: "stage-arrow",
66
59
  viewBox: "0 0 10 10",
@@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react";
4
4
 
5
5
  //#region src/view/useInView.ts
6
6
  /**
7
- * useInView pause work when a lab scrolls off-screen. Sim labs drive an
7
+ * useInView, pause work when a lab scrolls off-screen. Sim labs drive an
8
8
  * unbounded requestAnimationFrame loop; without this, every sim on a page runs
9
9
  * forever even when nobody can see it (and a gallery of labs pins the CPU). Gate
10
10
  * `useFrameLoop({ running: inView })` on this. SSR-safe; if IntersectionObserver