@motionscript/plot 0.0.0-stage → 0.1.0-alpha.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 (102) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/LICENSE +201 -0
  3. package/dist/browser/chunks/chunk-RCFWYW6O.js +2 -0
  4. package/dist/browser/chunks/chunk-RCFWYW6O.js.map +7 -0
  5. package/dist/browser/index.js +84 -0
  6. package/dist/browser/index.js.map +7 -0
  7. package/dist/browser/kit.js +2 -0
  8. package/dist/browser/kit.js.map +7 -0
  9. package/dist/browser/manifest.json +12 -0
  10. package/dist/engine.d.ts +14 -0
  11. package/dist/engine.d.ts.map +1 -0
  12. package/dist/engine.js +14 -0
  13. package/dist/engine.js.map +1 -0
  14. package/dist/graph2d/curve-cache.d.ts +60 -0
  15. package/dist/graph2d/curve-cache.d.ts.map +1 -0
  16. package/dist/graph2d/curve-cache.js +79 -0
  17. package/dist/graph2d/curve-cache.js.map +1 -0
  18. package/dist/graph2d/curve-error.d.ts +12 -0
  19. package/dist/graph2d/curve-error.d.ts.map +1 -0
  20. package/dist/graph2d/curve-error.js +15 -0
  21. package/dist/graph2d/curve-error.js.map +1 -0
  22. package/dist/graph2d/curve.d.ts +104 -0
  23. package/dist/graph2d/curve.d.ts.map +1 -0
  24. package/dist/graph2d/curve.js +531 -0
  25. package/dist/graph2d/curve.js.map +1 -0
  26. package/dist/graph2d/graph2d.d.ts +164 -0
  27. package/dist/graph2d/graph2d.d.ts.map +1 -0
  28. package/dist/graph2d/graph2d.js +404 -0
  29. package/dist/graph2d/graph2d.js.map +1 -0
  30. package/dist/graph2d/index.d.ts +32 -0
  31. package/dist/graph2d/index.d.ts.map +1 -0
  32. package/dist/graph2d/index.js +32 -0
  33. package/dist/graph2d/index.js.map +1 -0
  34. package/dist/graph2d/plane-fill.d.ts +110 -0
  35. package/dist/graph2d/plane-fill.d.ts.map +1 -0
  36. package/dist/graph2d/plane-fill.js +248 -0
  37. package/dist/graph2d/plane-fill.js.map +1 -0
  38. package/dist/graph2d/plane.d.ts +179 -0
  39. package/dist/graph2d/plane.d.ts.map +1 -0
  40. package/dist/graph2d/plane.js +359 -0
  41. package/dist/graph2d/plane.js.map +1 -0
  42. package/dist/graph2d/shared.d.ts +40 -0
  43. package/dist/graph2d/shared.d.ts.map +1 -0
  44. package/dist/graph2d/shared.js +70 -0
  45. package/dist/graph2d/shared.js.map +1 -0
  46. package/dist/graph3d/expression.d.ts +30 -0
  47. package/dist/graph3d/expression.d.ts.map +1 -0
  48. package/dist/graph3d/expression.js +35 -0
  49. package/dist/graph3d/expression.js.map +1 -0
  50. package/dist/graph3d/graph3d.d.ts +186 -0
  51. package/dist/graph3d/graph3d.d.ts.map +1 -0
  52. package/dist/graph3d/graph3d.js +404 -0
  53. package/dist/graph3d/graph3d.js.map +1 -0
  54. package/dist/graph3d/index.d.ts +22 -0
  55. package/dist/graph3d/index.d.ts.map +1 -0
  56. package/dist/graph3d/index.js +22 -0
  57. package/dist/graph3d/index.js.map +1 -0
  58. package/dist/graph3d/shared.d.ts +61 -0
  59. package/dist/graph3d/shared.d.ts.map +1 -0
  60. package/dist/graph3d/shared.js +101 -0
  61. package/dist/graph3d/shared.js.map +1 -0
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +4 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/kit/equations.d.ts +56 -0
  67. package/dist/kit/equations.d.ts.map +1 -0
  68. package/dist/kit/equations.js +63 -0
  69. package/dist/kit/equations.js.map +1 -0
  70. package/dist/kit/expression.d.ts +60 -0
  71. package/dist/kit/expression.d.ts.map +1 -0
  72. package/dist/kit/expression.js +268 -0
  73. package/dist/kit/expression.js.map +1 -0
  74. package/dist/kit/index.d.ts +15 -0
  75. package/dist/kit/index.d.ts.map +1 -0
  76. package/dist/kit/index.js +13 -0
  77. package/dist/kit/index.js.map +1 -0
  78. package/dist/nodes.d.ts +19 -0
  79. package/dist/nodes.d.ts.map +1 -0
  80. package/dist/nodes.js +19 -0
  81. package/dist/nodes.js.map +1 -0
  82. package/package.json +68 -3
  83. package/registry.json +34 -0
  84. package/src/engine.ts +13 -0
  85. package/src/graph2d/curve-cache.ts +103 -0
  86. package/src/graph2d/curve-error.ts +19 -0
  87. package/src/graph2d/curve.ts +657 -0
  88. package/src/graph2d/graph2d.ts +586 -0
  89. package/src/graph2d/index.ts +31 -0
  90. package/src/graph2d/plane-fill.ts +308 -0
  91. package/src/graph2d/plane.ts +457 -0
  92. package/src/graph2d/shared.ts +103 -0
  93. package/src/graph3d/expression.ts +51 -0
  94. package/src/graph3d/graph3d.ts +614 -0
  95. package/src/graph3d/index.ts +21 -0
  96. package/src/graph3d/shared.ts +143 -0
  97. package/src/index.ts +3 -0
  98. package/src/kit/equations.ts +102 -0
  99. package/src/kit/expression.ts +323 -0
  100. package/src/kit/index.ts +29 -0
  101. package/src/nodes.ts +19 -0
  102. package/README.md +0 -4
