@classytic/stage 0.1.0 → 0.2.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.
- package/README.md +2 -2
- package/dist/assets/kit/glyphs.d.mts +5 -5
- package/dist/assets/kit/glyphs.mjs +4 -4
- package/dist/builder/SceneBuilder.mjs +4 -4
- package/dist/builder/editor.d.mts +2 -1
- package/dist/builder/editor.mjs +14 -4
- package/dist/builder/tools.d.mts +1 -1
- package/dist/builder/tools.mjs +1 -1
- package/dist/chem/index.d.mts +5 -5
- package/dist/chem/index.mjs +4 -4
- package/dist/circuit/index.d.mts +92 -0
- package/dist/circuit/index.mjs +333 -0
- package/dist/core/clock.d.mts +1 -1
- package/dist/core/clock.mjs +3 -3
- package/dist/core/control.d.mts +1 -1
- package/dist/core/control.mjs +2 -2
- package/dist/core/coords.d.mts +4 -4
- package/dist/core/coords.mjs +1 -1
- package/dist/core/learner.d.mts +1 -1
- package/dist/core/learner.mjs +1 -1
- package/dist/core/motion.d.mts +6 -6
- package/dist/core/motion.mjs +6 -6
- package/dist/core/richText.d.mts +2 -2
- package/dist/core/vec.d.mts +1 -1
- package/dist/field/index.d.mts +11 -3
- package/dist/field/index.mjs +20 -3
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +3 -1
- package/dist/interaction/MovableDot.d.mts +17 -1
- package/dist/interaction/MovableDot.mjs +141 -43
- package/dist/interaction/index.d.mts +2 -1
- package/dist/interaction/index.mjs +2 -1
- package/dist/interaction/useDraggable.d.mts +1 -1
- package/dist/interaction/useDraggable.mjs +31 -1
- package/dist/interaction/usePressSpring.d.mts +24 -0
- package/dist/interaction/usePressSpring.mjs +67 -0
- package/dist/logic/ast.d.mts +1 -1
- package/dist/logic/index.mjs +8 -3
- package/dist/logic/minimize.d.mts +2 -2
- package/dist/logic/minimize.mjs +56 -18
- package/dist/logic/table.mjs +1 -1
- package/dist/math/ast.d.mts +1 -1
- package/dist/math/ast.mjs +1 -1
- package/dist/math/calculus.mjs +68 -9
- package/dist/math/compile.d.mts +8 -0
- package/dist/math/compile.mjs +40 -0
- package/dist/math/defs.mjs +1 -1
- package/dist/math/index.d.mts +4 -3
- package/dist/math/index.mjs +4 -3
- package/dist/math/parse.mjs +2 -2
- package/dist/math/tokenize.mjs +1 -1
- package/dist/primitives/CanvasLayer.mjs +3 -3
- package/dist/primitives/Grid.d.mts +12 -1
- package/dist/primitives/Grid.mjs +15 -9
- package/dist/primitives/Label.d.mts +1 -1
- package/dist/primitives/Label.mjs +1 -1
- package/dist/primitives/Tex.mjs +2 -2
- package/dist/scene/Scene.mjs +1 -1
- package/dist/scene/assets.d.mts +2 -2
- package/dist/scene/assets.mjs +2 -2
- package/dist/scene/commands.d.mts +10 -1
- package/dist/scene/commands.mjs +42 -2
- package/dist/scene/evaluators.mjs +2 -2
- package/dist/scene/index.d.mts +2 -2
- package/dist/scene/index.mjs +2 -2
- package/dist/scene/migrate.mjs +1 -1
- package/dist/scene/sims.mjs +1 -1
- package/dist/scene/types.d.mts +4 -4
- package/dist/sim/particles.d.mts +2 -2
- package/dist/sim/rate.d.mts +4 -4
- package/dist/sim/rate.mjs +1 -1
- package/dist/sim/registry.d.mts +1 -1
- package/dist/sim/sampler.d.mts +5 -5
- package/dist/sim/thermal.d.mts +3 -3
- package/dist/sim/thermal.mjs +2 -2
- package/dist/sim/types.d.mts +2 -2
- package/dist/sim/wave.d.mts +2 -2
- package/dist/sim/wave.mjs +2 -2
- package/dist/steps/index.d.mts +1 -1
- package/dist/steps/index.mjs +3 -3
- package/dist/thermo/index.d.mts +1 -1
- package/dist/thermo/index.mjs +1 -1
- package/dist/view/Stage.mjs +2 -2
- package/dist/view/useInView.mjs +1 -1
- package/package.json +8 -4
- package/styles.css +23 -0
package/dist/math/ast.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { CONSTANTS, FN1, FN2 } from "./defs.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/math/ast.ts
|
|
4
4
|
/**
|
|
5
|
-
* expr/ast
|
|
5
|
+
* expr/ast, the Abstract Syntax Tree: node types, evaluation, free-variable
|
|
6
6
|
* collection, and small builder helpers. The AST is the shared representation
|
|
7
7
|
* that evaluation, differentiation, simplification, and LaTeX printing all
|
|
8
8
|
* operate on.
|
package/dist/math/calculus.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { add, call, div, mul, neg, num, pow, sub } from "./ast.mjs";
|
|
|
3
3
|
|
|
4
4
|
//#region src/math/calculus.ts
|
|
5
5
|
/**
|
|
6
|
-
* expr/calculus
|
|
6
|
+
* expr/calculus, symbolic differentiation + algebraic simplification on the AST.
|
|
7
7
|
*
|
|
8
8
|
* `differentiate(node, x)` returns the exact derivative as a new AST, or `null`
|
|
9
9
|
* when the expression contains something not symbolically differentiable
|
|
@@ -71,6 +71,72 @@ function differentiate(node, x) {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
const isNum = (n, v) => n.type === "num" && (v === void 0 || n.value === v);
|
|
74
|
+
/** A stable structural key, so identical cores group (x and x; x² and x²). */
|
|
75
|
+
function nodeKey(n) {
|
|
76
|
+
switch (n.type) {
|
|
77
|
+
case "num": return `#${n.value}`;
|
|
78
|
+
case "var": return `$${n.name}`;
|
|
79
|
+
case "neg": return `-${nodeKey(n.arg)}`;
|
|
80
|
+
case "binary": return `(${nodeKey(n.left)}${n.op}${nodeKey(n.right)})`;
|
|
81
|
+
case "call": return `${n.fn}(${n.args.map(nodeKey).join(",")})`;
|
|
82
|
+
default: return "?";
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** Split a term into (numeric coefficient, the rest); core null = a pure number. */
|
|
86
|
+
function splitCoeff(n) {
|
|
87
|
+
if (n.type === "num") return [n.value, null];
|
|
88
|
+
if (n.type === "neg") {
|
|
89
|
+
const [c, core] = splitCoeff(n.arg);
|
|
90
|
+
return [-c, core];
|
|
91
|
+
}
|
|
92
|
+
if (n.type === "binary" && n.op === "*") {
|
|
93
|
+
if (n.left.type === "num") return [n.left.value, n.right];
|
|
94
|
+
if (n.right.type === "num") return [n.right.value, n.left];
|
|
95
|
+
}
|
|
96
|
+
return [1, n];
|
|
97
|
+
}
|
|
98
|
+
/** Flatten a +/- chain into signed (coeff, core) terms. */
|
|
99
|
+
function flattenTerms(node, s, out) {
|
|
100
|
+
if (node.type === "binary" && (node.op === "+" || node.op === "-")) {
|
|
101
|
+
flattenTerms(node.left, s, out);
|
|
102
|
+
flattenTerms(node.right, node.op === "-" ? -s : s, out);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (node.type === "neg") {
|
|
106
|
+
flattenTerms(node.arg, -s, out);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const [coeff, core] = splitCoeff(node);
|
|
110
|
+
out.push({
|
|
111
|
+
coeff: coeff * s,
|
|
112
|
+
core
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/** Combine like terms across `l op r`: 2x + 3x → 5x, x − x → 0, constants merge. */
|
|
116
|
+
function combineLikeTerms(l, r, op) {
|
|
117
|
+
const list = [];
|
|
118
|
+
flattenTerms(l, 1, list);
|
|
119
|
+
flattenTerms(r, op === "-" ? -1 : 1, list);
|
|
120
|
+
const groups = /* @__PURE__ */ new Map();
|
|
121
|
+
for (const t of list) {
|
|
122
|
+
const k = t.core ? nodeKey(t.core) : "";
|
|
123
|
+
const g = groups.get(k);
|
|
124
|
+
if (g) g.coeff += t.coeff;
|
|
125
|
+
else groups.set(k, {
|
|
126
|
+
coeff: t.coeff,
|
|
127
|
+
core: t.core
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
let result = null;
|
|
131
|
+
for (const { coeff, core } of groups.values()) {
|
|
132
|
+
if (Math.abs(coeff) < 1e-12) continue;
|
|
133
|
+
const mag = Math.abs(coeff);
|
|
134
|
+
const body = core === null ? num(mag) : mag === 1 ? core : mul(num(mag), core);
|
|
135
|
+
if (result === null) result = coeff < 0 ? neg(body) : body;
|
|
136
|
+
else result = coeff < 0 ? sub(result, body) : add(result, body);
|
|
137
|
+
}
|
|
138
|
+
return result ?? num(0);
|
|
139
|
+
}
|
|
74
140
|
/** Fold constants and trivial identities so derivative output is readable. */
|
|
75
141
|
function simplify(node) {
|
|
76
142
|
switch (node.type) {
|
|
@@ -90,15 +156,8 @@ function simplify(node) {
|
|
|
90
156
|
const v = fold(op, l.value, r.value);
|
|
91
157
|
if (v !== null && Number.isFinite(v)) return num(v);
|
|
92
158
|
}
|
|
159
|
+
if (op === "+" || op === "-") return combineLikeTerms(l, r, op);
|
|
93
160
|
switch (op) {
|
|
94
|
-
case "+":
|
|
95
|
-
if (isNum(l, 0)) return r;
|
|
96
|
-
if (isNum(r, 0)) return l;
|
|
97
|
-
break;
|
|
98
|
-
case "-":
|
|
99
|
-
if (isNum(r, 0)) return l;
|
|
100
|
-
if (isNum(l, 0)) return simplify(neg(r));
|
|
101
|
-
break;
|
|
102
161
|
case "*":
|
|
103
162
|
if (isNum(l, 0) || isNum(r, 0)) return num(0);
|
|
104
163
|
if (isNum(l, 1)) return r;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Node } from "./ast.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/math/compile.d.ts
|
|
4
|
+
type CompiledFn = (scope?: Record<string, number>) => number;
|
|
5
|
+
/** Compile an AST to a fast `(scope) => number`. */
|
|
6
|
+
declare function compile(node: Node): CompiledFn;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { CompiledFn, compile };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { CONSTANTS, FN1, FN2 } from "./defs.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/math/compile.ts
|
|
4
|
+
const JS_OP = {
|
|
5
|
+
"+": "+",
|
|
6
|
+
"-": "-",
|
|
7
|
+
"*": "*",
|
|
8
|
+
"/": "/",
|
|
9
|
+
"%": "%",
|
|
10
|
+
"^": "**"
|
|
11
|
+
};
|
|
12
|
+
/** Emit a JS expression string (every node fully parenthesised, so `**` and unary
|
|
13
|
+
* minus never collide — `(-x)**2` is valid, `-x**2` is a JS SyntaxError). */
|
|
14
|
+
function gen(node) {
|
|
15
|
+
switch (node.type) {
|
|
16
|
+
case "num": return `(${node.value})`;
|
|
17
|
+
case "var": {
|
|
18
|
+
const lc = node.name.toLowerCase();
|
|
19
|
+
if (lc in CONSTANTS) return `(${CONSTANTS[lc]})`;
|
|
20
|
+
return `(s[${JSON.stringify(node.name)}]??NaN)`;
|
|
21
|
+
}
|
|
22
|
+
case "neg": return `(-${gen(node.arg)})`;
|
|
23
|
+
case "binary": return `(${gen(node.left)}${JS_OP[node.op] ?? "+"}${gen(node.right)})`;
|
|
24
|
+
case "call": {
|
|
25
|
+
const a = node.args.map(gen);
|
|
26
|
+
if (node.fn in FN1 && a[0]) return `F1[${JSON.stringify(node.fn)}](${a[0]})`;
|
|
27
|
+
if (node.fn in FN2 && a[0] && a[1]) return `F2[${JSON.stringify(node.fn)}](${a[0]},${a[1]})`;
|
|
28
|
+
return "NaN";
|
|
29
|
+
}
|
|
30
|
+
default: return "NaN";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Compile an AST to a fast `(scope) => number`. */
|
|
34
|
+
function compile(node) {
|
|
35
|
+
const f = new Function("s", "F1", "F2", `return ${gen(node)};`);
|
|
36
|
+
return (scope = {}) => f(scope, FN1, FN2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
//#endregion
|
|
40
|
+
export { compile };
|
package/dist/math/defs.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/math/defs.ts
|
|
2
2
|
/**
|
|
3
|
-
* expr/defs
|
|
3
|
+
* expr/defs, the function & constant tables shared by the tokenizer, evaluator,
|
|
4
4
|
* differentiator, and LaTeX printer. Pure data; no AST knowledge here.
|
|
5
5
|
*/
|
|
6
6
|
const CONSTANTS = {
|
package/dist/math/index.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BinOp, Node, compileNode, evaluate, freeVars } from "./ast.mjs";
|
|
2
2
|
import { parse } from "./parse.mjs";
|
|
3
3
|
import { differentiate, simplify } from "./calculus.mjs";
|
|
4
|
+
import { CompiledFn, compile } from "./compile.mjs";
|
|
4
5
|
import { toLatex } from "./latex.mjs";
|
|
5
6
|
|
|
6
7
|
//#region src/math/index.d.ts
|
|
@@ -9,7 +10,7 @@ interface CompiledExpr {
|
|
|
9
10
|
readonly fn: (scope: Record<string, number>) => number;
|
|
10
11
|
/** Free variables referenced (excludes constants & functions), e.g. `['x','a']`. */
|
|
11
12
|
readonly vars: string[];
|
|
12
|
-
/** The parsed syntax tree
|
|
13
|
+
/** The parsed syntax tree, for differentiation, LaTeX, or analysis. */
|
|
13
14
|
readonly ast: Node;
|
|
14
15
|
readonly error?: undefined;
|
|
15
16
|
}
|
|
@@ -20,7 +21,7 @@ interface ExprError {
|
|
|
20
21
|
readonly ast?: undefined;
|
|
21
22
|
}
|
|
22
23
|
type ExprResult = CompiledExpr | ExprError;
|
|
23
|
-
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }
|
|
24
|
+
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }`, never throws. */
|
|
24
25
|
declare function compileExpr(src: string): ExprResult;
|
|
25
26
|
//#endregion
|
|
26
|
-
export { type BinOp, CompiledExpr, ExprError, ExprResult, type Node, compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
|
|
27
|
+
export { type BinOp, CompiledExpr, type CompiledFn, ExprError, ExprResult, type Node, compile, compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
|
package/dist/math/index.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { compileNode, evaluate, freeVars } from "./ast.mjs";
|
|
2
2
|
import { parse } from "./parse.mjs";
|
|
3
3
|
import { differentiate, simplify } from "./calculus.mjs";
|
|
4
|
+
import { compile } from "./compile.mjs";
|
|
4
5
|
import { toLatex } from "./latex.mjs";
|
|
5
6
|
|
|
6
7
|
//#region src/math/index.ts
|
|
7
8
|
/**
|
|
8
|
-
* expr
|
|
9
|
+
* expr, a tiny, safe, dependency-free symbolic math engine.
|
|
9
10
|
*
|
|
10
11
|
* Pipeline: `tokenize → parse (AST) → evaluate`. On top of the AST it offers
|
|
11
12
|
* exact `differentiate`, `simplify`, and `toLatex`, so the same typed formula
|
|
@@ -22,7 +23,7 @@ import { toLatex } from "./latex.mjs";
|
|
|
22
23
|
* csc asin acos atan sinh cosh tanh sqrt cbrt abs exp ln log log2 log10 floor
|
|
23
24
|
* ceil round sign` (+ 2-arg `pow atan2 min max mod hypot`).
|
|
24
25
|
*/
|
|
25
|
-
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }
|
|
26
|
+
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }`, never throws. */
|
|
26
27
|
function compileExpr(src) {
|
|
27
28
|
if (!src || !src.trim()) return { error: "Empty expression" };
|
|
28
29
|
let ast;
|
|
@@ -39,4 +40,4 @@ function compileExpr(src) {
|
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
//#endregion
|
|
42
|
-
export { compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
|
|
43
|
+
export { compile, compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
|
package/dist/math/parse.mjs
CHANGED
|
@@ -4,11 +4,11 @@ import { bin, call, neg, num, variable } from "./ast.mjs";
|
|
|
4
4
|
|
|
5
5
|
//#region src/math/parse.ts
|
|
6
6
|
/**
|
|
7
|
-
* expr/parse
|
|
7
|
+
* expr/parse, token stream → AST, via precedence-climbing (Pratt).
|
|
8
8
|
*
|
|
9
9
|
* Binary precedence: `+ -` < `* / %` < `^` (right-assoc). Unary minus binds
|
|
10
10
|
* looser than `^` (so `-2^2 = -(2^2) = -4`) but tighter than `*`. Throws a
|
|
11
|
-
* descriptive Error on malformed input
|
|
11
|
+
* descriptive Error on malformed input, the parser is the single source of
|
|
12
12
|
* truth for "is this expression well-formed?".
|
|
13
13
|
*/
|
|
14
14
|
const BIN_PREC = {
|
package/dist/math/tokenize.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { isFunction } from "./defs.mjs";
|
|
|
2
2
|
|
|
3
3
|
//#region src/math/tokenize.ts
|
|
4
4
|
/**
|
|
5
|
-
* expr/tokenize
|
|
5
|
+
* expr/tokenize, source string → token stream.
|
|
6
6
|
*
|
|
7
7
|
* Handles numbers (incl. `1.5e3`, rejecting malformed `2..3`), identifiers
|
|
8
8
|
* (classified as function vs variable), operators, parens, and commas. Inserts
|
|
@@ -7,16 +7,16 @@ import { jsx } from "react/jsx-runtime";
|
|
|
7
7
|
|
|
8
8
|
//#region src/primitives/CanvasLayer.tsx
|
|
9
9
|
/**
|
|
10
|
-
* <CanvasLayer
|
|
10
|
+
* <CanvasLayer>, the engine's HIGH-ELEMENT-COUNT escape hatch: a zero-dependency
|
|
11
11
|
* HiDPI <canvas> that shares the engine's coordinate system + clock. Use it when
|
|
12
12
|
* a lab needs a heatmap, a particle/gas sim, or thousands of moving dots at 60fps
|
|
13
|
-
|
|
13
|
+
*, cases where one retained SVG node per element would drop frames. Declarative,
|
|
14
14
|
* low-count, accessible figures should stay on <Stage> (SVG); this is the
|
|
15
15
|
* deliberate, bundle-free alternative to a WebGL engine (no Pixi/three dependency)
|
|
16
16
|
* until a lab MEASURABLY needs shaders/tens-of-thousands of textured sprites.
|
|
17
17
|
*
|
|
18
18
|
* `draw(ctx, coords)` runs in CSS-pixel space (the context is pre-scaled for dpr)
|
|
19
|
-
* on mount, on resize, and whenever `draw`'s identity changes
|
|
19
|
+
* on mount, on resize, and whenever `draw`'s identity changes, so memoize it on
|
|
20
20
|
* your state (or bump it from a useFrameLoop tick) to drive animation. Overlay a
|
|
21
21
|
* sibling <Stage> with the SAME view for accessible SVG handles/labels on top.
|
|
22
22
|
*/
|
|
@@ -5,18 +5,27 @@ import { ReactNode } from "react";
|
|
|
5
5
|
declare function niceStep(span: number, target?: number): number;
|
|
6
6
|
interface GridProps {
|
|
7
7
|
step?: number;
|
|
8
|
+
/** Per-axis overrides (win over `step`); keeps a tall-thin or short-wide plot
|
|
9
|
+
* from inheriting the other axis's spacing. */
|
|
10
|
+
stepX?: number;
|
|
11
|
+
stepY?: number;
|
|
8
12
|
color?: string;
|
|
9
13
|
}
|
|
10
14
|
declare function Grid({
|
|
11
15
|
step,
|
|
16
|
+
stepX,
|
|
17
|
+
stepY,
|
|
12
18
|
color
|
|
13
19
|
}: GridProps): ReactNode;
|
|
14
20
|
interface AxesProps {
|
|
15
21
|
color?: string;
|
|
16
22
|
ticks?: boolean;
|
|
17
23
|
step?: number;
|
|
24
|
+
/** Per-axis tick spacing overrides (win over `step`). */
|
|
25
|
+
stepX?: number;
|
|
26
|
+
stepY?: number;
|
|
18
27
|
/** Show the numeric value at each tick (and a "0" at the origin) so learners
|
|
19
|
-
* can READ coordinates off the grid
|
|
28
|
+
* can READ coordinates off the grid, needed for graphs where the answer is a
|
|
20
29
|
* point (systems, plotting). Default off (most figures want a clean axis). */
|
|
21
30
|
labels?: boolean;
|
|
22
31
|
}
|
|
@@ -24,6 +33,8 @@ declare function Axes({
|
|
|
24
33
|
color,
|
|
25
34
|
ticks,
|
|
26
35
|
step,
|
|
36
|
+
stepX,
|
|
37
|
+
stepY,
|
|
27
38
|
labels
|
|
28
39
|
}: AxesProps): ReactNode;
|
|
29
40
|
//#endregion
|
package/dist/primitives/Grid.mjs
CHANGED
|
@@ -17,11 +17,12 @@ function tickValues(min, max, step) {
|
|
|
17
17
|
for (let v = start; v <= max + step * 1e-6; v += step) out.push(Math.round(v / step) * step);
|
|
18
18
|
return out;
|
|
19
19
|
}
|
|
20
|
-
function Grid({ step, color = "var(--stage-grid)" }) {
|
|
20
|
+
function Grid({ step, stepX, stepY, color = "var(--stage-grid)" }) {
|
|
21
21
|
const c = useCoords();
|
|
22
|
-
const
|
|
22
|
+
const sx = stepX ?? step ?? niceStep(c.view.xMax - c.view.xMin);
|
|
23
|
+
const sy = stepY ?? step ?? niceStep(c.view.yMax - c.view.yMin);
|
|
23
24
|
const lines = [];
|
|
24
|
-
for (const x of tickValues(c.view.xMin, c.view.xMax,
|
|
25
|
+
for (const x of tickValues(c.view.xMin, c.view.xMax, sx)) {
|
|
25
26
|
const a = c.toPx(x, c.view.yMin);
|
|
26
27
|
const b = c.toPx(x, c.view.yMax);
|
|
27
28
|
lines.push(/* @__PURE__ */ jsx("line", {
|
|
@@ -33,7 +34,7 @@ function Grid({ step, color = "var(--stage-grid)" }) {
|
|
|
33
34
|
strokeWidth: 1
|
|
34
35
|
}, `gx${x}`));
|
|
35
36
|
}
|
|
36
|
-
for (const y of tickValues(c.view.yMin, c.view.yMax,
|
|
37
|
+
for (const y of tickValues(c.view.yMin, c.view.yMax, sy)) {
|
|
37
38
|
const a = c.toPx(c.view.xMin, y);
|
|
38
39
|
const b = c.toPx(c.view.xMax, y);
|
|
39
40
|
lines.push(/* @__PURE__ */ jsx("line", {
|
|
@@ -47,10 +48,15 @@ function Grid({ step, color = "var(--stage-grid)" }) {
|
|
|
47
48
|
}
|
|
48
49
|
return /* @__PURE__ */ jsx("g", { children: lines });
|
|
49
50
|
}
|
|
50
|
-
const fmtTick = (v) =>
|
|
51
|
-
|
|
51
|
+
const fmtTick = (v) => {
|
|
52
|
+
if (Number.isInteger(v)) return String(v);
|
|
53
|
+
const r = Math.round(v * 100) / 100;
|
|
54
|
+
return String(r);
|
|
55
|
+
};
|
|
56
|
+
function Axes({ color = "var(--stage-axis)", ticks = true, step, stepX, stepY, labels = false }) {
|
|
52
57
|
const c = useCoords();
|
|
53
|
-
const
|
|
58
|
+
const sx = stepX ?? step ?? niceStep(c.view.xMax - c.view.xMin);
|
|
59
|
+
const sy = stepY ?? step ?? niceStep(c.view.yMax - c.view.yMin);
|
|
54
60
|
const xAxisA = c.toPx(c.view.xMin, 0);
|
|
55
61
|
const xAxisB = c.toPx(c.view.xMax, 0);
|
|
56
62
|
const yAxisA = c.toPx(0, c.view.yMin);
|
|
@@ -87,7 +93,7 @@ function Axes({ color = "var(--stage-axis)", ticks = true, step, labels = false
|
|
|
87
93
|
strokeWidth: 1.5
|
|
88
94
|
}, "ay")];
|
|
89
95
|
if (ticks) {
|
|
90
|
-
for (const x of tickValues(c.view.xMin, c.view.xMax,
|
|
96
|
+
for (const x of tickValues(c.view.xMin, c.view.xMax, sx)) {
|
|
91
97
|
if (Math.abs(x) < 1e-9) continue;
|
|
92
98
|
const p = c.toPx(x, 0);
|
|
93
99
|
nodes.push(/* @__PURE__ */ jsx("line", {
|
|
@@ -100,7 +106,7 @@ function Axes({ color = "var(--stage-axis)", ticks = true, step, labels = false
|
|
|
100
106
|
}, `tx${x}`));
|
|
101
107
|
if (labels) nodes.push(num(`lx${x}`, p[0], p[1] + 14, fmtTick(x), "middle"));
|
|
102
108
|
}
|
|
103
|
-
for (const y of tickValues(c.view.yMin, c.view.yMax,
|
|
109
|
+
for (const y of tickValues(c.view.yMin, c.view.yMax, sy)) {
|
|
104
110
|
if (Math.abs(y) < 1e-9) continue;
|
|
105
111
|
const p = c.toPx(0, y);
|
|
106
112
|
nodes.push(/* @__PURE__ */ jsx("line", {
|
|
@@ -16,7 +16,7 @@ interface LabelProps {
|
|
|
16
16
|
}
|
|
17
17
|
/** Upright pixel-space text with a background-colored outline for legibility.
|
|
18
18
|
* Renders `_`/`^` as real SVG sub/superscripts via the shared `parseRichText`
|
|
19
|
-
* grammar (the same one labs' HTML `<RichText>` uses
|
|
19
|
+
* grammar (the same one labs' HTML `<RichText>` uses, one source of truth). */
|
|
20
20
|
declare function Label({
|
|
21
21
|
x,
|
|
22
22
|
y,
|
|
@@ -8,7 +8,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
8
8
|
//#region src/primitives/Label.tsx
|
|
9
9
|
/** Upright pixel-space text with a background-colored outline for legibility.
|
|
10
10
|
* Renders `_`/`^` as real SVG sub/superscripts via the shared `parseRichText`
|
|
11
|
-
* grammar (the same one labs' HTML `<RichText>` uses
|
|
11
|
+
* grammar (the same one labs' HTML `<RichText>` uses, one source of truth). */
|
|
12
12
|
function Label({ x, y, text, color = "var(--stage-fg)", size = 14, dx = 0, dy = 0, anchor = "middle", baseline = "middle", weight = 600 }) {
|
|
13
13
|
const [px, py] = useCoords().toPx(x, y);
|
|
14
14
|
const spans = parseRichText(text);
|
package/dist/primitives/Tex.mjs
CHANGED
|
@@ -8,11 +8,11 @@ import katex from "katex";
|
|
|
8
8
|
|
|
9
9
|
//#region src/primitives/Tex.tsx
|
|
10
10
|
/**
|
|
11
|
-
* Tex
|
|
11
|
+
* Tex, KaTeX maths in a <foreignObject>, rendered AT RENDER TIME (not in an
|
|
12
12
|
* effect) so the markup is identical on the server and the client: SSR/static
|
|
13
13
|
* exports show real maths (not raw `\left…\right`), there's no raw-then-hydrate
|
|
14
14
|
* flash, and hydration matches. KaTeX is a static import kept in its OWN module so
|
|
15
|
-
* only Tex's importers pull it (stage's neverBundle leaves `katex` external
|
|
15
|
+
* only Tex's importers pull it (stage's neverBundle leaves `katex` external, the
|
|
16
16
|
* consumer resolves the peer; non-Tex consumers never bundle it).
|
|
17
17
|
*
|
|
18
18
|
* Needs the consumer to load `katex/dist/katex.min.css` for proper glyphs.
|
package/dist/scene/Scene.mjs
CHANGED
|
@@ -14,7 +14,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
14
14
|
|
|
15
15
|
//#region src/scene/Scene.tsx
|
|
16
16
|
/**
|
|
17
|
-
* <Scene
|
|
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
|
*/
|
package/dist/scene/assets.d.mts
CHANGED
|
@@ -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
|
|
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
|
|
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.)
|
package/dist/scene/assets.mjs
CHANGED
|
@@ -12,13 +12,13 @@ function listAssets() {
|
|
|
12
12
|
return [...ASSETS.keys()];
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Typed read of an asset's `meta
|
|
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
|
|
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 };
|
package/dist/scene/commands.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { parentIds } from "./schema.mjs";
|
|
|
3
3
|
|
|
4
4
|
//#region src/scene/commands.ts
|
|
5
5
|
/**
|
|
6
|
-
* Commands
|
|
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
|
|
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)
|
|
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);
|
package/dist/scene/index.d.mts
CHANGED
|
@@ -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 };
|
package/dist/scene/index.mjs
CHANGED
|
@@ -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 };
|
package/dist/scene/migrate.mjs
CHANGED
|
@@ -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
|
|
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 {
|
package/dist/scene/sims.mjs
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/scene/types.d.mts
CHANGED
|
@@ -165,7 +165,7 @@ interface Binding {
|
|
|
165
165
|
props?: Record<string, unknown>;
|
|
166
166
|
}
|
|
167
167
|
/**
|
|
168
|
-
* Structured pedagogy for a lab
|
|
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
|
|
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
|
|
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
|
|
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';
|