@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,100 @@
1
+ 'use client';
2
+
3
+ import { ToolIcon } from "./icons.mjs";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
5
+
6
+ //#region src/builder/Palette.tsx
7
+ const btn = (active) => ({
8
+ display: "inline-flex",
9
+ alignItems: "center",
10
+ gap: 6,
11
+ padding: "6px 10px",
12
+ borderRadius: 8,
13
+ fontSize: 13,
14
+ fontWeight: 600,
15
+ cursor: "pointer",
16
+ color: active ? "var(--stage-bg)" : "var(--stage-fg)",
17
+ background: active ? "var(--stage-accent)" : "transparent",
18
+ border: `1px solid ${active ? "var(--stage-accent)" : "var(--stage-muted)"}`
19
+ });
20
+ const iconBtn = (disabled) => ({
21
+ display: "inline-flex",
22
+ alignItems: "center",
23
+ gap: 6,
24
+ padding: "6px 10px",
25
+ borderRadius: 8,
26
+ fontSize: 13,
27
+ fontWeight: 600,
28
+ cursor: disabled ? "not-allowed" : "pointer",
29
+ opacity: disabled ? .4 : 1,
30
+ color: "var(--stage-fg)",
31
+ background: "transparent",
32
+ border: "1px solid var(--stage-muted)"
33
+ });
34
+ function Palette({ tools, activeTool, onSelect, onUndo, onRedo, canUndo, canRedo, onClear, onSave, hint }) {
35
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
36
+ role: "toolbar",
37
+ "aria-label": "construction tools",
38
+ style: {
39
+ display: "flex",
40
+ flexWrap: "wrap",
41
+ gap: 6,
42
+ alignItems: "center"
43
+ },
44
+ children: [
45
+ tools.map((t) => /* @__PURE__ */ jsxs("button", {
46
+ type: "button",
47
+ "aria-pressed": activeTool === t.id,
48
+ onClick: () => onSelect(t.id),
49
+ style: btn(activeTool === t.id),
50
+ children: [/* @__PURE__ */ jsx(ToolIcon, { name: t.icon }), t.label]
51
+ }, t.id)),
52
+ /* @__PURE__ */ jsx("span", { style: {
53
+ width: 1,
54
+ alignSelf: "stretch",
55
+ background: "var(--stage-muted)",
56
+ margin: "0 4px"
57
+ } }),
58
+ /* @__PURE__ */ jsxs("button", {
59
+ type: "button",
60
+ onClick: onUndo,
61
+ disabled: !canUndo,
62
+ style: iconBtn(!canUndo),
63
+ children: [/* @__PURE__ */ jsx(ToolIcon, { name: "undo" }), "Undo"]
64
+ }),
65
+ /* @__PURE__ */ jsxs("button", {
66
+ type: "button",
67
+ onClick: onRedo,
68
+ disabled: !canRedo,
69
+ style: iconBtn(!canRedo),
70
+ children: [/* @__PURE__ */ jsx(ToolIcon, { name: "redo" }), "Redo"]
71
+ }),
72
+ /* @__PURE__ */ jsx("button", {
73
+ type: "button",
74
+ onClick: onClear,
75
+ style: iconBtn(false),
76
+ children: "Clear"
77
+ }),
78
+ onSave && /* @__PURE__ */ jsx("button", {
79
+ type: "button",
80
+ onClick: onSave,
81
+ style: {
82
+ ...iconBtn(false),
83
+ color: "var(--stage-good)",
84
+ borderColor: "var(--stage-good)"
85
+ },
86
+ children: "Save"
87
+ })
88
+ ]
89
+ }), /* @__PURE__ */ jsx("p", {
90
+ style: {
91
+ fontSize: 12,
92
+ opacity: .7,
93
+ margin: "6px 2px 0"
94
+ },
95
+ children: hint
96
+ })] });
97
+ }
98
+
99
+ //#endregion
100
+ export { Palette };
@@ -0,0 +1,26 @@
1
+ import { ViewBox } from "../core/coords.mjs";
2
+ import { SceneDoc } from "../scene/types.mjs";
3
+ import { Tool } from "./tools.mjs";
4
+ import { ReactNode } from "react";
5
+
6
+ //#region src/builder/SceneBuilder.d.ts
7
+ interface SceneBuilderProps {
8
+ initial?: SceneDoc;
9
+ onSave?: (doc: SceneDoc) => void;
10
+ view?: ViewBox;
11
+ height?: number;
12
+ tools?: Tool[];
13
+ /** Authoring mode: adds slider/label tools, a live scalar strip, and the
14
+ * pedagogy (LabMeta) editor; onSave emits meta.pedagogy. */
15
+ authoring?: boolean;
16
+ }
17
+ declare function SceneBuilder({
18
+ initial,
19
+ onSave,
20
+ view,
21
+ height,
22
+ tools,
23
+ authoring
24
+ }: SceneBuilderProps): ReactNode;
25
+ //#endregion
26
+ export { SceneBuilder, SceneBuilderProps };
@@ -0,0 +1,340 @@
1
+ 'use client';
2
+
3
+ import { Stage } from "../view/Stage.mjs";
4
+ import { Point } from "../primitives/Dot.mjs";
5
+ import { Axes, Grid } from "../primitives/Grid.mjs";
6
+ import { isCircleVal, isFree, isLineVal, isVec2 } from "../scene/types.mjs";
7
+ import { emptyDoc } from "../scene/migrate.mjs";
8
+ import { renderElements } from "../scene/render.mjs";
9
+ import { useEditor } from "./editor.mjs";
10
+ import { Palette } from "./Palette.mjs";
11
+ import { AUTHORING_TOOLS, GEOMETRY_TOOLS } from "./tools.mjs";
12
+ import { useMemo, useState } from "react";
13
+ import { jsx, jsxs } from "react/jsx-runtime";
14
+
15
+ //#region src/builder/SceneBuilder.tsx
16
+ /**
17
+ * <SceneBuilder> — the click-to-construct authoring surface. Pick a tool, click
18
+ * the canvas: every action funnels through the Editor command stack (undo/redo +
19
+ * the agent API). Output is a SceneDoc (onSave) — the same JSON the player
20
+ * renders and the DB stores. Free points drag only in Select mode, so clicks in
21
+ * other tools go to the tool, not a drag.
22
+ */
23
+ const DEFAULT_VIEW = {
24
+ xMin: -8,
25
+ xMax: 8,
26
+ yMin: -5,
27
+ yMax: 5
28
+ };
29
+ const MATH_TOL = .45;
30
+ const snap = (m) => ({
31
+ x: Math.round(m.x * 2) / 2,
32
+ y: Math.round(m.y * 2) / 2
33
+ });
34
+ function SceneBuilder({ initial, onSave, view, height = 440, tools, authoring = false }) {
35
+ const toolSet = tools ?? (authoring ? AUTHORING_TOOLS : GEOMETRY_TOOLS);
36
+ const { editor, doc, resolved } = useEditor(initial ?? emptyDoc());
37
+ const [activeTool, setActiveTool] = useState("point");
38
+ const [pending, setPending] = useState([]);
39
+ const [pedagogy, setPedagogy] = useState(() => initial?.meta?.pedagogy ?? {});
40
+ const v = view ?? doc.view ?? DEFAULT_VIEW;
41
+ const tool = toolSet.find((t) => t.id === activeTool) ?? toolSet[0];
42
+ const hasPedagogy = Object.values(pedagogy).some((x) => Array.isArray(x) ? x.length : x != null && x !== "");
43
+ const sceneWithMeta = () => {
44
+ const s = editor.getScene();
45
+ return hasPedagogy ? {
46
+ ...s,
47
+ meta: {
48
+ ...s.meta,
49
+ pedagogy
50
+ }
51
+ } : s;
52
+ };
53
+ const scalars = doc.elements.filter((e) => e.kind === "scalar" && isFree(e));
54
+ const hitPoint = (m) => {
55
+ let best = null;
56
+ let bestD = MATH_TOL;
57
+ for (const el of doc.elements) {
58
+ if (el.kind !== "point") continue;
59
+ const val = resolved.values.get(el.id);
60
+ if (!isVec2(val)) continue;
61
+ const d = Math.hypot(val.x - m.x, val.y - m.y);
62
+ if (d < bestD) {
63
+ bestD = d;
64
+ best = el.id;
65
+ }
66
+ }
67
+ return best;
68
+ };
69
+ const hitElement = (m) => {
70
+ let best = null;
71
+ let bestD = MATH_TOL;
72
+ for (const el of doc.elements) {
73
+ const val = resolved.values.get(el.id);
74
+ if (isCircleVal(val)) {
75
+ const d = Math.abs(Math.hypot(val.center.x - m.x, val.center.y - m.y) - val.r);
76
+ if (d < bestD) {
77
+ bestD = d;
78
+ best = el.id;
79
+ }
80
+ } else if (isLineVal(val)) {
81
+ const ab = {
82
+ x: val.b.x - val.a.x,
83
+ y: val.b.y - val.a.y
84
+ };
85
+ const len2 = ab.x * ab.x + ab.y * ab.y || 1;
86
+ let t = ((m.x - val.a.x) * ab.x + (m.y - val.a.y) * ab.y) / len2;
87
+ if (el.kind === "segment") t = Math.max(0, Math.min(1, t));
88
+ const cp = {
89
+ x: val.a.x + t * ab.x,
90
+ y: val.a.y + t * ab.y
91
+ };
92
+ const d = Math.hypot(cp.x - m.x, cp.y - m.y);
93
+ if (d < bestD) {
94
+ bestD = d;
95
+ best = el.id;
96
+ }
97
+ }
98
+ }
99
+ return best;
100
+ };
101
+ const nextId = (prefix) => {
102
+ const used = new Set(editor.getScene().elements.map((e) => e.id));
103
+ if (prefix === "P") {
104
+ for (const ch of "ABCDEFGHJKLMNOPQRSTUVWXYZ") if (!used.has(ch)) return ch;
105
+ }
106
+ let i = 1;
107
+ while (used.has(`${prefix}${i}`)) i += 1;
108
+ return `${prefix}${i}`;
109
+ };
110
+ const handlePoint = (mArr) => {
111
+ const m = {
112
+ x: mArr[0],
113
+ y: mArr[1]
114
+ };
115
+ const ctx = {
116
+ editor,
117
+ hitPoint,
118
+ hitElement,
119
+ pending,
120
+ setPending,
121
+ snap,
122
+ nextId
123
+ };
124
+ tool.onPoint(ctx, m);
125
+ };
126
+ const selectTool = (id) => {
127
+ setActiveTool(id);
128
+ setPending([]);
129
+ };
130
+ const clear = () => {
131
+ const cmds = editor.getScene().elements.map((e) => ({
132
+ op: "remove",
133
+ id: e.id
134
+ }));
135
+ if (cmds.length) editor.dispatch({
136
+ op: "batch",
137
+ label: "clear",
138
+ commands: cmds
139
+ });
140
+ setPending([]);
141
+ };
142
+ const pendingDots = useMemo(() => pending.map((id) => {
143
+ const val = resolved.values.get(id);
144
+ return isVec2(val) ? /* @__PURE__ */ jsx(Point, {
145
+ x: val.x,
146
+ y: val.y,
147
+ r: 9,
148
+ color: "var(--stage-warn)"
149
+ }, `pend-${id}`) : null;
150
+ }), [pending, resolved]);
151
+ return /* @__PURE__ */ jsxs("div", {
152
+ className: "not-prose",
153
+ style: {
154
+ display: "flex",
155
+ flexDirection: "column",
156
+ gap: 10
157
+ },
158
+ children: [
159
+ /* @__PURE__ */ jsx(Palette, {
160
+ tools: toolSet,
161
+ activeTool,
162
+ onSelect: selectTool,
163
+ onUndo: () => editor.undo(),
164
+ onRedo: () => editor.redo(),
165
+ canUndo: editor.canUndo(),
166
+ canRedo: editor.canRedo(),
167
+ onClear: clear,
168
+ onSave: onSave ? () => onSave(sceneWithMeta()) : void 0,
169
+ hint: tool.hint
170
+ }),
171
+ /* @__PURE__ */ jsxs(Stage, {
172
+ view: v,
173
+ height,
174
+ ariaLabel: "Construction builder",
175
+ onPointerMath: handlePoint,
176
+ children: [
177
+ /* @__PURE__ */ jsx(Grid, {}),
178
+ /* @__PURE__ */ jsx(Axes, { ticks: false }),
179
+ renderElements(doc, resolved, {
180
+ draggablePoints: activeTool === "select",
181
+ onPointMove: (id, p, phase) => phase === "move" ? editor.updateDrag(id, snap(p)) : editor.endDrag(id, snap(p))
182
+ }),
183
+ pendingDots
184
+ ]
185
+ }),
186
+ authoring && scalars.length > 0 && /* @__PURE__ */ jsx("div", {
187
+ style: {
188
+ display: "flex",
189
+ flexWrap: "wrap",
190
+ gap: 14,
191
+ padding: "2px",
192
+ fontSize: 13
193
+ },
194
+ children: scalars.map((el) => {
195
+ const f = el.free;
196
+ return /* @__PURE__ */ jsxs("label", {
197
+ style: {
198
+ display: "inline-flex",
199
+ alignItems: "center",
200
+ gap: 6
201
+ },
202
+ children: [
203
+ /* @__PURE__ */ jsx("strong", { children: el.label ?? el.id }),
204
+ /* @__PURE__ */ jsx("input", {
205
+ type: "range",
206
+ min: f.min ?? 0,
207
+ max: f.max ?? 10,
208
+ step: f.step ?? 1,
209
+ value: f.value,
210
+ onChange: (e) => editor.dispatch({
211
+ op: "mutate",
212
+ id: el.id,
213
+ patch: { free: { value: Number(e.currentTarget.value) } }
214
+ })
215
+ }),
216
+ /* @__PURE__ */ jsx("span", {
217
+ style: {
218
+ fontVariantNumeric: "tabular-nums",
219
+ minWidth: 24
220
+ },
221
+ children: f.value
222
+ })
223
+ ]
224
+ }, el.id);
225
+ })
226
+ }),
227
+ authoring && /* @__PURE__ */ jsxs("details", {
228
+ style: {
229
+ fontSize: 13,
230
+ border: "1px solid var(--stage-muted)",
231
+ borderRadius: 8,
232
+ padding: "6px 10px"
233
+ },
234
+ children: [/* @__PURE__ */ jsx("summary", {
235
+ style: {
236
+ cursor: "pointer",
237
+ fontWeight: 600
238
+ },
239
+ children: "Pedagogy — objectives · hints · difficulty"
240
+ }), /* @__PURE__ */ jsxs("div", {
241
+ style: {
242
+ display: "grid",
243
+ gap: 8,
244
+ marginTop: 8
245
+ },
246
+ children: [
247
+ /* @__PURE__ */ jsxs("label", {
248
+ style: {
249
+ display: "grid",
250
+ gap: 3
251
+ },
252
+ children: ["Objectives (one per line)", /* @__PURE__ */ jsx("textarea", {
253
+ rows: 2,
254
+ value: (pedagogy.objectives ?? []).join("\n"),
255
+ onChange: (e) => setPedagogy((p) => ({
256
+ ...p,
257
+ objectives: splitLines(e.currentTarget.value)
258
+ })),
259
+ style: FIELD
260
+ })]
261
+ }),
262
+ /* @__PURE__ */ jsxs("label", {
263
+ style: {
264
+ display: "grid",
265
+ gap: 3
266
+ },
267
+ children: ["Hints (one per line)", /* @__PURE__ */ jsx("textarea", {
268
+ rows: 2,
269
+ value: (pedagogy.hints ?? []).join("\n"),
270
+ onChange: (e) => setPedagogy((p) => ({
271
+ ...p,
272
+ hints: splitLines(e.currentTarget.value)
273
+ })),
274
+ style: FIELD
275
+ })]
276
+ }),
277
+ /* @__PURE__ */ jsxs("label", {
278
+ style: {
279
+ display: "grid",
280
+ gap: 3
281
+ },
282
+ children: ["Success criteria", /* @__PURE__ */ jsx("input", {
283
+ value: pedagogy.successCriteria ?? "",
284
+ onChange: (e) => setPedagogy((p) => ({
285
+ ...p,
286
+ successCriteria: e.currentTarget.value || void 0
287
+ })),
288
+ style: FIELD
289
+ })]
290
+ }),
291
+ /* @__PURE__ */ jsxs("label", {
292
+ style: {
293
+ display: "inline-flex",
294
+ alignItems: "center",
295
+ gap: 8
296
+ },
297
+ children: ["Difficulty", /* @__PURE__ */ jsxs("select", {
298
+ value: pedagogy.difficulty ?? "",
299
+ onChange: (e) => setPedagogy((p) => ({
300
+ ...p,
301
+ difficulty: e.currentTarget.value ? Number(e.currentTarget.value) : void 0
302
+ })),
303
+ style: {
304
+ ...FIELD,
305
+ width: "auto"
306
+ },
307
+ children: [/* @__PURE__ */ jsx("option", {
308
+ value: "",
309
+ children: "—"
310
+ }), [
311
+ 1,
312
+ 2,
313
+ 3,
314
+ 4,
315
+ 5
316
+ ].map((d) => /* @__PURE__ */ jsx("option", {
317
+ value: d,
318
+ children: d
319
+ }, d))]
320
+ })]
321
+ })
322
+ ]
323
+ })]
324
+ })
325
+ ]
326
+ });
327
+ }
328
+ const FIELD = {
329
+ width: "100%",
330
+ border: "1px solid var(--stage-muted)",
331
+ borderRadius: 6,
332
+ padding: "4px 8px",
333
+ background: "transparent",
334
+ color: "inherit",
335
+ font: "inherit"
336
+ };
337
+ const splitLines = (s) => s.split("\n").map((x) => x.trim()).filter(Boolean);
338
+
339
+ //#endregion
340
+ export { SceneBuilder };
@@ -0,0 +1,18 @@
1
+ import { Id } from "../scene/types.mjs";
2
+ import { Editor } from "./editor.mjs";
3
+ import { ControlMap } from "../core/control.mjs";
4
+
5
+ //#region src/builder/controls.d.ts
6
+ interface ControlPick {
7
+ /** Element id of a free scalar (slider / unknown). */
8
+ id: Id;
9
+ /** Public control name (what an agent addresses). */
10
+ name: string;
11
+ label?: string;
12
+ min?: number;
13
+ max?: number;
14
+ step?: number;
15
+ }
16
+ declare function controlsFromScene(editor: Editor, picks: ControlPick[]): ControlMap;
17
+ //#endregion
18
+ export { ControlPick, controlsFromScene };
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+
3
+ //#region src/builder/controls.ts
4
+ function controlsFromScene(editor, picks) {
5
+ const map = {};
6
+ for (const p of picks) map[p.name] = {
7
+ type: "number",
8
+ label: p.label,
9
+ min: p.min,
10
+ max: p.max,
11
+ step: p.step,
12
+ get: () => {
13
+ const v = editor.getResolved().values.get(p.id);
14
+ return typeof v === "number" ? v : 0;
15
+ },
16
+ set: (v) => {
17
+ editor.dispatch({
18
+ op: "mutate",
19
+ id: p.id,
20
+ patch: { free: { value: v } }
21
+ });
22
+ editor.mark(`set ${p.name}`);
23
+ }
24
+ };
25
+ return map;
26
+ }
27
+
28
+ //#endregion
29
+ export { controlsFromScene };
@@ -0,0 +1,46 @@
1
+ import { Vec2 } from "../core/vec.mjs";
2
+ import { Id, SceneDoc } from "../scene/types.mjs";
3
+ import { Command, CommandResult } from "../scene/commands.mjs";
4
+ import { Resolved } from "../scene/resolve.mjs";
5
+
6
+ //#region src/builder/editor.d.ts
7
+ interface DispatchOpts {
8
+ history?: 'record' | 'ignore';
9
+ }
10
+ declare class Editor {
11
+ private doc;
12
+ private undoStack;
13
+ private redoStack;
14
+ private overlay;
15
+ private listeners;
16
+ private prev?;
17
+ constructor(doc: SceneDoc);
18
+ getScene(): SceneDoc;
19
+ /** Replace the whole scene (e.g. the authored config changed in the editor). */
20
+ reset(doc: SceneDoc): void;
21
+ getResolved(): Resolved;
22
+ subscribe(fn: () => void): () => void;
23
+ private emit;
24
+ /** Apply a command. The ONLY mutation entry point. */
25
+ dispatch(cmd: Command, opts?: DispatchOpts): CommandResult;
26
+ /** Group boundary marker (label-only in this impl; batch commands group atomically). */
27
+ mark(_label?: string): void;
28
+ canUndo(): boolean;
29
+ canRedo(): boolean;
30
+ undo(): void;
31
+ redo(): void;
32
+ /** Discard the live drag overlay without committing (drag cancel). */
33
+ bail(): void;
34
+ beginDrag(_id: Id): void;
35
+ updateDrag(id: Id, p: Vec2): void;
36
+ endDrag(id: Id, p: Vec2): void;
37
+ getOverlay(): ReadonlyMap<Id, Vec2>;
38
+ }
39
+ /** React binding: holds one Editor and re-renders on its changes. */
40
+ declare function useEditor(initial: SceneDoc): {
41
+ editor: Editor;
42
+ doc: SceneDoc;
43
+ resolved: Resolved;
44
+ };
45
+ //#endregion
46
+ export { DispatchOpts, Editor, useEditor };
@@ -0,0 +1,130 @@
1
+ 'use client';
2
+
3
+ import { resolve } from "../scene/resolve.mjs";
4
+ import { applyCommand, inverse } from "../scene/commands.mjs";
5
+ import { useEffect, useReducer, useRef, useState } from "react";
6
+
7
+ //#region src/builder/editor.ts
8
+ /**
9
+ * Editor — the single funnel for ALL scene mutations (builder tools, agents,
10
+ * undo/redo, the control-surface bridge). Commands ARE the agent action API:
11
+ * dispatch(cmd) is exactly what a human tool, a voice agent, and an undo all call.
12
+ * Drag is an ephemeral overlay layer; updateDrag writes no command (no undo, no
13
+ * persist), endDrag commits ONE mutate.
14
+ */
15
+ var Editor = class {
16
+ doc;
17
+ undoStack = [];
18
+ redoStack = [];
19
+ overlay = /* @__PURE__ */ new Map();
20
+ listeners = /* @__PURE__ */ new Set();
21
+ prev;
22
+ constructor(doc) {
23
+ this.doc = doc;
24
+ }
25
+ getScene() {
26
+ return this.doc;
27
+ }
28
+ /** Replace the whole scene (e.g. the authored config changed in the editor). */
29
+ reset(doc) {
30
+ this.doc = doc;
31
+ this.undoStack = [];
32
+ this.redoStack = [];
33
+ this.overlay.clear();
34
+ this.prev = void 0;
35
+ this.emit();
36
+ }
37
+ getResolved() {
38
+ const r = resolve(this.doc, this.overlay.size ? this.overlay : void 0, this.prev);
39
+ this.prev = r;
40
+ return r;
41
+ }
42
+ subscribe(fn) {
43
+ this.listeners.add(fn);
44
+ return () => {
45
+ this.listeners.delete(fn);
46
+ };
47
+ }
48
+ emit() {
49
+ this.listeners.forEach((l) => l());
50
+ }
51
+ /** Apply a command. The ONLY mutation entry point. */
52
+ dispatch(cmd, opts = {}) {
53
+ const inv = inverse(cmd, this.doc);
54
+ this.doc = applyCommand(this.doc, cmd);
55
+ if (opts.history !== "ignore") {
56
+ this.undoStack.push({
57
+ cmd,
58
+ inv
59
+ });
60
+ this.redoStack = [];
61
+ }
62
+ this.emit();
63
+ return { ok: true };
64
+ }
65
+ /** Group boundary marker (label-only in this impl; batch commands group atomically). */
66
+ mark(_label) {}
67
+ canUndo() {
68
+ return this.undoStack.length > 0;
69
+ }
70
+ canRedo() {
71
+ return this.redoStack.length > 0;
72
+ }
73
+ undo() {
74
+ const entry = this.undoStack.pop();
75
+ if (!entry) return;
76
+ this.doc = applyCommand(this.doc, entry.inv);
77
+ this.redoStack.push(entry);
78
+ this.emit();
79
+ }
80
+ redo() {
81
+ const entry = this.redoStack.pop();
82
+ if (!entry) return;
83
+ this.doc = applyCommand(this.doc, entry.cmd);
84
+ this.undoStack.push(entry);
85
+ this.emit();
86
+ }
87
+ /** Discard the live drag overlay without committing (drag cancel). */
88
+ bail() {
89
+ this.overlay.clear();
90
+ this.emit();
91
+ }
92
+ beginDrag(_id) {}
93
+ updateDrag(id, p) {
94
+ this.overlay.set(id, p);
95
+ this.emit();
96
+ }
97
+ endDrag(id, p) {
98
+ this.overlay.delete(id);
99
+ this.dispatch({
100
+ op: "mutate",
101
+ id,
102
+ patch: { free: { at: p } }
103
+ });
104
+ }
105
+ getOverlay() {
106
+ return this.overlay;
107
+ }
108
+ };
109
+ /** React binding: holds one Editor and re-renders on its changes. */
110
+ function useEditor(initial) {
111
+ const [editor] = useState(() => new Editor(initial));
112
+ const [, force] = useReducer((x) => x + 1, 0);
113
+ useEffect(() => editor.subscribe(() => force()), [editor]);
114
+ const firstRun = useRef(true);
115
+ useEffect(() => {
116
+ if (firstRun.current) {
117
+ firstRun.current = false;
118
+ return;
119
+ }
120
+ editor.reset(initial);
121
+ }, [editor, initial]);
122
+ return {
123
+ editor,
124
+ doc: editor.getScene(),
125
+ resolved: editor.getResolved()
126
+ };
127
+ }
128
+
129
+ //#endregion
130
+ export { Editor, useEditor };
@@ -0,0 +1,14 @@
1
+ import { ReactNode } from "react";
2
+
3
+ //#region src/builder/icons.d.ts
4
+ type IconName = 'select' | 'point' | 'segment' | 'line' | 'circle' | 'midpoint' | 'intersect' | 'measure' | 'scalar' | 'note' | 'delete' | 'undo' | 'redo';
5
+ /** A single builder icon. `size` in px; inherits `currentColor`. Decorative. */
6
+ declare function ToolIcon({
7
+ name,
8
+ size
9
+ }: {
10
+ name: IconName;
11
+ size?: number;
12
+ }): ReactNode;
13
+ //#endregion
14
+ export { IconName, ToolIcon };