@@ -0,0 +1,101 @@
1
+ /**
2
+ * The mappers, tweens and small maths the {@link Graph3D} node is built from.
3
+ *
4
+ * These exist because a `@property` is only as good as the `mapper`/`tween` it
5
+ * is declared with: the mapper turns a loose authored value (a CSS colour, a
6
+ * partly-filled options bag) into one canonical internal shape, and the tween is
7
+ * what makes `node.to({ … })` *interpolate* that shape instead of holding the
8
+ * old value and snapping at the end. Core's own attributes are built the same
9
+ * way — see `cornerRadiusOps` behind `Rect.cornerRadius`.
10
+ *
11
+ * The colour, number and camera helpers this used to define now live in
12
+ * `nodes/view3d-kit`, because the Protein node needs the same six and none of
13
+ * them were ever about graphs. The equation list's shape and its tween moved the
14
+ * same way, into `nodes/graph-kit`, once the 2D graph needed both. They are
15
+ * re-exported here so this stays the one import a graph3d module reaches for.
16
+ */
17
+ import { clamp } from "@motionscript/core";
18
+ import { lerpGraphEquations, } from "../kit/equations.js";
19
+ export { lerpColor3D, lerpCount, lerpFinite, cameraOrbit, resolveColor3D, snapFlag, } from "@motionscript/core/component";
20
+ // --- Sampling --------------------------------------------------------------
21
+ /**
22
+ * Makes a sampled height safe to put in a vertex buffer.
23
+ *
24
+ * `NaN` (from `sqrt` of a negative, `log` of zero) poisons a whole triangle and
25
+ * leaves a hole; an asymptote (`1/x`, `tan`) produces values large enough to
26
+ * stretch the mesh off screen and wreck the camera framing. Both are flattened
27
+ * rather than dropped, so the surface stays continuous.
28
+ */
29
+ export function sanitizeHeight(value, maxHeight) {
30
+ if (!Number.isFinite(value))
31
+ return 0;
32
+ return clamp(value, -maxHeight, maxHeight);
33
+ }
34
+ /**
35
+ * Tween for the equation list — the shared match-by-id walk, with the one thing
36
+ * a surface does differently: mid-morph its *height* is sampled from both
37
+ * functions and blended, so it deforms into the new one rather than popping.
38
+ *
39
+ * The blend deliberately returns a **fresh closure** per frame, which is what
40
+ * {@link surfaceRevision} reads as "re-evaluate this mesh".
41
+ */
42
+ export function lerpEquations(from, to, t) {
43
+ return lerpGraphEquations(from, to, t, blendSurface);
44
+ }
45
+ /** How two surfaces' heights blend mid-tween. */
46
+ function blendSurface(a, b, t) {
47
+ return (x, y) => {
48
+ const start = a(x, y);
49
+ return start + (b(x, y) - start) * t;
50
+ };
51
+ }
52
+ /**
53
+ * A small integer standing for a compiled expression's *identity*.
54
+ *
55
+ * `Geo.parametric`'s `revision` is a number, and what a surface's vertex callback
56
+ * actually depends on includes a function — so the function has to be reduced to
57
+ * something numeric. Identity is the right comparison here rather than the source
58
+ * text: `compileExpressionCached` memoises by source, so two equal sources are
59
+ * already `===`, and a mid-morph blend (see {@link blendEquation}) deliberately
60
+ * builds a *fresh* closure each frame, which is exactly when the surface must be
61
+ * re-evaluated.
62
+ *
63
+ * A `WeakMap` so retiring an expression doesn't pin it, and ids are never reused
64
+ * — a recycled id could make a changed surface look unchanged.
65
+ */
66
+ const expressionIds = new WeakMap();
67
+ let nextExpressionId = 1;
68
+ function expressionId(sample) {
69
+ let id = expressionIds.get(sample);
70
+ if (id === undefined) {
71
+ id = nextExpressionId++;
72
+ expressionIds.set(sample, id);
73
+ }
74
+ return id;
75
+ }
76
+ /**
77
+ * The `revision` for one surface: a value that changes exactly when its vertex
78
+ * callback would return something different.
79
+ *
80
+ * The callback in `Graph3D.addSurface` closes over three things — the compiled
81
+ * expression, the domain (as `span`/`domain`) and `maxHeight` — so those three
82
+ * are what this covers, and nothing else. `segments` is deliberately absent: it
83
+ * is plain data on the descriptor that the renderer already compares itself.
84
+ *
85
+ * Mixed by FNV-1a over the three values' text rather than by arithmetic, because
86
+ * `domain` and `maxHeight` are tweenable floats and there is no cheap numeric
87
+ * combination of two floats and an int that doesn't collide somewhere. A
88
+ * collision here would show as a surface that refuses to update, which is a
89
+ * miserable bug to find; the string is a few dozen characters per surface per
90
+ * frame, against the ~6.6k vertex evaluations it exists to avoid.
91
+ */
92
+ export function surfaceRevision(sample, domain, maxHeight) {
93
+ const key = `${expressionId(sample)}|${domain}|${maxHeight}`;
94
+ let hash = 0x811c9dc5;
95
+ for (let i = 0; i < key.length; i++) {
96
+ hash ^= key.charCodeAt(i);
97
+ hash = Math.imul(hash, 0x01000193);
98
+ }
99
+ return hash >>> 0;
100
+ }
101
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/graph3d/shared.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;AAE1C,OAAO,EACL,kBAAkB,GAEnB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,EACL,WAAW,EACX,SAAS,EACT,UAAU,EACV,WAAW,EACX,cAAc,EACd,QAAQ,GAET,MAAM,8BAA8B,CAAA;AAIrC,8EAA8E;AAE9E;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,KAAa,EAAE,SAAiB;IAC7D,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IACrC,OAAO,KAAK,CAAC,KAAK,EAAE,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;AAC5C,CAAC;AAWD;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAC3B,IAAwB,EACxB,EAAsB,EACtB,CAAS;IAET,OAAO,kBAAkB,CAAC,IAAI,EAAE,EAAE,EAAE,CAAC,EAAE,YAAY,CAAC,CAAA;AACtD,CAAC;AAED,iDAAiD;AACjD,SAAS,YAAY,CACnB,CAAqB,EACrB,CAAqB,EACrB,CAAS;IAET,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACd,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QACrB,OAAO,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;IACtC,CAAC,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,aAAa,GAAG,IAAI,OAAO,EAA8B,CAAA;AAC/D,IAAI,gBAAgB,GAAG,CAAC,CAAA;AAExB,SAAS,YAAY,CAAC,MAA0B;IAC9C,IAAI,EAAE,GAAG,aAAa,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IAClC,IAAI,EAAE,KAAK,SAAS,EAAE,CAAC;QACrB,EAAE,GAAG,gBAAgB,EAAE,CAAA;QACvB,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;IAC/B,CAAC;IACD,OAAO,EAAE,CAAA;AACX,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAC7B,MAA0B,EAC1B,MAAc,EACd,SAAiB;IAEjB,MAAM,GAAG,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,SAAS,EAAE,CAAA;IAC5D,IAAI,IAAI,GAAG,UAAU,CAAA;IACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,IAAI,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAA;QACzB,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IACpC,CAAC;IACD,OAAO,IAAI,KAAK,CAAC,CAAA;AACnB,CAAC","sourcesContent":["/**\n * The mappers, tweens and small maths the {@link Graph3D} node is built from.\n *\n * These exist because a `@property` is only as good as the `mapper`/`tween` it\n * is declared with: the mapper turns a loose authored value (a CSS colour, a\n * partly-filled options bag) into one canonical internal shape, and the tween is\n * what makes `node.to({ … })` *interpolate* that shape instead of holding the\n * old value and snapping at the end. Core's own attributes are built the same\n * way — see `cornerRadiusOps` behind `Rect.cornerRadius`.\n *\n * The colour, number and camera helpers this used to define now live in\n * `nodes/view3d-kit`, because the Protein node needs the same six and none of\n * them were ever about graphs. The equation list's shape and its tween moved the\n * same way, into `nodes/graph-kit`, once the 2D graph needed both. They are\n * re-exported here so this stays the one import a graph3d module reaches for.\n */\n\nimport { clamp } from \"@motionscript/core\"\n\nimport {\n lerpGraphEquations,\n type GraphEquationResolved,\n} from \"../kit/equations\"\n\nexport {\n lerpColor3D,\n lerpCount,\n lerpFinite,\n cameraOrbit,\n resolveColor3D,\n snapFlag,\n type Orbit,\n} from \"@motionscript/core/component\"\n\nimport type { CompiledExpression } from \"./expression\"\n\n// --- Sampling --------------------------------------------------------------\n\n/**\n * Makes a sampled height safe to put in a vertex buffer.\n *\n * `NaN` (from `sqrt` of a negative, `log` of zero) poisons a whole triangle and\n * leaves a hole; an asymptote (`1/x`, `tan`) produces values large enough to\n * stretch the mesh off screen and wreck the camera framing. Both are flattened\n * rather than dropped, so the surface stays continuous.\n */\nexport function sanitizeHeight(value: number, maxHeight: number): number {\n if (!Number.isFinite(value)) return 0\n return clamp(value, -maxHeight, maxHeight)\n}\n\n// --- Equations -------------------------------------------------------------\n\n/**\n * One surface after {@link Graph3D}'s mapper has run: compiled, resolved and\n * fully defaulted, so the per-frame builder never re-derives anything and the\n * tween never tests for an absent field.\n */\nexport type EquationResolved = GraphEquationResolved<CompiledExpression>\n\n/**\n * Tween for the equation list — the shared match-by-id walk, with the one thing\n * a surface does differently: mid-morph its *height* is sampled from both\n * functions and blended, so it deforms into the new one rather than popping.\n *\n * The blend deliberately returns a **fresh closure** per frame, which is what\n * {@link surfaceRevision} reads as \"re-evaluate this mesh\".\n */\nexport function lerpEquations(\n from: EquationResolved[],\n to: EquationResolved[],\n t: number\n): EquationResolved[] {\n return lerpGraphEquations(from, to, t, blendSurface)\n}\n\n/** How two surfaces' heights blend mid-tween. */\nfunction blendSurface(\n a: CompiledExpression,\n b: CompiledExpression,\n t: number\n): CompiledExpression {\n return (x, y) => {\n const start = a(x, y)\n return start + (b(x, y) - start) * t\n }\n}\n\n/**\n * A small integer standing for a compiled expression's *identity*.\n *\n * `Geo.parametric`'s `revision` is a number, and what a surface's vertex callback\n * actually depends on includes a function — so the function has to be reduced to\n * something numeric. Identity is the right comparison here rather than the source\n * text: `compileExpressionCached` memoises by source, so two equal sources are\n * already `===`, and a mid-morph blend (see {@link blendEquation}) deliberately\n * builds a *fresh* closure each frame, which is exactly when the surface must be\n * re-evaluated.\n *\n * A `WeakMap` so retiring an expression doesn't pin it, and ids are never reused\n * — a recycled id could make a changed surface look unchanged.\n */\nconst expressionIds = new WeakMap<CompiledExpression, number>()\nlet nextExpressionId = 1\n\nfunction expressionId(sample: CompiledExpression): number {\n let id = expressionIds.get(sample)\n if (id === undefined) {\n id = nextExpressionId++\n expressionIds.set(sample, id)\n }\n return id\n}\n\n/**\n * The `revision` for one surface: a value that changes exactly when its vertex\n * callback would return something different.\n *\n * The callback in `Graph3D.addSurface` closes over three things — the compiled\n * expression, the domain (as `span`/`domain`) and `maxHeight` — so those three\n * are what this covers, and nothing else. `segments` is deliberately absent: it\n * is plain data on the descriptor that the renderer already compares itself.\n *\n * Mixed by FNV-1a over the three values' text rather than by arithmetic, because\n * `domain` and `maxHeight` are tweenable floats and there is no cheap numeric\n * combination of two floats and an int that doesn't collide somewhere. A\n * collision here would show as a surface that refuses to update, which is a\n * miserable bug to find; the string is a few dozen characters per surface per\n * frame, against the ~6.6k vertex evaluations it exists to avoid.\n */\nexport function surfaceRevision(\n sample: CompiledExpression,\n domain: number,\n maxHeight: number\n): number {\n const key = `${expressionId(sample)}|${domain}|${maxHeight}`\n let hash = 0x811c9dc5\n for (let i = 0; i < key.length; i++) {\n hash ^= key.charCodeAt(i)\n hash = Math.imul(hash, 0x01000193)\n }\n return hash >>> 0\n}\n"]}
@@ -0,0 +1,4 @@
1
+ export * from "./graph2d/index.js";
2
+ export * from "./graph3d/index.js";
3
+ export { NODES } from "./nodes.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./graph2d/index.js";
2
+ export * from "./graph3d/index.js";
3
+ export { NODES } from "./nodes.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC","sourcesContent":["export * from \"./graph2d\";\nexport * from \"./graph3d\";\nexport { NODES } from \"./nodes\";\n"]}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * What a resolved equation is, and how a list of them interpolates — shared by
3
+ * both graph nodes because the answer doesn't depend on how many dimensions the
4
+ * marks are drawn in.
5
+ *
6
+ * The tween is the part worth sharing. Matching two lists by `id` is what makes
7
+ * an equation that stays *morph* rather than pop, one that leaves fade out as
8
+ * itself, and one that arrives fade in; getting that wrong is invisible until
9
+ * somebody animates an equation list, at which point every surface deforms into
10
+ * its neighbour. One implementation, one set of tests.
11
+ *
12
+ * What is *not* shared is how two sampled functions blend into one — a surface
13
+ * blends `f(x, y)` and a curve blends `f(x)` — so that is the one thing a caller
14
+ * passes in. See `graph3d/impl/shared` and `graph2d/impl/shared`, which are each
15
+ * three lines because of it.
16
+ */
17
+ import { type NormalizedColor } from "@motionscript/core";
18
+ /**
19
+ * One equation after a node's mapper has run: compiled, resolved and fully
20
+ * defaulted, so the per-frame builder never re-derives anything and the tween
21
+ * never tests for an absent field.
22
+ *
23
+ * Generic in the sampled function because that is the only part that differs:
24
+ * `(x, y) => z` for a surface, `(x) => y` for a curve.
25
+ */
26
+ export interface GraphEquationResolved<Sample> {
27
+ id: string;
28
+ /** The compiled expression — compiled once at *write* time, not per sample. */
29
+ sample: Sample;
30
+ color: NormalizedColor;
31
+ /** `enabled: false` is folded in here as 0, so hiding is a fade. */
32
+ opacity: number;
33
+ }
34
+ /**
35
+ * How two sampled functions blend mid-tween.
36
+ *
37
+ * Handed in rather than inferred, and it must return a **fresh closure per
38
+ * call** when the two differ: that freshness is what a node keyed on the
39
+ * function's identity reads as "this needs re-evaluating" (see
40
+ * `surfaceRevision`).
41
+ */
42
+ export type BlendSample<Sample> = (a: Sample, b: Sample, t: number) => Sample;
43
+ /**
44
+ * Tween for an equation list: match by `id`, then morph / fade in / fade out.
45
+ *
46
+ * Three outcomes, and each is the one that reads correctly:
47
+ *
48
+ * - present in both → the mark **morphs**, because its value is sampled from
49
+ * both functions and blended, so it deforms into the new one rather than
50
+ * popping;
51
+ * - only in the old list → it fades out *still sampling its own expression*, so
52
+ * it leaves as itself;
53
+ * - only in the new list → it fades in.
54
+ */
55
+ export declare function lerpGraphEquations<Sample>(from: GraphEquationResolved<Sample>[], to: GraphEquationResolved<Sample>[], t: number, blendSample: BlendSample<Sample>): GraphEquationResolved<Sample>[];
56
+ //# sourceMappingURL=equations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"equations.d.ts","sourceRoot":"","sources":["../../src/kit/equations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAIrE;;;;;;;GAOG;AACH,MAAM,WAAW,qBAAqB,CAAC,MAAM;IAC3C,EAAE,EAAE,MAAM,CAAA;IACV,+EAA+E;IAC/E,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,eAAe,CAAA;IACtB,oEAAoE;IACpE,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,CAAC,MAAM,IAAI,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;AAE7E;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EACvC,IAAI,EAAE,qBAAqB,CAAC,MAAM,CAAC,EAAE,EACrC,EAAE,EAAE,qBAAqB,CAAC,MAAM,CAAC,EAAE,EACnC,CAAC,EAAE,MAAM,EACT,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,GAC/B,qBAAqB,CAAC,MAAM,CAAC,EAAE,CAiBjC"}
@@ -0,0 +1,63 @@
1
+ /**
2
+ * What a resolved equation is, and how a list of them interpolates — shared by
3
+ * both graph nodes because the answer doesn't depend on how many dimensions the
4
+ * marks are drawn in.
5
+ *
6
+ * The tween is the part worth sharing. Matching two lists by `id` is what makes
7
+ * an equation that stays *morph* rather than pop, one that leaves fade out as
8
+ * itself, and one that arrives fade in; getting that wrong is invisible until
9
+ * somebody animates an equation list, at which point every surface deforms into
10
+ * its neighbour. One implementation, one set of tests.
11
+ *
12
+ * What is *not* shared is how two sampled functions blend into one — a surface
13
+ * blends `f(x, y)` and a curve blends `f(x)` — so that is the one thing a caller
14
+ * passes in. See `graph3d/impl/shared` and `graph2d/impl/shared`, which are each
15
+ * three lines because of it.
16
+ */
17
+ import { lerpNumber } from "@motionscript/core";
18
+ import { lerpColor3D } from "@motionscript/core/component";
19
+ /**
20
+ * Tween for an equation list: match by `id`, then morph / fade in / fade out.
21
+ *
22
+ * Three outcomes, and each is the one that reads correctly:
23
+ *
24
+ * - present in both → the mark **morphs**, because its value is sampled from
25
+ * both functions and blended, so it deforms into the new one rather than
26
+ * popping;
27
+ * - only in the old list → it fades out *still sampling its own expression*, so
28
+ * it leaves as itself;
29
+ * - only in the new list → it fades in.
30
+ */
31
+ export function lerpGraphEquations(from, to, t, blendSample) {
32
+ if (t <= 0)
33
+ return from;
34
+ if (t >= 1)
35
+ return to;
36
+ const arriving = new Map(to.map((equation) => [equation.id, equation]));
37
+ const out = from.map((a) => {
38
+ const b = arriving.get(a.id);
39
+ return b
40
+ ? blendEquation(a, b, t, blendSample)
41
+ : { ...a, opacity: a.opacity * (1 - t) };
42
+ });
43
+ const held = new Set(from.map((equation) => equation.id));
44
+ for (const b of to) {
45
+ if (!held.has(b.id))
46
+ out.push({ ...b, opacity: b.opacity * t });
47
+ }
48
+ return out;
49
+ }
50
+ /** Interpolates one equation that exists on both sides of the tween. */
51
+ function blendEquation(a, b, t, blendSample) {
52
+ return {
53
+ id: b.id,
54
+ // Same id, different expression → morph the *geometry*. The double
55
+ // evaluation is only paid mid-tween, and the identity check skips it
56
+ // entirely when the source didn't change: the expression cache memoises by
57
+ // source, so equal source is `===`.
58
+ sample: a.sample === b.sample ? b.sample : blendSample(a.sample, b.sample, t),
59
+ color: lerpColor3D(a.color, b.color, t),
60
+ opacity: lerpNumber(a.opacity, b.opacity, t),
61
+ };
62
+ }
63
+ //# sourceMappingURL=equations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"equations.js","sourceRoot":"","sources":["../../src/kit/equations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,UAAU,EAAwB,MAAM,oBAAoB,CAAA;AAErE,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAA;AA6B1D;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,kBAAkB,CAChC,IAAqC,EACrC,EAAmC,EACnC,CAAS,EACT,WAAgC;IAEhC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAA;IACvB,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,CAAA;IAErB,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAA;IACvE,MAAM,GAAG,GAAoC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QAC1D,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QAC5B,OAAO,CAAC;YACN,CAAC,CAAC,aAAa,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC;YACrC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAA;IAC5C,CAAC,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAA;IACzD,KAAK,MAAM,CAAC,IAAI,EAAE,EAAE,CAAC;QACnB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC,CAAA;IACjE,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,wEAAwE;AACxE,SAAS,aAAa,CACpB,CAAgC,EAChC,CAAgC,EAChC,CAAS,EACT,WAAgC;IAEhC,OAAO;QACL,EAAE,EAAE,CAAC,CAAC,EAAE;QACR,mEAAmE;QACnE,qEAAqE;QACrE,2EAA2E;QAC3E,oCAAoC;QACpC,MAAM,EAAE,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7E,KAAK,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;QACvC,OAAO,EAAE,UAAU,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;KAC7C,CAAA;AACH,CAAC","sourcesContent":["/**\n * What a resolved equation is, and how a list of them interpolates — shared by\n * both graph nodes because the answer doesn't depend on how many dimensions the\n * marks are drawn in.\n *\n * The tween is the part worth sharing. Matching two lists by `id` is what makes\n * an equation that stays *morph* rather than pop, one that leaves fade out as\n * itself, and one that arrives fade in; getting that wrong is invisible until\n * somebody animates an equation list, at which point every surface deforms into\n * its neighbour. One implementation, one set of tests.\n *\n * What is *not* shared is how two sampled functions blend into one — a surface\n * blends `f(x, y)` and a curve blends `f(x)` — so that is the one thing a caller\n * passes in. See `graph3d/impl/shared` and `graph2d/impl/shared`, which are each\n * three lines because of it.\n */\n\nimport { lerpNumber, type NormalizedColor } from \"@motionscript/core\"\n\nimport { lerpColor3D } from \"@motionscript/core/component\"\n\n/**\n * One equation after a node's mapper has run: compiled, resolved and fully\n * defaulted, so the per-frame builder never re-derives anything and the tween\n * never tests for an absent field.\n *\n * Generic in the sampled function because that is the only part that differs:\n * `(x, y) => z` for a surface, `(x) => y` for a curve.\n */\nexport interface GraphEquationResolved<Sample> {\n id: string\n /** The compiled expression — compiled once at *write* time, not per sample. */\n sample: Sample\n color: NormalizedColor\n /** `enabled: false` is folded in here as 0, so hiding is a fade. */\n opacity: number\n}\n\n/**\n * How two sampled functions blend mid-tween.\n *\n * Handed in rather than inferred, and it must return a **fresh closure per\n * call** when the two differ: that freshness is what a node keyed on the\n * function's identity reads as \"this needs re-evaluating\" (see\n * `surfaceRevision`).\n */\nexport type BlendSample<Sample> = (a: Sample, b: Sample, t: number) => Sample\n\n/**\n * Tween for an equation list: match by `id`, then morph / fade in / fade out.\n *\n * Three outcomes, and each is the one that reads correctly:\n *\n * - present in both → the mark **morphs**, because its value is sampled from\n * both functions and blended, so it deforms into the new one rather than\n * popping;\n * - only in the old list → it fades out *still sampling its own expression*, so\n * it leaves as itself;\n * - only in the new list → it fades in.\n */\nexport function lerpGraphEquations<Sample>(\n from: GraphEquationResolved<Sample>[],\n to: GraphEquationResolved<Sample>[],\n t: number,\n blendSample: BlendSample<Sample>\n): GraphEquationResolved<Sample>[] {\n if (t <= 0) return from\n if (t >= 1) return to\n\n const arriving = new Map(to.map((equation) => [equation.id, equation]))\n const out: GraphEquationResolved<Sample>[] = from.map((a) => {\n const b = arriving.get(a.id)\n return b\n ? blendEquation(a, b, t, blendSample)\n : { ...a, opacity: a.opacity * (1 - t) }\n })\n\n const held = new Set(from.map((equation) => equation.id))\n for (const b of to) {\n if (!held.has(b.id)) out.push({ ...b, opacity: b.opacity * t })\n }\n return out\n}\n\n/** Interpolates one equation that exists on both sides of the tween. */\nfunction blendEquation<Sample>(\n a: GraphEquationResolved<Sample>,\n b: GraphEquationResolved<Sample>,\n t: number,\n blendSample: BlendSample<Sample>\n): GraphEquationResolved<Sample> {\n return {\n id: b.id,\n // Same id, different expression → morph the *geometry*. The double\n // evaluation is only paid mid-tween, and the identity check skips it\n // entirely when the source didn't change: the expression cache memoises by\n // source, so equal source is `===`.\n sample: a.sample === b.sample ? b.sample : blendSample(a.sample, b.sample, t),\n color: lerpColor3D(a.color, b.color, t),\n opacity: lerpNumber(a.opacity, b.opacity, t),\n }\n}\n"]}
@@ -0,0 +1,60 @@
1
+ import type { ExpressionDialect } from "@motionscript/core";
2
+ /**
3
+ * A compiled expression: pure, and safe to call thousands of times per frame.
4
+ *
5
+ * Always two arguments regardless of how many variables it was compiled with,
6
+ * because the arity is a *parse-time* fact and the signature is a hot path — a
7
+ * variadic form would allocate an array per sample. A one-variable expression
8
+ * simply never reads the second, so a curve calls it as `sample(x, 0)`.
9
+ */
10
+ export type CompiledExpression = (x: number, y: number) => number;
11
+ /** The two free variables a `z = f(x, y)` surface is sampled over. */
12
+ export declare const SURFACE_VARIABLES: readonly ["x", "y"];
13
+ /** The one free variable a `y = f(x)` curve is sampled over. */
14
+ export declare const CURVE_VARIABLES: readonly ["x"];
15
+ /**
16
+ * The names bound to the two argument slots, in order.
17
+ *
18
+ * At most two, because {@link CompiledExpression} has two argument slots — see
19
+ * the note there on why that is fixed rather than variadic.
20
+ */
21
+ export type ExpressionVariables = readonly string[];
22
+ /** The equation language, as a dialect of the shared grammar. */
23
+ export declare const PLOT_DIALECT: ExpressionDialect;
24
+ /** What a tile's help popover lists, for a node with these free variables. */
25
+ export interface ExpressionVocabulary {
26
+ variables: readonly string[];
27
+ functions: readonly string[];
28
+ constants: readonly string[];
29
+ }
30
+ /**
31
+ * Everything an expression over `variables` may name, for the inspector's
32
+ * "what can I write here" hint.
33
+ *
34
+ * Derived from the parser's own tables rather than re-listed on the client, so
35
+ * the help can never claim a function the grammar doesn't have — which is the
36
+ * way this kind of reference always rots.
37
+ */
38
+ export declare function expressionVocabulary(variables: ExpressionVariables): ExpressionVocabulary;
39
+ /** {@link expressionVocabulary} for the 3D graph's `z = f(x, y)` surfaces. */
40
+ export declare const EXPRESSION_VOCABULARY: ExpressionVocabulary;
41
+ /** {@link expressionVocabulary} for the 2D graph's `y = f(x)` curves. */
42
+ export declare const CURVE_VOCABULARY: ExpressionVocabulary;
43
+ /**
44
+ * Compiles `source` into a callable over `variables`. Throws on a syntax error,
45
+ * so a caller rendering user-typed text should catch and skip that equation —
46
+ * see {@link compileExpressionCached}, which does exactly that.
47
+ */
48
+ export declare function compileExpression(source: string, variables?: ExpressionVariables): CompiledExpression;
49
+ /**
50
+ * Why an expression won't compile, in the terms the tile shows: the message and
51
+ * nothing else. `null` means it compiles.
52
+ *
53
+ * Separate from {@link compileExpressionCached} because the two callers want
54
+ * opposite things — the renderer wants the function or nothing, the inspector
55
+ * wants the *reason* — and both must agree, which they do by going through the
56
+ * same parser.
57
+ */
58
+ export declare function expressionError(source: string, variables?: ExpressionVariables): string | null;
59
+ export declare function compileExpressionCached(source: string, variables?: ExpressionVariables): CompiledExpression | null;
60
+ //# sourceMappingURL=expression.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"expression.d.ts","sourceRoot":"","sources":["../../src/kit/expression.ts"],"names":[],"mappings":"AAsCA,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,oBAAoB,CAAA;AAE3E;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAA;AAEjE,sEAAsE;AACtE,eAAO,MAAM,iBAAiB,qBAAsB,CAAA;AAEpD,gEAAgE;AAChE,eAAO,MAAM,eAAe,gBAAiB,CAAA;AAE7C;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,MAAM,EAAE,CAAA;AAkEnD,iEAAiE;AACjE,eAAO,MAAM,YAAY,EAAE,iBAY1B,CAAA;AASD,8EAA8E;AAC9E,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;IAC5B,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;CAC7B;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,mBAAmB,GAC7B,oBAAoB,CAEtB;AAED,8EAA8E;AAC9E,eAAO,MAAM,qBAAqB,sBAA0C,CAAA;AAE5E,yEAAyE;AACzE,eAAO,MAAM,gBAAgB,sBAAwC,CAAA;AAErE;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,mBAAuC,GACjD,kBAAkB,CASpB;AA0DD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,mBAAuC,GACjD,MAAM,GAAG,IAAI,CAWf;AAsBD,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,mBAAuC,GACjD,kBAAkB,GAAG,IAAI,CAsB3B"}
@@ -0,0 +1,268 @@
1
+ /**
2
+ * The little language every graph node's equation tiles are written in — the
3
+ * one binding behind `z = f(x, y)` on the 3D graph and `y = f(x)` on the 2D one.
4
+ *
5
+ * **The grammar is not here.** It is `expressionOps.parse` in
6
+ * `@motionscript/core`, shared with the `"=4[g]+3"` expressions a scene
7
+ * document writes in props. There used to be two parsers, and they agreed on
8
+ * every rule they documented while disagreeing everywhere they hadn't: this one
9
+ * scanned `1.2.3` as a single token that `Number` turned into a `NaN` which
10
+ * parsed, compiled and rendered an invisible surface with no diagnostic
11
+ * anywhere; it treated `\r` as an unexpected character; and it checked no arity
12
+ * on its variadic functions, so `atan2(1)` was a silent `NaN` too.
13
+ *
14
+ * What is genuinely this language's own is a **dialect** and a back end:
15
+ *
16
+ * - {@link PLOT_DIALECT} — trig in **radians** (a plot of `sin(x)` is a plot of
17
+ * `sin(x)`, where a document's `rotation` is in degrees like every other angle
18
+ * in the library), the hyperbolics and log bases a plot wants, `phi`, and
19
+ * references spelled as bare identifiers rather than `[id]`.
20
+ * - {@link compileExpression} — the emitter, which turns the shared AST into a
21
+ * two-argument closure rather than into the signal binding a document prop
22
+ * needs.
23
+ *
24
+ * **Deliberately not `new Function(...)`.** An expression is a value typed into
25
+ * the inspector, stored in a scene document and shared with whoever opens it
26
+ * next; compiling it as JavaScript would make every scene file an execution
27
+ * vector. The shared parser accepts a fixed grammar and can only ever produce
28
+ * arithmetic.
29
+ *
30
+ * ## Which letters are variables is the caller's business
31
+ *
32
+ * What differs between the two graphs is *how many free variables there are*,
33
+ * and that matters more than it sounds. A 2D curve compiled with `x` alone
34
+ * rejects `y` as an unknown identifier, so typing `y = x + y` says so in the
35
+ * tile instead of silently plotting `x` — and a caller cannot forget to check,
36
+ * because the check is the same one that catches `alert(1)`.
37
+ */
38
+ import { expressionOps } from "@motionscript/core";
39
+ /** The two free variables a `z = f(x, y)` surface is sampled over. */
40
+ export const SURFACE_VARIABLES = ["x", "y"];
41
+ /** The one free variable a `y = f(x)` curve is sampled over. */
42
+ export const CURVE_VARIABLES = ["x"];
43
+ /** Single-argument functions available to an expression. */
44
+ const UNARY_FNS = {
45
+ sin: Math.sin,
46
+ cos: Math.cos,
47
+ tan: Math.tan,
48
+ asin: Math.asin,
49
+ acos: Math.acos,
50
+ atan: Math.atan,
51
+ sinh: Math.sinh,
52
+ cosh: Math.cosh,
53
+ tanh: Math.tanh,
54
+ sqrt: Math.sqrt,
55
+ cbrt: Math.cbrt,
56
+ abs: Math.abs,
57
+ exp: Math.exp,
58
+ log: Math.log,
59
+ ln: Math.log,
60
+ log2: Math.log2,
61
+ log10: Math.log10,
62
+ floor: Math.floor,
63
+ ceil: Math.ceil,
64
+ round: Math.round,
65
+ sign: Math.sign,
66
+ trunc: Math.trunc,
67
+ };
68
+ /** Multi-argument functions. */
69
+ const NARY_FNS = {
70
+ atan2: Math.atan2,
71
+ pow: Math.pow,
72
+ hypot: Math.hypot,
73
+ min: Math.min,
74
+ max: Math.max,
75
+ mod: (a, b) => a % b,
76
+ };
77
+ /**
78
+ * How many arguments each of {@link NARY_FNS} takes.
79
+ *
80
+ * Written from what this language already accepts, not copied from core's table:
81
+ * `hypot(3)` and `min(3)` are meaningful and have always compiled, so they stay
82
+ * `[1, Infinity]` where the document dialect has `hypot: [2, Infinity]`. What
83
+ * the arity check is actually for is the other end — `atan2(1)` and `min()`
84
+ * used to compile to a silent `NaN` and an `Infinity`, which rendered as
85
+ * nothing at all and said nothing about why.
86
+ */
87
+ const NARY_ARITY = {
88
+ atan2: [2, 2],
89
+ pow: [2, 2],
90
+ mod: [2, 2],
91
+ hypot: [1, Infinity],
92
+ min: [1, Infinity],
93
+ max: [1, Infinity],
94
+ };
95
+ const CONSTANTS = {
96
+ pi: Math.PI,
97
+ PI: Math.PI,
98
+ e: Math.E,
99
+ E: Math.E,
100
+ tau: Math.PI * 2,
101
+ phi: (1 + Math.sqrt(5)) / 2,
102
+ };
103
+ /** The equation language, as a dialect of the shared grammar. */
104
+ export const PLOT_DIALECT = {
105
+ functions: {
106
+ ...Object.fromEntries(Object.keys(UNARY_FNS).map((name) => [name, { arity: [1, 1] }])),
107
+ ...Object.fromEntries(Object.entries(NARY_ARITY).map(([name, arity]) => [name, { arity }])),
108
+ },
109
+ constants: CONSTANTS,
110
+ refs: "bare",
111
+ unknownName: (name) => `Unknown identifier '${name}'`,
112
+ };
113
+ /** Everything an expression may name, minus the variables. */
114
+ const NAMEABLE = {
115
+ functions: [...Object.keys(UNARY_FNS), ...Object.keys(NARY_FNS)].sort(),
116
+ /** Lower-case spellings only; `PI`/`E` are accepted but not advertised twice. */
117
+ constants: ["pi", "e", "tau", "phi"],
118
+ };
119
+ /**
120
+ * Everything an expression over `variables` may name, for the inspector's
121
+ * "what can I write here" hint.
122
+ *
123
+ * Derived from the parser's own tables rather than re-listed on the client, so
124
+ * the help can never claim a function the grammar doesn't have — which is the
125
+ * way this kind of reference always rots.
126
+ */
127
+ export function expressionVocabulary(variables) {
128
+ return { variables, ...NAMEABLE };
129
+ }
130
+ /** {@link expressionVocabulary} for the 3D graph's `z = f(x, y)` surfaces. */
131
+ export const EXPRESSION_VOCABULARY = expressionVocabulary(SURFACE_VARIABLES);
132
+ /** {@link expressionVocabulary} for the 2D graph's `y = f(x)` curves. */
133
+ export const CURVE_VOCABULARY = expressionVocabulary(CURVE_VARIABLES);
134
+ /**
135
+ * Compiles `source` into a callable over `variables`. Throws on a syntax error,
136
+ * so a caller rendering user-typed text should catch and skip that equation —
137
+ * see {@link compileExpressionCached}, which does exactly that.
138
+ */
139
+ export function compileExpression(source, variables = SURFACE_VARIABLES) {
140
+ // The parser folds a constant before it will yield a reference, so a name that
141
+ // is both would silently stop being the variable it was passed as.
142
+ for (const name of variables) {
143
+ if (name in CONSTANTS) {
144
+ throw new Error(`'${name}' is a constant, so it cannot also be a free variable`);
145
+ }
146
+ }
147
+ return emit(expressionOps.parse(source, PLOT_DIALECT).root, variables);
148
+ }
149
+ /** The shared AST into this language's two-argument closure. */
150
+ function emit(node, variables) {
151
+ switch (node.kind) {
152
+ case "const": {
153
+ const { value } = node;
154
+ return () => value;
155
+ }
156
+ case "ref": {
157
+ // Which argument slot this name reads, or -1 for a name that isn't a
158
+ // variable *here* — the whole point of taking the list: `y` in a curve is
159
+ // an unknown identifier, not a silent zero.
160
+ const slot = variables.indexOf(node.name);
161
+ if (slot === 0)
162
+ return (x) => x;
163
+ if (slot === 1)
164
+ return (_x, y) => y;
165
+ throw new Error(PLOT_DIALECT.unknownName(node.name));
166
+ }
167
+ case "unary": {
168
+ const operand = emit(node.operand, variables);
169
+ return (x, y) => -operand(x, y);
170
+ }
171
+ case "binary": {
172
+ const l = emit(node.left, variables);
173
+ const r = emit(node.right, variables);
174
+ switch (node.op) {
175
+ case "+": return (x, y) => l(x, y) + r(x, y);
176
+ case "-": return (x, y) => l(x, y) - r(x, y);
177
+ case "*": return (x, y) => l(x, y) * r(x, y);
178
+ case "/": return (x, y) => l(x, y) / r(x, y);
179
+ case "%": return (x, y) => l(x, y) % r(x, y);
180
+ case "^": return (x, y) => Math.pow(l(x, y), r(x, y));
181
+ }
182
+ break;
183
+ }
184
+ case "call": {
185
+ const args = node.args.map((arg) => emit(arg, variables));
186
+ const unary = UNARY_FNS[node.name];
187
+ if (unary) {
188
+ const [a] = args;
189
+ return (x, y) => unary(a(x, y));
190
+ }
191
+ const nary = NARY_FNS[node.name];
192
+ // Specialise the common arity so the hot path doesn't allocate an array
193
+ // per sample.
194
+ if (args.length === 2) {
195
+ const [a, b] = args;
196
+ return (x, y) => nary(a(x, y), b(x, y));
197
+ }
198
+ return (x, y) => nary(...args.map((arg) => arg(x, y)));
199
+ }
200
+ }
201
+ throw new Error("Unsupported expression node");
202
+ }
203
+ /**
204
+ * Why an expression won't compile, in the terms the tile shows: the message and
205
+ * nothing else. `null` means it compiles.
206
+ *
207
+ * Separate from {@link compileExpressionCached} because the two callers want
208
+ * opposite things — the renderer wants the function or nothing, the inspector
209
+ * wants the *reason* — and both must agree, which they do by going through the
210
+ * same parser.
211
+ */
212
+ export function expressionError(source, variables = SURFACE_VARIABLES) {
213
+ const trimmed = source.trim();
214
+ // Empty is not an error: it is a tile nobody has filled in yet, which the
215
+ // panel already shows as a placeholder rather than as a mistake.
216
+ if (trimmed === "")
217
+ return null;
218
+ try {
219
+ compileExpression(trimmed, variables);
220
+ return null;
221
+ }
222
+ catch (error) {
223
+ return error instanceof Error ? error.message : String(error);
224
+ }
225
+ }
226
+ /**
227
+ * Compiles with memoisation, returning `null` for anything that doesn't parse.
228
+ *
229
+ * Both halves matter here. A node's builder re-runs every frame, so an uncached
230
+ * compile would re-parse every equation sixty times a second; and the inspector
231
+ * commits on every keystroke, so half-typed source reaches the renderer
232
+ * constantly and must be *skipped* rather than thrown out of the render pass.
233
+ *
234
+ * The cache is also what makes the equation tween cheap: equal source compiles
235
+ * to the identical closure, so "did this equation change" is an `===`. Note it
236
+ * stores a failure as `null` and tests `hit !== undefined`, so unparseable
237
+ * source is not re-parsed on every frame either.
238
+ *
239
+ * Keyed by the variable list as well as the source, because the same text means
240
+ * different things under different bindings — `x + y` compiles for a surface and
241
+ * is an error for a curve, and one cache would hand the surface's answer to the
242
+ * curve that asked second.
243
+ */
244
+ const caches = new Map();
245
+ export function compileExpressionCached(source, variables = SURFACE_VARIABLES) {
246
+ const trimmed = source.trim();
247
+ if (trimmed === "")
248
+ return null;
249
+ const key = variables.join(",");
250
+ let cache = caches.get(key);
251
+ if (!cache) {
252
+ cache = new Map();
253
+ caches.set(key, cache);
254
+ }
255
+ const hit = cache.get(trimmed);
256
+ if (hit !== undefined)
257
+ return hit;
258
+ let compiled;
259
+ try {
260
+ compiled = compileExpression(trimmed, variables);
261
+ }
262
+ catch {
263
+ compiled = null;
264
+ }
265
+ cache.set(trimmed, compiled);
266
+ return compiled;
267
+ }
268
+ //# sourceMappingURL=expression.js.map