@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.
- package/LICENSE +21 -0
- package/README.md +65 -0
- package/dist/assets/index.d.mts +3 -0
- package/dist/assets/index.mjs +5 -0
- package/dist/assets/kit/glyphs.d.mts +61 -0
- package/dist/assets/kit/glyphs.mjs +256 -0
- package/dist/assets/kit/index.d.mts +2 -0
- package/dist/assets/kit/index.mjs +4 -0
- package/dist/assets/kit/svg-defs.d.mts +6 -0
- package/dist/assets/kit/svg-defs.mjs +137 -0
- package/dist/builder/Palette.d.mts +30 -0
- package/dist/builder/Palette.mjs +100 -0
- package/dist/builder/SceneBuilder.d.mts +26 -0
- package/dist/builder/SceneBuilder.mjs +340 -0
- package/dist/builder/controls.d.mts +18 -0
- package/dist/builder/controls.mjs +29 -0
- package/dist/builder/editor.d.mts +46 -0
- package/dist/builder/editor.mjs +130 -0
- package/dist/builder/icons.d.mts +14 -0
- package/dist/builder/icons.mjs +143 -0
- package/dist/builder/index.d.mts +6 -0
- package/dist/builder/index.mjs +7 -0
- package/dist/builder/tools.d.mts +47 -0
- package/dist/builder/tools.mjs +303 -0
- package/dist/chem/index.d.mts +118 -0
- package/dist/chem/index.mjs +131 -0
- package/dist/core/clock.d.mts +41 -0
- package/dist/core/clock.mjs +62 -0
- package/dist/core/context.d.mts +13 -0
- package/dist/core/context.mjs +29 -0
- package/dist/core/control.d.mts +58 -0
- package/dist/core/control.mjs +113 -0
- package/dist/core/coords.d.mts +51 -0
- package/dist/core/coords.mjs +44 -0
- package/dist/core/index.d.mts +8 -0
- package/dist/core/index.mjs +9 -0
- package/dist/core/learner.d.mts +44 -0
- package/dist/core/learner.mjs +23 -0
- package/dist/core/motion.d.mts +73 -0
- package/dist/core/motion.mjs +103 -0
- package/dist/core/richText.d.mts +28 -0
- package/dist/core/richText.mjs +52 -0
- package/dist/core/vec.d.mts +47 -0
- package/dist/core/vec.mjs +144 -0
- package/dist/field/index.d.mts +58 -0
- package/dist/field/index.mjs +156 -0
- package/dist/index.d.mts +54 -0
- package/dist/index.mjs +55 -0
- package/dist/interaction/MovableDot.d.mts +33 -0
- package/dist/interaction/MovableDot.mjs +66 -0
- package/dist/interaction/hitTest.d.mts +16 -0
- package/dist/interaction/hitTest.mjs +39 -0
- package/dist/interaction/index.d.mts +5 -0
- package/dist/interaction/index.mjs +6 -0
- package/dist/interaction/useDraggable.d.mts +51 -0
- package/dist/interaction/useDraggable.mjs +116 -0
- package/dist/interaction/useScreenToMath.d.mts +12 -0
- package/dist/interaction/useScreenToMath.mjs +25 -0
- package/dist/logic/ast.d.mts +29 -0
- package/dist/logic/ast.mjs +37 -0
- package/dist/logic/index.d.mts +25 -0
- package/dist/logic/index.mjs +36 -0
- package/dist/logic/latex.d.mts +6 -0
- package/dist/logic/latex.mjs +33 -0
- package/dist/logic/minimize.d.mts +37 -0
- package/dist/logic/minimize.mjs +175 -0
- package/dist/logic/parse.d.mts +6 -0
- package/dist/logic/parse.mjs +68 -0
- package/dist/logic/table.d.mts +24 -0
- package/dist/logic/table.mjs +65 -0
- package/dist/logic/tokenize.mjs +258 -0
- package/dist/math/ast.d.mts +35 -0
- package/dist/math/ast.mjs +94 -0
- package/dist/math/calculus.d.mts +9 -0
- package/dist/math/calculus.mjs +188 -0
- package/dist/math/defs.mjs +51 -0
- package/dist/math/index.d.mts +26 -0
- package/dist/math/index.mjs +42 -0
- package/dist/math/latex.d.mts +7 -0
- package/dist/math/latex.mjs +115 -0
- package/dist/math/parse.d.mts +6 -0
- package/dist/math/parse.mjs +91 -0
- package/dist/math/tokenize.mjs +112 -0
- package/dist/primitives/CanvasLayer.d.mts +26 -0
- package/dist/primitives/CanvasLayer.mjs +87 -0
- package/dist/primitives/Dot.d.mts +27 -0
- package/dist/primitives/Dot.mjs +40 -0
- package/dist/primitives/Grid.d.mts +30 -0
- package/dist/primitives/Grid.mjs +122 -0
- package/dist/primitives/Label.d.mts +33 -0
- package/dist/primitives/Label.mjs +45 -0
- package/dist/primitives/Lines.d.mts +50 -0
- package/dist/primitives/Lines.mjs +82 -0
- package/dist/primitives/Plot.d.mts +50 -0
- package/dist/primitives/Plot.mjs +87 -0
- package/dist/primitives/Shapes.d.mts +76 -0
- package/dist/primitives/Shapes.mjs +79 -0
- package/dist/primitives/Tex.d.mts +27 -0
- package/dist/primitives/Tex.mjs +54 -0
- package/dist/primitives/index.d.mts +10 -0
- package/dist/primitives/index.mjs +10 -0
- package/dist/primitives/props.d.mts +15 -0
- package/dist/scene/Scene.d.mts +31 -0
- package/dist/scene/Scene.mjs +168 -0
- package/dist/scene/assets.d.mts +37 -0
- package/dist/scene/assets.mjs +31 -0
- package/dist/scene/commands.d.mts +47 -0
- package/dist/scene/commands.mjs +162 -0
- package/dist/scene/evaluators.d.mts +17 -0
- package/dist/scene/evaluators.mjs +210 -0
- package/dist/scene/index.d.mts +13 -0
- package/dist/scene/index.mjs +13 -0
- package/dist/scene/migrate.d.mts +11 -0
- package/dist/scene/migrate.mjs +35 -0
- package/dist/scene/render.d.mts +14 -0
- package/dist/scene/render.mjs +126 -0
- package/dist/scene/resolve.d.mts +12 -0
- package/dist/scene/resolve.mjs +95 -0
- package/dist/scene/schema.d.mts +9 -0
- package/dist/scene/schema.mjs +51 -0
- package/dist/scene/sims.d.mts +18 -0
- package/dist/scene/sims.mjs +54 -0
- package/dist/scene/store.d.mts +22 -0
- package/dist/scene/store.mjs +37 -0
- package/dist/scene/types.d.mts +228 -0
- package/dist/scene/types.mjs +27 -0
- package/dist/sim/equilibrium.d.mts +36 -0
- package/dist/sim/equilibrium.mjs +54 -0
- package/dist/sim/index.d.mts +9 -0
- package/dist/sim/index.mjs +9 -0
- package/dist/sim/particles.d.mts +43 -0
- package/dist/sim/particles.mjs +136 -0
- package/dist/sim/rate.d.mts +36 -0
- package/dist/sim/rate.mjs +39 -0
- package/dist/sim/registry.d.mts +10 -0
- package/dist/sim/registry.mjs +27 -0
- package/dist/sim/sampler.d.mts +47 -0
- package/dist/sim/sampler.mjs +79 -0
- package/dist/sim/thermal.d.mts +78 -0
- package/dist/sim/thermal.mjs +172 -0
- package/dist/sim/types.d.mts +24 -0
- package/dist/sim/wave.d.mts +34 -0
- package/dist/sim/wave.mjs +61 -0
- package/dist/steps/index.d.mts +62 -0
- package/dist/steps/index.mjs +133 -0
- package/dist/thermo/index.d.mts +82 -0
- package/dist/thermo/index.mjs +133 -0
- package/dist/view/Stage.d.mts +39 -0
- package/dist/view/Stage.mjs +91 -0
- package/dist/view/index.d.mts +4 -0
- package/dist/view/index.mjs +5 -0
- package/dist/view/useElementSize.d.mts +13 -0
- package/dist/view/useElementSize.mjs +40 -0
- package/dist/view/useInView.d.mts +9 -0
- package/dist/view/useInView.mjs +33 -0
- package/package.json +141 -0
- package/styles.css +70 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { CONSTANTS, FN1, FN2 } from "./defs.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/math/ast.ts
|
|
4
|
+
/**
|
|
5
|
+
* expr/ast — the Abstract Syntax Tree: node types, evaluation, free-variable
|
|
6
|
+
* collection, and small builder helpers. The AST is the shared representation
|
|
7
|
+
* that evaluation, differentiation, simplification, and LaTeX printing all
|
|
8
|
+
* operate on.
|
|
9
|
+
*/
|
|
10
|
+
const num = (value) => ({
|
|
11
|
+
type: "num",
|
|
12
|
+
value
|
|
13
|
+
});
|
|
14
|
+
const variable = (name) => ({
|
|
15
|
+
type: "var",
|
|
16
|
+
name
|
|
17
|
+
});
|
|
18
|
+
const neg = (arg) => ({
|
|
19
|
+
type: "neg",
|
|
20
|
+
arg
|
|
21
|
+
});
|
|
22
|
+
const bin = (op, left, right) => ({
|
|
23
|
+
type: "binary",
|
|
24
|
+
op,
|
|
25
|
+
left,
|
|
26
|
+
right
|
|
27
|
+
});
|
|
28
|
+
const add = (l, r) => bin("+", l, r);
|
|
29
|
+
const sub = (l, r) => bin("-", l, r);
|
|
30
|
+
const mul = (l, r) => bin("*", l, r);
|
|
31
|
+
const div = (l, r) => bin("/", l, r);
|
|
32
|
+
const pow = (l, r) => bin("^", l, r);
|
|
33
|
+
const call = (fn, ...args) => ({
|
|
34
|
+
type: "call",
|
|
35
|
+
fn,
|
|
36
|
+
args
|
|
37
|
+
});
|
|
38
|
+
/** Evaluate the AST against a scope of variable values. Returns NaN for unknowns. */
|
|
39
|
+
function evaluate(node, scope) {
|
|
40
|
+
switch (node.type) {
|
|
41
|
+
case "num": return node.value;
|
|
42
|
+
case "var": {
|
|
43
|
+
const lc = node.name.toLowerCase();
|
|
44
|
+
return lc in CONSTANTS ? CONSTANTS[lc] ?? NaN : scope[node.name] ?? NaN;
|
|
45
|
+
}
|
|
46
|
+
case "neg": return -evaluate(node.arg, scope);
|
|
47
|
+
case "binary": {
|
|
48
|
+
const a = evaluate(node.left, scope);
|
|
49
|
+
const b = evaluate(node.right, scope);
|
|
50
|
+
switch (node.op) {
|
|
51
|
+
case "+": return a + b;
|
|
52
|
+
case "-": return a - b;
|
|
53
|
+
case "*": return a * b;
|
|
54
|
+
case "/": return a / b;
|
|
55
|
+
case "%": return a % b;
|
|
56
|
+
case "^": return a ** b;
|
|
57
|
+
default: return NaN;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
case "call": {
|
|
61
|
+
const f1 = FN1[node.fn];
|
|
62
|
+
const f2 = FN2[node.fn];
|
|
63
|
+
if (f1 && node.args[0]) return f1(evaluate(node.args[0], scope));
|
|
64
|
+
if (f2 && node.args[0] && node.args[1]) return f2(evaluate(node.args[0], scope), evaluate(node.args[1], scope));
|
|
65
|
+
return NaN;
|
|
66
|
+
}
|
|
67
|
+
default: return NaN;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Free variables referenced (excludes constants & function names). */
|
|
71
|
+
function freeVars(node, into = /* @__PURE__ */ new Set()) {
|
|
72
|
+
switch (node.type) {
|
|
73
|
+
case "var":
|
|
74
|
+
if (!(node.name.toLowerCase() in CONSTANTS)) into.add(node.name);
|
|
75
|
+
break;
|
|
76
|
+
case "neg":
|
|
77
|
+
freeVars(node.arg, into);
|
|
78
|
+
break;
|
|
79
|
+
case "binary":
|
|
80
|
+
freeVars(node.left, into);
|
|
81
|
+
freeVars(node.right, into);
|
|
82
|
+
break;
|
|
83
|
+
case "call":
|
|
84
|
+
for (const a of node.args) freeVars(a, into);
|
|
85
|
+
break;
|
|
86
|
+
default: break;
|
|
87
|
+
}
|
|
88
|
+
return into;
|
|
89
|
+
}
|
|
90
|
+
/** Compile to a fast closure (a thin wrapper over `evaluate`). */
|
|
91
|
+
const compileNode = (node) => (scope) => evaluate(node, scope);
|
|
92
|
+
|
|
93
|
+
//#endregion
|
|
94
|
+
export { add, bin, call, compileNode, div, evaluate, freeVars, mul, neg, num, pow, sub, variable };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Node } from "./ast.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/math/calculus.d.ts
|
|
4
|
+
/** Exact symbolic derivative, or `null` if a non-differentiable node is present. */
|
|
5
|
+
declare function differentiate(node: Node, x: string): Node | null;
|
|
6
|
+
/** Fold constants and trivial identities so derivative output is readable. */
|
|
7
|
+
declare function simplify(node: Node): Node;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { differentiate, simplify };
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { FN1, FN2 } from "./defs.mjs";
|
|
2
|
+
import { add, call, div, mul, neg, num, pow, sub } from "./ast.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/math/calculus.ts
|
|
5
|
+
/**
|
|
6
|
+
* expr/calculus — symbolic differentiation + algebraic simplification on the AST.
|
|
7
|
+
*
|
|
8
|
+
* `differentiate(node, x)` returns the exact derivative as a new AST, or `null`
|
|
9
|
+
* when the expression contains something not symbolically differentiable
|
|
10
|
+
* (floor/round/min/max/…). Callers should fall back to a numerical derivative
|
|
11
|
+
* in that case. `simplify(node)` folds constants and trivial identities so the
|
|
12
|
+
* derivative reads cleanly (e.g. `1*cos(x) + 0` → `cos(x)`).
|
|
13
|
+
*/
|
|
14
|
+
const LN10 = Math.LN10;
|
|
15
|
+
const LN2 = Math.LN2;
|
|
16
|
+
/** Outer derivative of a 1-arg function, given its argument node `u` (pre chain-rule). */
|
|
17
|
+
const DERIV = {
|
|
18
|
+
sin: (u) => call("cos", u),
|
|
19
|
+
cos: (u) => neg(call("sin", u)),
|
|
20
|
+
tan: (u) => div(num(1), pow(call("cos", u), num(2))),
|
|
21
|
+
cot: (u) => neg(div(num(1), pow(call("sin", u), num(2)))),
|
|
22
|
+
sec: (u) => mul(call("sec", u), call("tan", u)),
|
|
23
|
+
csc: (u) => neg(mul(call("csc", u), call("cot", u))),
|
|
24
|
+
asin: (u) => div(num(1), call("sqrt", sub(num(1), pow(u, num(2))))),
|
|
25
|
+
acos: (u) => neg(div(num(1), call("sqrt", sub(num(1), pow(u, num(2)))))),
|
|
26
|
+
atan: (u) => div(num(1), add(num(1), pow(u, num(2)))),
|
|
27
|
+
sinh: (u) => call("cosh", u),
|
|
28
|
+
cosh: (u) => call("sinh", u),
|
|
29
|
+
tanh: (u) => div(num(1), pow(call("cosh", u), num(2))),
|
|
30
|
+
exp: (u) => call("exp", u),
|
|
31
|
+
ln: (u) => div(num(1), u),
|
|
32
|
+
log: (u) => div(num(1), mul(u, num(LN10))),
|
|
33
|
+
log10: (u) => div(num(1), mul(u, num(LN10))),
|
|
34
|
+
log2: (u) => div(num(1), mul(u, num(LN2))),
|
|
35
|
+
sqrt: (u) => div(num(1), mul(num(2), call("sqrt", u))),
|
|
36
|
+
cbrt: (u) => div(num(1), mul(num(3), pow(call("cbrt", u), num(2)))),
|
|
37
|
+
abs: (u) => div(u, call("abs", u))
|
|
38
|
+
};
|
|
39
|
+
/** Exact symbolic derivative, or `null` if a non-differentiable node is present. */
|
|
40
|
+
function differentiate(node, x) {
|
|
41
|
+
switch (node.type) {
|
|
42
|
+
case "num": return num(0);
|
|
43
|
+
case "var": return num(node.name === x ? 1 : 0);
|
|
44
|
+
case "neg": {
|
|
45
|
+
const d = differentiate(node.arg, x);
|
|
46
|
+
return d && neg(d);
|
|
47
|
+
}
|
|
48
|
+
case "binary": {
|
|
49
|
+
const { op, left, right } = node;
|
|
50
|
+
const dl = differentiate(left, x);
|
|
51
|
+
const dr = differentiate(right, x);
|
|
52
|
+
if (op === "+") return dl && dr && add(dl, dr);
|
|
53
|
+
if (op === "-") return dl && dr && sub(dl, dr);
|
|
54
|
+
if (op === "*") return dl && dr && add(mul(dl, right), mul(left, dr));
|
|
55
|
+
if (op === "/") return dl && dr && div(sub(mul(dl, right), mul(left, dr)), pow(right, num(2)));
|
|
56
|
+
if (op === "^") {
|
|
57
|
+
if (!dl) return null;
|
|
58
|
+
if (right.type === "num") return mul(mul(num(right.value), pow(left, num(right.value - 1))), dl);
|
|
59
|
+
return dr && mul(pow(left, right), add(mul(dr, call("ln", left)), div(mul(right, dl), left)));
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
case "call": {
|
|
64
|
+
const rule = DERIV[node.fn];
|
|
65
|
+
const u = node.args[0];
|
|
66
|
+
if (!rule || !u || node.fn in FN2) return null;
|
|
67
|
+
const du = differentiate(u, x);
|
|
68
|
+
return du && mul(rule(u), du);
|
|
69
|
+
}
|
|
70
|
+
default: return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const isNum = (n, v) => n.type === "num" && (v === void 0 || n.value === v);
|
|
74
|
+
/** Fold constants and trivial identities so derivative output is readable. */
|
|
75
|
+
function simplify(node) {
|
|
76
|
+
switch (node.type) {
|
|
77
|
+
case "num":
|
|
78
|
+
case "var": return node;
|
|
79
|
+
case "neg": {
|
|
80
|
+
const a = simplify(node.arg);
|
|
81
|
+
if (a.type === "num") return num(-a.value);
|
|
82
|
+
if (a.type === "neg") return a.arg;
|
|
83
|
+
return neg(a);
|
|
84
|
+
}
|
|
85
|
+
case "binary": {
|
|
86
|
+
const l = simplify(node.left);
|
|
87
|
+
const r = simplify(node.right);
|
|
88
|
+
const op = node.op;
|
|
89
|
+
if (l.type === "num" && r.type === "num") {
|
|
90
|
+
const v = fold(op, l.value, r.value);
|
|
91
|
+
if (v !== null && Number.isFinite(v)) return num(v);
|
|
92
|
+
}
|
|
93
|
+
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
|
+
case "*":
|
|
103
|
+
if (isNum(l, 0) || isNum(r, 0)) return num(0);
|
|
104
|
+
if (isNum(l, 1)) return r;
|
|
105
|
+
if (isNum(r, 1)) return l;
|
|
106
|
+
if (isNum(l, -1)) return simplify(neg(r));
|
|
107
|
+
if (isNum(r, -1)) return simplify(neg(l));
|
|
108
|
+
if (l.type === "num" && r.type === "binary" && r.op === "*") {
|
|
109
|
+
if (r.left.type === "num") return {
|
|
110
|
+
type: "binary",
|
|
111
|
+
op: "*",
|
|
112
|
+
left: num(l.value * r.left.value),
|
|
113
|
+
right: r.right
|
|
114
|
+
};
|
|
115
|
+
if (r.right.type === "num") return {
|
|
116
|
+
type: "binary",
|
|
117
|
+
op: "*",
|
|
118
|
+
left: num(l.value * r.right.value),
|
|
119
|
+
right: r.left
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
if (r.type === "num" && l.type === "binary" && l.op === "*") {
|
|
123
|
+
if (l.left.type === "num") return {
|
|
124
|
+
type: "binary",
|
|
125
|
+
op: "*",
|
|
126
|
+
left: num(r.value * l.left.value),
|
|
127
|
+
right: l.right
|
|
128
|
+
};
|
|
129
|
+
if (l.right.type === "num") return {
|
|
130
|
+
type: "binary",
|
|
131
|
+
op: "*",
|
|
132
|
+
left: num(r.value * l.right.value),
|
|
133
|
+
right: l.left
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
break;
|
|
137
|
+
case "/":
|
|
138
|
+
if (isNum(r, 1)) return l;
|
|
139
|
+
if (isNum(l, 0)) return num(0);
|
|
140
|
+
break;
|
|
141
|
+
case "^":
|
|
142
|
+
if (isNum(r, 1)) return l;
|
|
143
|
+
if (isNum(r, 0)) return num(1);
|
|
144
|
+
if (isNum(l, 1)) return num(1);
|
|
145
|
+
break;
|
|
146
|
+
default: break;
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
type: "binary",
|
|
150
|
+
op,
|
|
151
|
+
left: l,
|
|
152
|
+
right: r
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
case "call": {
|
|
156
|
+
const args = node.args.map(simplify);
|
|
157
|
+
if (args.every((a) => a.type === "num")) {
|
|
158
|
+
const nums = args.map((a) => a.value);
|
|
159
|
+
const f1 = FN1[node.fn];
|
|
160
|
+
const f2 = FN2[node.fn];
|
|
161
|
+
let v;
|
|
162
|
+
if (f1 && nums.length === 1) v = f1(nums[0]);
|
|
163
|
+
else if (f2 && nums.length === 2) v = f2(nums[0], nums[1]);
|
|
164
|
+
if (v !== void 0 && Number.isFinite(v)) return num(v);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
type: "call",
|
|
168
|
+
fn: node.fn,
|
|
169
|
+
args
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
default: return node;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
function fold(op, a, b) {
|
|
176
|
+
switch (op) {
|
|
177
|
+
case "+": return a + b;
|
|
178
|
+
case "-": return a - b;
|
|
179
|
+
case "*": return a * b;
|
|
180
|
+
case "/": return a / b;
|
|
181
|
+
case "%": return a % b;
|
|
182
|
+
case "^": return a ** b;
|
|
183
|
+
default: return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
export { differentiate, simplify };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
//#region src/math/defs.ts
|
|
2
|
+
/**
|
|
3
|
+
* expr/defs — the function & constant tables shared by the tokenizer, evaluator,
|
|
4
|
+
* differentiator, and LaTeX printer. Pure data; no AST knowledge here.
|
|
5
|
+
*/
|
|
6
|
+
const CONSTANTS = {
|
|
7
|
+
pi: Math.PI,
|
|
8
|
+
e: Math.E,
|
|
9
|
+
tau: Math.PI * 2
|
|
10
|
+
};
|
|
11
|
+
const FN1 = {
|
|
12
|
+
sin: Math.sin,
|
|
13
|
+
cos: Math.cos,
|
|
14
|
+
tan: Math.tan,
|
|
15
|
+
cot: (x) => 1 / Math.tan(x),
|
|
16
|
+
sec: (x) => 1 / Math.cos(x),
|
|
17
|
+
csc: (x) => 1 / Math.sin(x),
|
|
18
|
+
asin: Math.asin,
|
|
19
|
+
acos: Math.acos,
|
|
20
|
+
atan: Math.atan,
|
|
21
|
+
sinh: Math.sinh,
|
|
22
|
+
cosh: Math.cosh,
|
|
23
|
+
tanh: Math.tanh,
|
|
24
|
+
sqrt: Math.sqrt,
|
|
25
|
+
cbrt: Math.cbrt,
|
|
26
|
+
abs: Math.abs,
|
|
27
|
+
exp: Math.exp,
|
|
28
|
+
ln: Math.log,
|
|
29
|
+
log: Math.log10,
|
|
30
|
+
log2: Math.log2,
|
|
31
|
+
log10: Math.log10,
|
|
32
|
+
floor: Math.floor,
|
|
33
|
+
ceil: Math.ceil,
|
|
34
|
+
round: Math.round,
|
|
35
|
+
trunc: Math.trunc,
|
|
36
|
+
sign: Math.sign
|
|
37
|
+
};
|
|
38
|
+
const FN2 = {
|
|
39
|
+
pow: Math.pow,
|
|
40
|
+
atan2: Math.atan2,
|
|
41
|
+
min: Math.min,
|
|
42
|
+
max: Math.max,
|
|
43
|
+
hypot: Math.hypot,
|
|
44
|
+
mod: (a, b) => a % b
|
|
45
|
+
};
|
|
46
|
+
const isFunction = (name) => name in FN1 || name in FN2;
|
|
47
|
+
/** Declared argument count for a function name, or null if unknown. */
|
|
48
|
+
const arityOf = (name) => name in FN1 ? 1 : name in FN2 ? 2 : null;
|
|
49
|
+
|
|
50
|
+
//#endregion
|
|
51
|
+
export { CONSTANTS, FN1, FN2, arityOf, isFunction };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { BinOp, Node, compileNode, evaluate, freeVars } from "./ast.mjs";
|
|
2
|
+
import { parse } from "./parse.mjs";
|
|
3
|
+
import { differentiate, simplify } from "./calculus.mjs";
|
|
4
|
+
import { toLatex } from "./latex.mjs";
|
|
5
|
+
|
|
6
|
+
//#region src/math/index.d.ts
|
|
7
|
+
interface CompiledExpr {
|
|
8
|
+
/** Evaluate against a scope of variable values (unknown vars → NaN). */
|
|
9
|
+
readonly fn: (scope: Record<string, number>) => number;
|
|
10
|
+
/** Free variables referenced (excludes constants & functions), e.g. `['x','a']`. */
|
|
11
|
+
readonly vars: string[];
|
|
12
|
+
/** The parsed syntax tree — for differentiation, LaTeX, or analysis. */
|
|
13
|
+
readonly ast: Node;
|
|
14
|
+
readonly error?: undefined;
|
|
15
|
+
}
|
|
16
|
+
interface ExprError {
|
|
17
|
+
readonly error: string;
|
|
18
|
+
readonly fn?: undefined;
|
|
19
|
+
readonly vars?: undefined;
|
|
20
|
+
readonly ast?: undefined;
|
|
21
|
+
}
|
|
22
|
+
type ExprResult = CompiledExpr | ExprError;
|
|
23
|
+
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }` — never throws. */
|
|
24
|
+
declare function compileExpr(src: string): ExprResult;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { type BinOp, CompiledExpr, ExprError, ExprResult, type Node, compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { compileNode, evaluate, freeVars } from "./ast.mjs";
|
|
2
|
+
import { parse } from "./parse.mjs";
|
|
3
|
+
import { differentiate, simplify } from "./calculus.mjs";
|
|
4
|
+
import { toLatex } from "./latex.mjs";
|
|
5
|
+
|
|
6
|
+
//#region src/math/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* expr — a tiny, safe, dependency-free symbolic math engine.
|
|
9
|
+
*
|
|
10
|
+
* Pipeline: `tokenize → parse (AST) → evaluate`. On top of the AST it offers
|
|
11
|
+
* exact `differentiate`, `simplify`, and `toLatex`, so the same typed formula
|
|
12
|
+
* can be plotted, differentiated for an exact tangent, and rendered beautifully.
|
|
13
|
+
* No `eval`/`new Function`.
|
|
14
|
+
*
|
|
15
|
+
* `compileExpr(src)` is the stable, simple entry point (returns a fast `fn` plus
|
|
16
|
+
* the free `vars` and the parsed `ast`); the lower-level pieces are exported for
|
|
17
|
+
* widgets that need the tree (derivative explorers, LaTeX labels, step-by-step).
|
|
18
|
+
*
|
|
19
|
+
* Supports: numbers (incl. `1.5e3`), variable `x` + named params, constants
|
|
20
|
+
* (`pi`/`e`/`tau`), operators `+ - * / % ^` (^ right-assoc) and unary minus,
|
|
21
|
+
* implicit multiplication (`2x`, `3sin(x)`), and functions `sin cos tan cot sec
|
|
22
|
+
* csc asin acos atan sinh cosh tanh sqrt cbrt abs exp ln log log2 log10 floor
|
|
23
|
+
* ceil round sign` (+ 2-arg `pow atan2 min max mod hypot`).
|
|
24
|
+
*/
|
|
25
|
+
/** Compile a formula string. Returns `{ fn, vars, ast }` or `{ error }` — never throws. */
|
|
26
|
+
function compileExpr(src) {
|
|
27
|
+
if (!src || !src.trim()) return { error: "Empty expression" };
|
|
28
|
+
let ast;
|
|
29
|
+
try {
|
|
30
|
+
ast = parse(src);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
return { error: e instanceof Error ? e.message : "Parse error" };
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
fn: (scope) => evaluate(ast, scope),
|
|
36
|
+
vars: [...freeVars(ast)],
|
|
37
|
+
ast
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//#endregion
|
|
42
|
+
export { compileExpr, compileNode, differentiate, evaluate, freeVars, parse, simplify, toLatex };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
//#region src/math/latex.ts
|
|
2
|
+
const GREEK = new Set([
|
|
3
|
+
"alpha",
|
|
4
|
+
"beta",
|
|
5
|
+
"gamma",
|
|
6
|
+
"delta",
|
|
7
|
+
"epsilon",
|
|
8
|
+
"zeta",
|
|
9
|
+
"eta",
|
|
10
|
+
"theta",
|
|
11
|
+
"iota",
|
|
12
|
+
"kappa",
|
|
13
|
+
"lambda",
|
|
14
|
+
"mu",
|
|
15
|
+
"nu",
|
|
16
|
+
"xi",
|
|
17
|
+
"pi",
|
|
18
|
+
"rho",
|
|
19
|
+
"sigma",
|
|
20
|
+
"tau",
|
|
21
|
+
"phi",
|
|
22
|
+
"chi",
|
|
23
|
+
"psi",
|
|
24
|
+
"omega"
|
|
25
|
+
]);
|
|
26
|
+
const NAMED_FN = new Set([
|
|
27
|
+
"sin",
|
|
28
|
+
"cos",
|
|
29
|
+
"tan",
|
|
30
|
+
"cot",
|
|
31
|
+
"sec",
|
|
32
|
+
"csc",
|
|
33
|
+
"sinh",
|
|
34
|
+
"cosh",
|
|
35
|
+
"tanh",
|
|
36
|
+
"log",
|
|
37
|
+
"ln",
|
|
38
|
+
"exp"
|
|
39
|
+
]);
|
|
40
|
+
function texVar(name) {
|
|
41
|
+
return GREEK.has(name) ? `\\${name}` : name;
|
|
42
|
+
}
|
|
43
|
+
function texNum(v) {
|
|
44
|
+
if (!Number.isFinite(v)) return v > 0 ? "\\infty" : "-\\infty";
|
|
45
|
+
return String(+v.toFixed(6));
|
|
46
|
+
}
|
|
47
|
+
function texP(node) {
|
|
48
|
+
switch (node.type) {
|
|
49
|
+
case "num": return {
|
|
50
|
+
s: texNum(node.value),
|
|
51
|
+
prec: node.value < 0 ? 1 : 4
|
|
52
|
+
};
|
|
53
|
+
case "var": return {
|
|
54
|
+
s: texVar(node.name),
|
|
55
|
+
prec: 4
|
|
56
|
+
};
|
|
57
|
+
case "neg": return {
|
|
58
|
+
s: `-${wrap(node.arg, 2)}`,
|
|
59
|
+
prec: 2
|
|
60
|
+
};
|
|
61
|
+
case "call": return {
|
|
62
|
+
s: texCall(node),
|
|
63
|
+
prec: 4
|
|
64
|
+
};
|
|
65
|
+
case "binary": {
|
|
66
|
+
const { op } = node;
|
|
67
|
+
if (op === "/") return {
|
|
68
|
+
s: `\\frac{${wrap(node.left, 0)}}{${wrap(node.right, 0)}}`,
|
|
69
|
+
prec: 4
|
|
70
|
+
};
|
|
71
|
+
if (op === "^") return {
|
|
72
|
+
s: `${wrap(node.left, 4)}^{${wrap(node.right, 0)}}`,
|
|
73
|
+
prec: 3
|
|
74
|
+
};
|
|
75
|
+
if (op === "+" || op === "-") return {
|
|
76
|
+
s: `${wrap(node.left, 1)} ${op} ${wrap(node.right, 1)}`,
|
|
77
|
+
prec: 1
|
|
78
|
+
};
|
|
79
|
+
if (op === "%") return {
|
|
80
|
+
s: `${wrap(node.left, 2)} \\bmod ${wrap(node.right, 2)}`,
|
|
81
|
+
prec: 2
|
|
82
|
+
};
|
|
83
|
+
const sep = node.right.type === "num" ? " \\cdot " : "";
|
|
84
|
+
return {
|
|
85
|
+
s: `${wrap(node.left, 2)}${sep}${wrap(node.right, 2)}`,
|
|
86
|
+
prec: 2
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
default: return {
|
|
90
|
+
s: "",
|
|
91
|
+
prec: 4
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function wrap(node, parentPrec) {
|
|
96
|
+
const { s, prec } = texP(node);
|
|
97
|
+
return prec < parentPrec ? `\\left(${s}\\right)` : s;
|
|
98
|
+
}
|
|
99
|
+
function texCall(node) {
|
|
100
|
+
const a0 = node.args[0];
|
|
101
|
+
if (!a0) return `\\operatorname{${node.fn}}()`;
|
|
102
|
+
if (node.fn === "sqrt") return `\\sqrt{${wrap(a0, 0)}}`;
|
|
103
|
+
if (node.fn === "cbrt") return `\\sqrt[3]{${wrap(a0, 0)}}`;
|
|
104
|
+
if (node.fn === "abs") return `\\left|${wrap(a0, 0)}\\right|`;
|
|
105
|
+
if (node.fn === "exp") return `e^{${wrap(a0, 0)}}`;
|
|
106
|
+
const args = node.args.map((a) => wrap(a, 0)).join(", ");
|
|
107
|
+
return `${NAMED_FN.has(node.fn) ? `\\${node.fn}` : `\\operatorname{${node.fn}}`}\\left(${args}\\right)`;
|
|
108
|
+
}
|
|
109
|
+
/** Render an AST as a LaTeX string (no surrounding `$`). */
|
|
110
|
+
function toLatex(node) {
|
|
111
|
+
return texP(node).s;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
export { toLatex };
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { arityOf } from "./defs.mjs";
|
|
2
|
+
import { tokenize } from "./tokenize.mjs";
|
|
3
|
+
import { bin, call, neg, num, variable } from "./ast.mjs";
|
|
4
|
+
|
|
5
|
+
//#region src/math/parse.ts
|
|
6
|
+
/**
|
|
7
|
+
* expr/parse — token stream → AST, via precedence-climbing (Pratt).
|
|
8
|
+
*
|
|
9
|
+
* Binary precedence: `+ -` < `* / %` < `^` (right-assoc). Unary minus binds
|
|
10
|
+
* looser than `^` (so `-2^2 = -(2^2) = -4`) but tighter than `*`. Throws a
|
|
11
|
+
* descriptive Error on malformed input — the parser is the single source of
|
|
12
|
+
* truth for "is this expression well-formed?".
|
|
13
|
+
*/
|
|
14
|
+
const BIN_PREC = {
|
|
15
|
+
"+": 2,
|
|
16
|
+
"-": 2,
|
|
17
|
+
"*": 3,
|
|
18
|
+
"/": 3,
|
|
19
|
+
"%": 3,
|
|
20
|
+
"^": 5
|
|
21
|
+
};
|
|
22
|
+
const RIGHT = new Set(["^"]);
|
|
23
|
+
const UNARY_PREC = 4;
|
|
24
|
+
function parse(src) {
|
|
25
|
+
const toks = tokenize(src);
|
|
26
|
+
let pos = 0;
|
|
27
|
+
const peek = () => toks[pos];
|
|
28
|
+
const next = () => toks[pos++];
|
|
29
|
+
const expect = (t) => {
|
|
30
|
+
const tok = next();
|
|
31
|
+
if (!tok || tok.t !== t) throw new Error(`Expected ${t}`);
|
|
32
|
+
};
|
|
33
|
+
function parsePrimary() {
|
|
34
|
+
const t = next();
|
|
35
|
+
if (!t) throw new Error("Unexpected end of expression");
|
|
36
|
+
if (t.t === "num") return num(t.v);
|
|
37
|
+
if (t.t === "var") return variable(t.v);
|
|
38
|
+
if (t.t === "fn") {
|
|
39
|
+
expect("lp");
|
|
40
|
+
const args = [];
|
|
41
|
+
if (peek()?.t !== "rp") {
|
|
42
|
+
args.push(parseExpr(0));
|
|
43
|
+
while (peek()?.t === "comma") {
|
|
44
|
+
next();
|
|
45
|
+
args.push(parseExpr(0));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
expect("rp");
|
|
49
|
+
const a = arityOf(t.v);
|
|
50
|
+
if (a !== null && args.length !== a) throw new Error(`${t.v}() expects ${a} argument(s), got ${args.length}`);
|
|
51
|
+
return call(t.v, ...args);
|
|
52
|
+
}
|
|
53
|
+
if (t.t === "lp") {
|
|
54
|
+
const e = parseExpr(0);
|
|
55
|
+
expect("rp");
|
|
56
|
+
return e;
|
|
57
|
+
}
|
|
58
|
+
throw new Error("Unexpected token");
|
|
59
|
+
}
|
|
60
|
+
function parseUnary() {
|
|
61
|
+
const t = peek();
|
|
62
|
+
if (t && t.t === "op" && t.v === "-") {
|
|
63
|
+
next();
|
|
64
|
+
return neg(parseExpr(UNARY_PREC));
|
|
65
|
+
}
|
|
66
|
+
if (t && t.t === "op" && t.v === "+") {
|
|
67
|
+
next();
|
|
68
|
+
return parseUnary();
|
|
69
|
+
}
|
|
70
|
+
return parsePrimary();
|
|
71
|
+
}
|
|
72
|
+
function parseExpr(minPrec) {
|
|
73
|
+
let left = parseUnary();
|
|
74
|
+
for (;;) {
|
|
75
|
+
const t = peek();
|
|
76
|
+
if (!t || t.t !== "op") break;
|
|
77
|
+
const prec = BIN_PREC[t.v];
|
|
78
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
79
|
+
next();
|
|
80
|
+
const nextMin = RIGHT.has(t.v) ? prec : prec + 1;
|
|
81
|
+
left = bin(t.v, left, parseExpr(nextMin));
|
|
82
|
+
}
|
|
83
|
+
return left;
|
|
84
|
+
}
|
|
85
|
+
const node = parseExpr(0);
|
|
86
|
+
if (pos !== toks.length) throw new Error("Unexpected trailing input");
|
|
87
|
+
return node;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
export { parse };
|