@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,47 @@
1
+ import { Binding, DerivedDef, ElementStyle, Id, SceneDoc, SceneElement } from "./types.mjs";
2
+
3
+ //#region src/scene/commands.d.ts
4
+ interface MutatePatch {
5
+ free?: Record<string, unknown>;
6
+ def?: Partial<DerivedDef>;
7
+ style?: ElementStyle;
8
+ label?: string;
9
+ }
10
+ type Command = {
11
+ op: 'create';
12
+ id: Id;
13
+ element: SceneElement;
14
+ index?: number;
15
+ } | {
16
+ op: 'mutate';
17
+ id: Id;
18
+ patch: MutatePatch;
19
+ } | {
20
+ op: 'bind';
21
+ binding: Binding;
22
+ } | {
23
+ op: 'unbind';
24
+ id: Id;
25
+ } | {
26
+ op: 'remove';
27
+ id: Id;
28
+ } | {
29
+ op: 'batch';
30
+ label: string;
31
+ commands: Command[];
32
+ };
33
+ interface CommandResult {
34
+ ok: boolean;
35
+ createdIds?: Id[];
36
+ error?: string;
37
+ }
38
+ /** Elements transitively depending on `id` (plus `id` itself), in document order
39
+ * (= creation order, parents before children), and the bindings touching them. */
40
+ declare function collectRemoved(doc: SceneDoc, id: Id): {
41
+ elements: SceneElement[];
42
+ bindings: Binding[];
43
+ };
44
+ declare function applyCommand(doc: SceneDoc, cmd: Command): SceneDoc;
45
+ declare function inverse(cmd: Command, prev: SceneDoc): Command;
46
+ //#endregion
47
+ export { Command, CommandResult, MutatePatch, applyCommand, collectRemoved, inverse };
@@ -0,0 +1,162 @@
1
+ import { isDerived, isFree } from "./types.mjs";
2
+ import { parentIds } from "./schema.mjs";
3
+
4
+ //#region src/scene/commands.ts
5
+ /**
6
+ * Commands — JSON, agent-emittable mutations. ALL changes funnel through these
7
+ * (builder, agent, undo, control-surface bridge). GeoGebra create-vs-mutate split.
8
+ * `applyCommand` is a pure reducer; `inverse` produces the undo command, including
9
+ * a concrete cascading-remove inverse that re-creates descendants + bindings in
10
+ * dependency order.
11
+ */
12
+ function mutateElement(el, patch) {
13
+ const next = { ...el };
14
+ if (patch.label !== void 0) next.label = patch.label;
15
+ if (patch.style) next.style = {
16
+ ...el.style,
17
+ ...patch.style
18
+ };
19
+ if (patch.free && isFree(next) && isFree(el)) next.free = {
20
+ ...el.free,
21
+ ...patch.free
22
+ };
23
+ if (patch.def && isDerived(next) && isDerived(el)) next.def = {
24
+ ...el.def,
25
+ ...patch.def
26
+ };
27
+ return next;
28
+ }
29
+ /** Elements transitively depending on `id` (plus `id` itself), in document order
30
+ * (= creation order, parents before children), and the bindings touching them. */
31
+ function collectRemoved(doc, id) {
32
+ const removed = new Set([id]);
33
+ let changed = true;
34
+ while (changed) {
35
+ changed = false;
36
+ for (const el of doc.elements) {
37
+ if (removed.has(el.id)) continue;
38
+ const ps = parentIds(el);
39
+ const boundTo = doc.bindings.filter((b) => b.from.ref === el.id).map((b) => b.to.ref);
40
+ if ([...ps, ...boundTo].some((p) => removed.has(p))) {
41
+ removed.add(el.id);
42
+ changed = true;
43
+ }
44
+ }
45
+ }
46
+ return {
47
+ elements: doc.elements.filter((e) => removed.has(e.id)),
48
+ bindings: doc.bindings.filter((b) => removed.has(b.from.ref) || removed.has(b.to.ref))
49
+ };
50
+ }
51
+ function applyCommand(doc, cmd) {
52
+ switch (cmd.op) {
53
+ case "create": {
54
+ if (cmd.index == null) return {
55
+ ...doc,
56
+ elements: [...doc.elements, cmd.element]
57
+ };
58
+ const els = doc.elements.slice();
59
+ els.splice(cmd.index, 0, cmd.element);
60
+ return {
61
+ ...doc,
62
+ elements: els
63
+ };
64
+ }
65
+ case "mutate": return {
66
+ ...doc,
67
+ elements: doc.elements.map((el) => el.id === cmd.id ? mutateElement(el, cmd.patch) : el)
68
+ };
69
+ case "bind": return {
70
+ ...doc,
71
+ bindings: [...doc.bindings, cmd.binding]
72
+ };
73
+ case "unbind": return {
74
+ ...doc,
75
+ bindings: doc.bindings.filter((b) => b.id !== cmd.id)
76
+ };
77
+ case "remove": {
78
+ const { elements, bindings } = collectRemoved(doc, cmd.id);
79
+ const elIds = new Set(elements.map((e) => e.id));
80
+ const bIds = new Set(bindings.map((b) => b.id));
81
+ return {
82
+ ...doc,
83
+ elements: doc.elements.filter((e) => !elIds.has(e.id)),
84
+ bindings: doc.bindings.filter((b) => !bIds.has(b.id))
85
+ };
86
+ }
87
+ case "batch": return cmd.commands.reduce(applyCommand, doc);
88
+ }
89
+ }
90
+ function inverse(cmd, prev) {
91
+ switch (cmd.op) {
92
+ case "create": return {
93
+ op: "remove",
94
+ id: cmd.id
95
+ };
96
+ case "mutate": {
97
+ const el = prev.elements.find((e) => e.id === cmd.id);
98
+ const patch = {};
99
+ if (el) {
100
+ if (cmd.patch.label !== void 0) patch.label = el.label ?? "";
101
+ if (cmd.patch.style) patch.style = el.style ?? {};
102
+ if (cmd.patch.free && isFree(el)) patch.free = { ...el.free };
103
+ if (cmd.patch.def && isDerived(el)) patch.def = { ...el.def };
104
+ }
105
+ return {
106
+ op: "mutate",
107
+ id: cmd.id,
108
+ patch
109
+ };
110
+ }
111
+ case "bind": return {
112
+ op: "unbind",
113
+ id: cmd.binding.id
114
+ };
115
+ case "unbind": {
116
+ const b = prev.bindings.find((x) => x.id === cmd.id);
117
+ if (!b) return {
118
+ op: "batch",
119
+ label: "noop",
120
+ commands: []
121
+ };
122
+ return {
123
+ op: "bind",
124
+ binding: b
125
+ };
126
+ }
127
+ case "remove": {
128
+ const { elements, bindings } = collectRemoved(prev, cmd.id);
129
+ return {
130
+ op: "batch",
131
+ label: "undo remove",
132
+ commands: [...elements.map((element) => ({
133
+ op: "create",
134
+ id: element.id,
135
+ element,
136
+ index: prev.elements.indexOf(element)
137
+ })), ...bindings.map((binding) => ({
138
+ op: "bind",
139
+ binding
140
+ }))]
141
+ };
142
+ }
143
+ case "batch": {
144
+ const inverses = [];
145
+ let cursor = prev;
146
+ const states = [];
147
+ for (const c of cmd.commands) {
148
+ states.push(cursor);
149
+ cursor = applyCommand(cursor, c);
150
+ }
151
+ for (let i = cmd.commands.length - 1; i >= 0; i--) inverses.push(inverse(cmd.commands[i], states[i]));
152
+ return {
153
+ op: "batch",
154
+ label: `undo ${cmd.label}`,
155
+ commands: inverses
156
+ };
157
+ }
158
+ }
159
+ }
160
+
161
+ //#endregion
162
+ export { applyCommand, collectRemoved, inverse };
@@ -0,0 +1,17 @@
1
+ import { Vec2 } from "../core/vec.mjs";
2
+ import { DerivedDef, Id, NumOrRef, Ref, Val } from "./types.mjs";
3
+
4
+ //#region src/scene/evaluators.d.ts
5
+ interface EvalCtx {
6
+ get(r: Ref): Val | undefined;
7
+ vec(r: Ref): Vec2 | undefined;
8
+ num(x: NumOrRef): number;
9
+ prevVal(id: Id): Val | undefined;
10
+ /** Read a live sim-state field (external input, like a drag overlay). */
11
+ simField(sim: Id, field: string): unknown;
12
+ selfId: Id;
13
+ }
14
+ type Evaluator = (def: DerivedDef, ctx: EvalCtx) => Val;
15
+ declare const EVALUATORS: Record<string, Evaluator>;
16
+ //#endregion
17
+ export { EVALUATORS, EvalCtx };
@@ -0,0 +1,210 @@
1
+ import { vec } from "../core/vec.mjs";
2
+ import { isCircleVal, isLineVal, isVec2, numOf } from "./types.mjs";
3
+ import { getAsset } from "./assets.mjs";
4
+ import { compileExpr } from "../math/index.mjs";
5
+
6
+ //#region src/scene/evaluators.ts
7
+ /**
8
+ * Evaluator registry, keyed by def.op. Each evaluator turns a derived def into a
9
+ * Val, reading parent values through `ctx`. Geometry intersection math is ported
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).
12
+ */
13
+ const exprCache = /* @__PURE__ */ new Map();
14
+ function compiledExpr(src) {
15
+ if (exprCache.has(src)) return exprCache.get(src) ?? null;
16
+ const r = compileExpr(src);
17
+ const compiled = "fn" in r && typeof r.fn === "function" ? r : null;
18
+ exprCache.set(src, compiled);
19
+ return compiled;
20
+ }
21
+ const dist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
22
+ function intersectCircleCircle(A, B) {
23
+ const d = dist(A.center, B.center);
24
+ if (d < 1e-9 || d > A.r + B.r || d < Math.abs(A.r - B.r)) return [];
25
+ const a = (d * d + A.r * A.r - B.r * B.r) / (2 * d);
26
+ const h2 = A.r * A.r - a * a;
27
+ if (h2 < 0) return [];
28
+ const h = Math.sqrt(h2);
29
+ const ux = (B.center.x - A.center.x) / d;
30
+ const uy = (B.center.y - A.center.y) / d;
31
+ const mx = A.center.x + a * ux;
32
+ const my = A.center.y + a * uy;
33
+ return [{
34
+ x: mx - h * uy,
35
+ y: my + h * ux
36
+ }, {
37
+ x: mx + h * uy,
38
+ y: my - h * ux
39
+ }];
40
+ }
41
+ function intersectLineLine(L1, L2) {
42
+ const { a: p1, b: p2 } = L1;
43
+ const { a: p3, b: p4 } = L2;
44
+ const d = (p1.x - p2.x) * (p3.y - p4.y) - (p1.y - p2.y) * (p3.x - p4.x);
45
+ if (Math.abs(d) < 1e-9) return [];
46
+ const a = p1.x * p2.y - p1.y * p2.x;
47
+ const b = p3.x * p4.y - p3.y * p4.x;
48
+ return [{
49
+ x: (a * (p3.x - p4.x) - (p1.x - p2.x) * b) / d,
50
+ y: (a * (p3.y - p4.y) - (p1.y - p2.y) * b) / d
51
+ }];
52
+ }
53
+ function intersectLineCircle(L, C) {
54
+ const dx = L.b.x - L.a.x;
55
+ const dy = L.b.y - L.a.y;
56
+ const fx = L.a.x - C.center.x;
57
+ const fy = L.a.y - C.center.y;
58
+ const a = dx * dx + dy * dy;
59
+ const b = 2 * (fx * dx + fy * dy);
60
+ const c = fx * fx + fy * fy - C.r * C.r;
61
+ const disc = b * b - 4 * a * c;
62
+ if (disc < 0 || a < 1e-12) return [];
63
+ const s = Math.sqrt(disc);
64
+ const t1 = (-b - s) / (2 * a);
65
+ const t2 = (-b + s) / (2 * a);
66
+ return [{
67
+ x: L.a.x + t1 * dx,
68
+ y: L.a.y + t1 * dy
69
+ }, {
70
+ x: L.a.x + t2 * dx,
71
+ y: L.a.y + t2 * dy
72
+ }];
73
+ }
74
+ function need(v, msg) {
75
+ if (v === void 0) throw new Error(msg);
76
+ return v;
77
+ }
78
+ const EVALUATORS = {
79
+ midpoint: (d, ctx) => {
80
+ const def = d;
81
+ return vec.midpoint(need(ctx.vec(def.of[0]), "midpoint: parent A missing"), need(ctx.vec(def.of[1]), "midpoint: parent B missing"));
82
+ },
83
+ circle: (d, ctx) => {
84
+ const def = d;
85
+ const center = need(ctx.vec(def.center), "circle: center missing");
86
+ return {
87
+ kind: "circle",
88
+ center,
89
+ r: def.radius != null ? def.radius : def.through ? dist(center, need(ctx.vec(def.through), "circle: through missing")) : 1
90
+ };
91
+ },
92
+ line: (d, ctx) => {
93
+ const def = d;
94
+ return {
95
+ kind: "line",
96
+ a: need(ctx.vec(def.through[0]), "line: A missing"),
97
+ b: need(ctx.vec(def.through[1]), "line: B missing")
98
+ };
99
+ },
100
+ segment: (d, ctx) => {
101
+ const def = d;
102
+ return {
103
+ kind: "segment",
104
+ a: need(ctx.vec(def.from), "segment: from missing"),
105
+ b: need(ctx.vec(def.to), "segment: to missing")
106
+ };
107
+ },
108
+ polygon: (d, ctx) => {
109
+ return d.vertices.map((r, i) => need(ctx.vec(r), `polygon: vertex ${i} missing`));
110
+ },
111
+ intersect: (d, ctx) => {
112
+ const def = d;
113
+ const v1 = ctx.get(def.of[0]);
114
+ const v2 = ctx.get(def.of[1]);
115
+ let sols = [];
116
+ if (isCircleVal(v1) && isCircleVal(v2)) sols = intersectCircleCircle(v1, v2);
117
+ else if (isLineVal(v1) && isLineVal(v2)) sols = intersectLineLine(v1, v2);
118
+ else if (isLineVal(v1) && isCircleVal(v2)) sols = intersectLineCircle(v1, v2);
119
+ else if (isCircleVal(v1) && isLineVal(v2)) sols = intersectLineCircle(v2, v1);
120
+ if (!sols.length) throw new Error("intersect: no solution");
121
+ const prev = ctx.prevVal(ctx.selfId);
122
+ if (isVec2(prev)) return sols.reduce((best, s) => dist(s, prev) < dist(best, prev) ? s : best, sols[0]);
123
+ return sols[def.pick ?? 0] ?? sols[0];
124
+ },
125
+ distance: (d, ctx) => {
126
+ const def = d;
127
+ return dist(need(ctx.vec(def.of[0]), "distance: A missing"), need(ctx.vec(def.of[1]), "distance: B missing"));
128
+ },
129
+ angle: (d, ctx) => {
130
+ const def = d;
131
+ const a = need(ctx.vec(def.of[0]), "angle: A");
132
+ const o = need(ctx.vec(def.of[1]), "angle: vertex");
133
+ const b = need(ctx.vec(def.of[2]), "angle: C");
134
+ const u = vec.sub(a, o);
135
+ const w = vec.sub(b, o);
136
+ const cos = vec.dot(u, w) / ((vec.mag(u) || 1) * (vec.mag(w) || 1));
137
+ return Math.acos(Math.max(-1, Math.min(1, cos))) * (180 / Math.PI);
138
+ },
139
+ linop: (d, ctx) => {
140
+ const def = d;
141
+ return def.terms.reduce((sum, t) => sum + t.coef * numOf(ctx.get(t.in)), def.const ?? 0);
142
+ },
143
+ clamp: (d, ctx) => {
144
+ const def = d;
145
+ return Math.max(def.min, Math.min(def.max, numOf(ctx.get(def.in))));
146
+ },
147
+ lerp: (d, ctx) => {
148
+ const def = d;
149
+ const a = numOf(ctx.get(def.a));
150
+ return a + (numOf(ctx.get(def.b)) - a) * ctx.num(def.t);
151
+ },
152
+ reduce: (d, ctx) => {
153
+ const def = d;
154
+ const xs = def.inputs.map((r) => numOf(ctx.get(r)));
155
+ if (!xs.length) return 0;
156
+ if (def.reducer === "sum") return xs.reduce((a, b) => a + b, 0);
157
+ if (def.reducer === "mean") return xs.reduce((a, b) => a + b, 0) / xs.length;
158
+ if (def.reducer === "max") return Math.max(...xs);
159
+ return Math.min(...xs);
160
+ },
161
+ product: (d, ctx) => {
162
+ const def = d;
163
+ return numOf(ctx.get(def.of[0])) * numOf(ctx.get(def.of[1]));
164
+ },
165
+ compare: (d, ctx) => {
166
+ const def = d;
167
+ const a = numOf(ctx.get(def.a));
168
+ const b = ctx.num(def.b);
169
+ const eps = def.eps ?? 0;
170
+ switch (def.cmp) {
171
+ case "lt": return a < b - eps;
172
+ case "le": return a <= b + eps;
173
+ case "eq": return Math.abs(a - b) <= eps;
174
+ case "ge": return a >= b - eps;
175
+ case "gt": return a > b + eps;
176
+ }
177
+ },
178
+ select: (d, ctx) => {
179
+ const def = d;
180
+ return numOf(ctx.get(def.cond)) !== 0 ? ctx.num(def.then) : ctx.num(def.else);
181
+ },
182
+ expr: (d, ctx) => {
183
+ const def = d;
184
+ const compiled = compiledExpr(def.fn);
185
+ if (!compiled) return NaN;
186
+ const scope = {};
187
+ for (const [name, ref] of Object.entries(def.inputs)) scope[name] = numOf(ctx.get(ref));
188
+ return compiled.fn(scope);
189
+ },
190
+ asset: (d, ctx) => {
191
+ const def = d;
192
+ const spec = getAsset(def.asset);
193
+ if (!spec) throw new Error(`asset '${def.asset}' not registered`);
194
+ const bound = {};
195
+ for (const [k, ref] of Object.entries(def.bind)) bound[k] = ctx.get(ref);
196
+ let sim;
197
+ if (def.simBind) {
198
+ sim = {};
199
+ for (const [k, b] of Object.entries(def.simBind)) sim[k] = ctx.simField(b.sim, b.field);
200
+ }
201
+ return spec.resolver({
202
+ params: def.params,
203
+ bound,
204
+ sim
205
+ });
206
+ }
207
+ };
208
+
209
+ //#endregion
210
+ export { EVALUATORS };
@@ -0,0 +1,13 @@
1
+ import { Vec2 } from "../core/vec.mjs";
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";
4
+ import { Resolved, resolve } from "./resolve.mjs";
5
+ import { OP_REFS, isRef, parentIds } from "./schema.mjs";
6
+ import { EVALUATORS, EvalCtx } from "./evaluators.mjs";
7
+ import { CURRENT_SCHEMA_VERSION, emptyDoc, migrate, parse, serialize } from "./migrate.mjs";
8
+ import { InstanceState, SceneStore } from "./store.mjs";
9
+ import { Scene, SceneProps } from "./Scene.mjs";
10
+ import { RenderOpts, renderElements } from "./render.mjs";
11
+ import { AssetResolveArgs, AssetSpec, assetMeta, getAsset, listAssets, registerAsset } from "./assets.mjs";
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 };
@@ -0,0 +1,13 @@
1
+ import { isAssetGeom, isCircleVal, isDerived, isFree, isLineVal, isVec2, numOf } from "./types.mjs";
2
+ import { OP_REFS, isRef, parentIds } from "./schema.mjs";
3
+ import { assetMeta, getAsset, listAssets, registerAsset } from "./assets.mjs";
4
+ import { EVALUATORS } from "./evaluators.mjs";
5
+ import { resolve } from "./resolve.mjs";
6
+ import { applyCommand, collectRemoved, inverse } from "./commands.mjs";
7
+ import { CURRENT_SCHEMA_VERSION, emptyDoc, migrate, parse, serialize } from "./migrate.mjs";
8
+ import { SceneStore } from "./store.mjs";
9
+ import { renderElements } from "./render.mjs";
10
+ import { initSims, stepSims } from "./sims.mjs";
11
+ import { Scene } from "./Scene.mjs";
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 };
@@ -0,0 +1,11 @@
1
+ import { SceneDoc } from "./types.mjs";
2
+
3
+ //#region src/scene/migrate.d.ts
4
+ declare const CURRENT_SCHEMA_VERSION: 2;
5
+ declare function emptyDoc(): SceneDoc;
6
+ declare function migrate(raw: unknown): SceneDoc;
7
+ /** Stable JSON serialize (sorted nowhere needed yet; element order is meaningful). */
8
+ declare function serialize(doc: SceneDoc): string;
9
+ declare function parse(json: string): SceneDoc;
10
+ //#endregion
11
+ export { CURRENT_SCHEMA_VERSION, emptyDoc, migrate, parse, serialize };
@@ -0,0 +1,35 @@
1
+ //#region src/scene/migrate.ts
2
+ const CURRENT_SCHEMA_VERSION = 2;
3
+ function emptyDoc() {
4
+ return {
5
+ schemaVersion: 2,
6
+ type: "stage-scene",
7
+ elements: [],
8
+ bindings: []
9
+ };
10
+ }
11
+ function migrate(raw) {
12
+ if (Array.isArray(raw)) throw new Error("legacy GeoElement[] array — use the geoToScene codec (Phase 6)");
13
+ if (!raw || typeof raw !== "object") return emptyDoc();
14
+ const doc = raw;
15
+ return {
16
+ schemaVersion: 2,
17
+ type: "stage-scene",
18
+ view: doc.view,
19
+ width: doc.width,
20
+ height: doc.height,
21
+ elements: doc.elements ?? [],
22
+ bindings: doc.bindings ?? [],
23
+ meta: doc.meta
24
+ };
25
+ }
26
+ /** Stable JSON serialize (sorted nowhere needed yet; element order is meaningful). */
27
+ function serialize(doc) {
28
+ return JSON.stringify(doc);
29
+ }
30
+ function parse(json) {
31
+ return migrate(JSON.parse(json));
32
+ }
33
+
34
+ //#endregion
35
+ export { CURRENT_SCHEMA_VERSION, emptyDoc, migrate, parse, serialize };
@@ -0,0 +1,14 @@
1
+ import { Vec2 } from "../core/vec.mjs";
2
+ import { Id, SceneDoc } from "./types.mjs";
3
+ import { Resolved } from "./resolve.mjs";
4
+ import { ReactNode } from "react";
5
+
6
+ //#region src/scene/render.d.ts
7
+ interface RenderOpts {
8
+ draggablePoints: boolean;
9
+ onPointMove?: (id: Id, p: Vec2, phase: 'move' | 'commit') => void;
10
+ selection?: Id | null;
11
+ }
12
+ declare function renderElements(doc: SceneDoc, resolved: Resolved, opts: RenderOpts): ReactNode;
13
+ //#endregion
14
+ export { RenderOpts, renderElements };
@@ -0,0 +1,126 @@
1
+ 'use client';
2
+
3
+ import { vec } from "../core/vec.mjs";
4
+ import { Point } from "../primitives/Dot.mjs";
5
+ import { Line, Segment } from "../primitives/Lines.mjs";
6
+ import { Circle, Polygon } from "../primitives/Shapes.mjs";
7
+ import { Label } from "../primitives/Label.mjs";
8
+ import { MovableDot } from "../interaction/MovableDot.mjs";
9
+ import { isAssetGeom, isCircleVal, isDerived, isLineVal, isVec2 } from "./types.mjs";
10
+ import { getAsset } from "./assets.mjs";
11
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
12
+
13
+ //#region src/scene/render.tsx
14
+ function parentRef(el, i) {
15
+ if (!isDerived(el)) return "";
16
+ const def = el.def;
17
+ if (def.op === "distance" || def.op === "angle") return def.of[i]?.ref ?? "";
18
+ return "";
19
+ }
20
+ function renderElements(doc, resolved, opts) {
21
+ const shapes = [];
22
+ const overlays = [];
23
+ const point = (ref) => {
24
+ const v = resolved.values.get(ref);
25
+ return isVec2(v) ? v : void 0;
26
+ };
27
+ for (const el of doc.elements) {
28
+ if (el.style?.hidden) continue;
29
+ const val = resolved.values.get(el.id);
30
+ const color = el.style?.color;
31
+ if (el.kind === "asset" && isAssetGeom(val) && isDerived(el) && el.def.op === "asset") {
32
+ const spec = getAsset(el.def.asset);
33
+ if (spec) {
34
+ const C = spec.Component;
35
+ shapes.push(/* @__PURE__ */ jsx(C, {
36
+ geom: val,
37
+ style: el.style,
38
+ label: el.label
39
+ }, el.id));
40
+ }
41
+ } else if (el.kind === "circle" && isCircleVal(val)) shapes.push(/* @__PURE__ */ jsx(Circle, {
42
+ center: val.center,
43
+ r: val.r,
44
+ color: color ?? "var(--stage-accent)",
45
+ fill: el.style?.fill ?? "none"
46
+ }, el.id));
47
+ else if ((el.kind === "line" || el.kind === "segment") && isLineVal(val)) shapes.push(el.kind === "line" ? /* @__PURE__ */ jsx(Line, {
48
+ from: val.a,
49
+ to: val.b,
50
+ color: color ?? "var(--stage-muted)",
51
+ dashed: el.style?.dashed
52
+ }, el.id) : /* @__PURE__ */ jsx(Segment, {
53
+ from: val.a,
54
+ to: val.b,
55
+ color: color ?? "var(--stage-good)",
56
+ dashed: el.style?.dashed
57
+ }, el.id));
58
+ else if (el.kind === "polygon" && Array.isArray(val)) shapes.push(/* @__PURE__ */ jsx(Polygon, {
59
+ points: val,
60
+ color: color ?? "var(--stage-accent)",
61
+ fill: el.style?.fill ?? "var(--stage-accent)"
62
+ }, el.id));
63
+ else if (el.kind === "measure" && isDerived(el)) {
64
+ const value = typeof val === "number" ? val : NaN;
65
+ const a = point(parentRef(el, 0));
66
+ const b = point(parentRef(el, 1));
67
+ const at = a && b ? vec.midpoint(a, b) : a;
68
+ if (at && Number.isFinite(value)) {
69
+ if (el.def.op === "distance" && a && b) shapes.push(/* @__PURE__ */ jsx(Segment, {
70
+ from: a,
71
+ to: b,
72
+ color: "var(--stage-muted)",
73
+ dashed: true,
74
+ weight: 1.5
75
+ }, `${el.id}-seg`));
76
+ overlays.push(/* @__PURE__ */ jsx(Label, {
77
+ x: at.x,
78
+ y: at.y,
79
+ dy: -8,
80
+ text: `${el.label ? el.label + " = " : ""}${value.toFixed(2)}`,
81
+ color: "var(--stage-warn)",
82
+ size: 13
83
+ }, el.id));
84
+ }
85
+ }
86
+ }
87
+ for (const el of doc.elements) {
88
+ if (el.style?.hidden) continue;
89
+ const val = resolved.values.get(el.id);
90
+ if (el.kind === "note" && isVec2(val)) overlays.push(/* @__PURE__ */ jsx(Label, {
91
+ x: val.x,
92
+ y: val.y,
93
+ text: "free" in el ? el.free.text : "",
94
+ color: el.style?.color ?? "var(--stage-fg)"
95
+ }, el.id));
96
+ else if (el.kind === "point" && isVec2(val)) {
97
+ const selected = opts.selection === el.id;
98
+ if (opts.draggablePoints && !!opts.onPointMove && "free" in el && !!el.free.draggable && "free" in el) {
99
+ const constrain = el.free.constrain === "horizontal" || el.free.constrain === "vertical" ? el.free.constrain : void 0;
100
+ overlays.push(/* @__PURE__ */ jsx(MovableDot, {
101
+ value: val,
102
+ onMove: (p, ph) => opts.onPointMove?.(el.id, p, ph),
103
+ constrain,
104
+ color: el.style?.color ?? "var(--stage-accent)",
105
+ ariaLabel: el.a11y?.label ?? el.label ?? "point"
106
+ }, el.id));
107
+ } else overlays.push(/* @__PURE__ */ jsx(Point, {
108
+ x: val.x,
109
+ y: val.y,
110
+ r: selected ? 7 : 6,
111
+ color: selected ? "var(--stage-warn)" : el.style?.color ?? "var(--stage-accent)"
112
+ }, el.id));
113
+ if (el.label) overlays.push(/* @__PURE__ */ jsx(Label, {
114
+ x: val.x,
115
+ y: val.y,
116
+ dy: -12,
117
+ text: el.label,
118
+ size: 13
119
+ }, `${el.id}-lbl`));
120
+ }
121
+ }
122
+ return /* @__PURE__ */ jsxs(Fragment, { children: [shapes, overlays] });
123
+ }
124
+
125
+ //#endregion
126
+ export { renderElements };
@@ -0,0 +1,12 @@
1
+ import { Vec2 } from "../core/vec.mjs";
2
+ import { Id, SceneDoc, Val } from "./types.mjs";
3
+
4
+ //#region src/scene/resolve.d.ts
5
+ interface Resolved {
6
+ values: Map<Id, Val>;
7
+ order: Id[];
8
+ errors: Record<Id, string>;
9
+ }
10
+ declare function resolve(doc: SceneDoc, overlay?: ReadonlyMap<Id, Vec2 | number>, prev?: Resolved, simStates?: ReadonlyMap<Id, Record<string, unknown>>): Resolved;
11
+ //#endregion
12
+ export { Resolved, resolve };