@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,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,7 @@
1
+ import { Node } from "./ast.mjs";
2
+
3
+ //#region src/math/latex.d.ts
4
+ /** Render an AST as a LaTeX string (no surrounding `$`). */
5
+ declare function toLatex(node: Node): string;
6
+ //#endregion
7
+ export { 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,6 @@
1
+ import { Node } from "./ast.mjs";
2
+
3
+ //#region src/math/parse.d.ts
4
+ declare function parse(src: string): Node;
5
+ //#endregion
6
+ export { parse };
@@ -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 };