@tinyvolt/pie 0.0.0-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 (72) hide show
  1. package/.nvmrc +1 -0
  2. package/LICENSE +21 -0
  3. package/README.md +56 -2
  4. package/functions/midpoint.ts +29 -0
  5. package/functions/square.ts +20 -0
  6. package/init/index.js +87 -0
  7. package/init/site-agents.md +253 -0
  8. package/init/site-gitignore +28 -0
  9. package/init/site-tsconfig.json +20 -0
  10. package/init/site-vite.config.ts +6 -0
  11. package/layouts/header.tsx +16 -0
  12. package/layouts/index.tsx +10 -0
  13. package/nodes/square.tsx +36 -0
  14. package/package.json +59 -4
  15. package/pages/example.md +7 -0
  16. package/pages/python-editor.md +29 -0
  17. package/public/favicon.ico +0 -0
  18. package/public/logo.svg +5 -0
  19. package/site.yml +1 -0
  20. package/src/app.css +343 -0
  21. package/src/app.tsx +30 -0
  22. package/src/authorStyles.ts +3 -0
  23. package/src/components/Header.tsx +16 -0
  24. package/src/components/PageTitle.tsx +10 -0
  25. package/src/components/SplitPane.tsx +170 -0
  26. package/src/components/icons.tsx +22 -0
  27. package/src/entry-client.tsx +4 -0
  28. package/src/entry-server.tsx +21 -0
  29. package/src/global.d.ts +7 -0
  30. package/src/lib/appDir.ts +4 -0
  31. package/src/lib/autodiff/index.ts +49 -0
  32. package/src/lib/layouts/layout.ts +20 -0
  33. package/src/lib/layouts/vitePlugin.ts +39 -0
  34. package/src/lib/markdown/components/CanvasPage.tsx +25 -0
  35. package/src/lib/markdown/components/MarkdownBody.tsx +41 -0
  36. package/src/lib/markdown/components/PythonEditor.tsx +166 -0
  37. package/src/lib/markdown/frontmatter.ts +81 -0
  38. package/src/lib/markdown/pages.ts +23 -0
  39. package/src/lib/markdown/render.ts +54 -0
  40. package/src/lib/markdown/vitePlugin.ts +94 -0
  41. package/src/lib/nodes/components/CenteredGrid.tsx +59 -0
  42. package/src/lib/nodes/components/NodeCanvas.tsx +58 -0
  43. package/src/lib/nodes/execute.ts +166 -0
  44. package/src/lib/nodes/functions/Node2NodeFunc.ts +307 -0
  45. package/src/lib/nodes/functions/array.ts +36 -0
  46. package/src/lib/nodes/functions/autodiff.ts +64 -0
  47. package/src/lib/nodes/functions/basic.ts +298 -0
  48. package/src/lib/nodes/functions/registry.ts +46 -0
  49. package/src/lib/nodes/functions/sideEffects.ts +193 -0
  50. package/src/lib/nodes/functions/vitePlugin.ts +40 -0
  51. package/src/lib/nodes/hooks/useCanvasResize.ts +38 -0
  52. package/src/lib/nodes/jax.ts +191 -0
  53. package/src/lib/nodes/render/renderLogic.tsx +306 -0
  54. package/src/lib/nodes/state/NameGenerator.ts +31 -0
  55. package/src/lib/nodes/state/Node.ts +211 -0
  56. package/src/lib/nodes/state/NodeStore.ts +110 -0
  57. package/src/lib/nodes/tensors/broadcast.ts +101 -0
  58. package/src/lib/nodes/tensors/tensor.ts +42 -0
  59. package/src/lib/python/lift.ts +105 -0
  60. package/src/lib/python/py/__pycache__/pie.cpython-314.pyc +0 -0
  61. package/src/lib/python/py/pie.py +231 -0
  62. package/src/lib/python/py/pie_mpl.py +15 -0
  63. package/src/lib/python/py/show_figures.py +5 -0
  64. package/src/lib/python/runner.ts +202 -0
  65. package/src/lib/python/worker.ts +109 -0
  66. package/src/lib/site/vitePlugin.ts +43 -0
  67. package/src/routes/[...slug].tsx +38 -0
  68. package/src/routes/about.tsx +10 -0
  69. package/src/routes/index.tsx +14 -0
  70. package/src/routes/pages/index.tsx +20 -0
  71. package/styles/theme.css +27 -0
  72. package/vite.ts +61 -0
@@ -0,0 +1,298 @@
1
+ import { Node2NodeFunc } from './Node2NodeFunc';
2
+ import { AnyNode, PointNode, ScalarNode, TextNode } from '../state/Node';
3
+ import { scalar, num, add, sub, mul, div, sqrt, ref } from '../jax';
4
+
5
+ /**
6
+ * Create a scalar value from another scalar.
7
+ * @param value - Input scalar value
8
+ * @returns Scalar node
9
+ * @auxiliaryNodes None
10
+ */
11
+ export const Scalar: Node2NodeFunc = {
12
+ keyword: 'scalar', outputType: 'Scalar',
13
+ parameters: [{ name: 'value', type: 'Scalar' }],
14
+ compute(inputs) {
15
+ const inner = (ins: AnyNode[]) => {
16
+ const valueNode = ins[0] as ScalarNode;
17
+ const value = valueNode?.value || scalar(0);
18
+ return { main: { type: 'Scalar', value } };
19
+ };
20
+ return inner(inputs);
21
+ },
22
+ };
23
+
24
+ /**
25
+ * Create a text node with a string value. It is drawn only when given both x and y.
26
+ * @param value - Text string value (default '')
27
+ * @param x - X coordinate of the center (default: not drawn)
28
+ * @param y - Y coordinate of the center (default: not drawn)
29
+ * @param fontSize - Font size in pixels (default 12)
30
+ * @param w - Box width: 0 with h 0 = one line, no box; above 0 = fixed; -1 = fits the text, at most 5 (default 0)
31
+ * @param h - Box height: 0 with w 0 = one line, no box; above 0 = fixed; -1 = fits the text, at most 5 (default 0)
32
+ * @returns Text node
33
+ * @auxiliaryNodes None
34
+ */
35
+ export const Text: Node2NodeFunc = {
36
+ keyword: 'text', outputType: 'Text',
37
+ parameters: [
38
+ { name: 'value', type: 'Text', defaultValue: '' },
39
+ { name: 'x', type: 'Scalar', defaultValue: null },
40
+ { name: 'y', type: 'Scalar', defaultValue: null },
41
+ { name: 'fontSize', type: 'Scalar', defaultValue: 12 },
42
+ { name: 'w', type: 'Scalar', defaultValue: 0 },
43
+ { name: 'h', type: 'Scalar', defaultValue: 0 },
44
+ ],
45
+ compute(inputs) {
46
+ const inner = (ins: AnyNode[]) => {
47
+ const textNode = ins[0] as TextNode;
48
+ const value = textNode?.value || '';
49
+ const [x, y, fontSize, w, h] = ins.slice(1) as (ScalarNode | undefined)[];
50
+ return { main: { type: 'Text', value, x, y, fontSize, w, h } };
51
+ };
52
+ return inner(inputs);
53
+ },
54
+ };
55
+
56
+ /**
57
+ * Create a point from x and y coordinates.
58
+ * @param x - X coordinate
59
+ * @param y - Y coordinate
60
+ * @returns Point node
61
+ * @auxiliaryNodes None
62
+ */
63
+ export const Point: Node2NodeFunc = {
64
+ keyword: 'point', outputType: 'Point',
65
+ parameters: [{ name: 'x', type: 'Scalar' }, { name: 'y', type: 'Scalar' }],
66
+ compute(inputs) {
67
+ const inner = (ins: AnyNode[]) => {
68
+ const xNode = ins[0] as ScalarNode;
69
+ const yNode = ins[1] as ScalarNode;
70
+ return {
71
+ main: {
72
+ type: 'Point',
73
+ x: xNode.value,
74
+ y: yNode.value
75
+ }
76
+ };
77
+ };
78
+ return inner(inputs);
79
+ },
80
+ };
81
+
82
+ /**
83
+ * Create a triangle from three vertex points.
84
+ * @param vertex1 - First vertex point
85
+ * @param vertex2 - Second vertex point
86
+ * @param vertex3 - Third vertex point
87
+ * @returns Triangle node
88
+ * @auxiliaryNodes l_0, l_1, l_2 (Line nodes)
89
+ */
90
+ export const Triangle: Node2NodeFunc = {
91
+ keyword: 'triangle', outputType: 'Triangle',
92
+ parameters: [{ name: 'vertex1', type: 'Point' }, { name: 'vertex2', type: 'Point' }, { name: 'vertex3', type: 'Point' }],
93
+ compute(inputs) {
94
+ const inner = (ins: AnyNode[]) => {
95
+ const nodes: Record<string, any> = {};
96
+ for (let i = 0; i < 3; i++) {
97
+ nodes[`l_${i}`] = { type: 'Line', p1: ins[i], p2: ins[(i + 1) % 3] };
98
+ }
99
+ nodes['main'] = { type: 'Triangle', vertices: ins };
100
+ return nodes;
101
+ };
102
+ return inner(inputs);
103
+ },
104
+ };
105
+
106
+ /**
107
+ * Create a line from two points.
108
+ * @param point1 - First point
109
+ * @param point2 - Second point
110
+ * @returns Line node
111
+ * @auxiliaryNodes None
112
+ */
113
+ export const Line: Node2NodeFunc = {
114
+ keyword: 'line', outputType: 'Line',
115
+ parameters: [{ name: 'point1', type: 'Point' }, { name: 'point2', type: 'Point' }],
116
+ compute(inputs) {
117
+ const inner = (ins: AnyNode[]) => ({ main: { type: 'Line', p1: ins[0], p2: ins[1] } });
118
+ return inner(inputs);
119
+ },
120
+ };
121
+
122
+ /**
123
+ * Compute the distance between two points.
124
+ * @param point1 - First point
125
+ * @param point2 - Second point
126
+ * @returns Scalar node
127
+ * @auxiliaryNodes None
128
+ */
129
+ export const Distance: Node2NodeFunc = {
130
+ keyword: 'distance', outputType: 'Scalar',
131
+ parameters: [{ name: 'point1', type: 'Point' }, { name: 'point2', type: 'Point' }],
132
+ compute(inputs) {
133
+ const inner = (ins: AnyNode[]) => {
134
+ const p1 = ins[0] as PointNode;
135
+ const p2 = ins[1] as PointNode;
136
+ const dx = sub(ref(p2.x), ref(p1.x));
137
+ const dy = sub(ref(p2.y), ref(p1.y));
138
+ const value = sqrt(add(mul(ref(dx), ref(dx)), mul(ref(dy), ref(dy))));
139
+ return { main: { type: 'Scalar', value } };
140
+ };
141
+ return inner(inputs);
142
+ },
143
+ };
144
+
145
+ /**
146
+ * Create a circle with a center point and radius.
147
+ * @param center - Center point of the circle
148
+ * @param radius - Radius of the circle
149
+ * @returns Circle node
150
+ * @auxiliaryNodes None
151
+ */
152
+ export const Circle: Node2NodeFunc = {
153
+ keyword: 'circle', outputType: 'Circle',
154
+ parameters: [{ name: 'center', type: 'Point' }, { name: 'radius', type: 'Scalar' }],
155
+ compute(inputs) {
156
+ const inner = (ins: AnyNode[]) => ({ main: { type: 'Circle', center: ins[0], radius: ins[1] } });
157
+ return inner(inputs);
158
+ },
159
+ };
160
+
161
+ /**
162
+ * Create an ellipse with center, radii, and rotation.
163
+ * @param center - Center point of the ellipse
164
+ * @param radiusX - Horizontal radius
165
+ * @param radiusY - Vertical radius
166
+ * @param rotation - Rotation angle in degrees
167
+ * @returns Ellipse node
168
+ * @auxiliaryNodes None
169
+ */
170
+ export const Ellipse: Node2NodeFunc = {
171
+ keyword: 'ellipse', outputType: 'Ellipse',
172
+ parameters: [
173
+ { name: 'center', type: 'Point' },
174
+ { name: 'radiusX', type: 'Scalar' },
175
+ { name: 'radiusY', type: 'Scalar' },
176
+ { name: 'rotation', type: 'Scalar' }
177
+ ],
178
+ compute(inputs) {
179
+ const inner = (ins: AnyNode[]) => ({ main: { type: 'Ellipse', center: ins[0], radiusX: ins[1], radiusY: ins[2], rotation: ins[3] } });
180
+ return inner(inputs);
181
+ },
182
+ };
183
+
184
+ /**
185
+ * Create a quadratic Bezier curve from two endpoints and a control point.
186
+ * @param p1 - Start point
187
+ * @param control - Control point
188
+ * @param p2 - End point
189
+ * @returns BezierQuadratic node
190
+ * @auxiliaryNodes None
191
+ */
192
+ export const BezierQuadratic: Node2NodeFunc = {
193
+ keyword: 'bezier_quadratic', outputType: 'BezierQuadratic',
194
+ parameters: [{ name: 'p1', type: 'Point' }, { name: 'control', type: 'Point' }, { name: 'p2', type: 'Point' }],
195
+ compute(inputs) {
196
+ const inner = (ins: AnyNode[]) => ({ main: { type: 'BezierQuadratic', p1: ins[0], control: ins[1], p2: ins[2] } });
197
+ return inner(inputs);
198
+ }
199
+ };
200
+
201
+ /**
202
+ * Create a cubic Bezier curve from two endpoints and two control points.
203
+ * @param p1 - Start point
204
+ * @param control1 - First control point
205
+ * @param control2 - Second control point
206
+ * @param p2 - End point
207
+ * @returns BezierCubic node
208
+ * @auxiliaryNodes None
209
+ */
210
+ export const BezierCubic: Node2NodeFunc = {
211
+ keyword: 'bezier_cubic', outputType: 'BezierCubic',
212
+ parameters: [{ name: 'p1', type: 'Point' }, { name: 'control1', type: 'Point' }, { name: 'control2', type: 'Point' }, { name: 'p2', type: 'Point' }],
213
+ compute(inputs) {
214
+ const inner = (ins: AnyNode[]) => ({ main: { type: 'BezierCubic', p1: ins[0], control1: ins[1], control2: ins[2], p2: ins[3] } });
215
+ return inner(inputs);
216
+ }
217
+ };
218
+
219
+ /**
220
+ * Create an arc with center, radius, and start/end angles.
221
+ * @param center - Center point of the arc
222
+ * @param radius - Radius of the arc
223
+ * @param startAngle - Start angle in degrees
224
+ * @param endAngle - End angle in degrees
225
+ * @returns Arc node
226
+ * @auxiliaryNodes None
227
+ */
228
+ export const Arc: Node2NodeFunc = {
229
+ keyword: 'arc', outputType: 'Arc',
230
+ parameters: [{ name: 'center', type: 'Point' }, { name: 'radius', type: 'Scalar' }, { name: 'startAngle', type: 'Scalar' }, { name: 'endAngle', type: 'Scalar' }],
231
+ compute(inputs) {
232
+ const inner = (ins: AnyNode[]) => ({ main: { type: 'Arc', center: ins[0], radius: ins[1], startAngle: ins[2], endAngle: ins[3] } });
233
+ return inner(inputs);
234
+ }
235
+ };
236
+
237
+ /**
238
+ * Create an ellipse from two foci and a string length.
239
+ * @param focus1 - First focus point
240
+ * @param focus2 - Second focus point
241
+ * @param stringLength - Total string length (sum of distances from any point on ellipse to both foci)
242
+ * @returns Ellipse node
243
+ * @auxiliaryNodes center, radiusX, radiusY, rotation
244
+ */
245
+ export const EllipseFromFociAndStringLength: Node2NodeFunc = {
246
+ keyword: 'ellipse_from_foci', outputType: 'Ellipse',
247
+ parameters: [
248
+ { name: 'focus1', type: 'Point' },
249
+ { name: 'focus2', type: 'Point' },
250
+ { name: 'stringLength', type: 'Scalar' }
251
+ ],
252
+ compute(inputs) {
253
+ const inner = (ins: AnyNode[]) => {
254
+ const focus1 = ins[0] as PointNode;
255
+ const focus2 = ins[1] as PointNode;
256
+ const stringLengthNode = ins[2] as ScalarNode;
257
+
258
+ const dx = sub(ref(focus2.x), ref(focus1.x));
259
+ const dy = sub(ref(focus2.y), ref(focus1.y));
260
+ const focalDistance = sqrt(add(mul(ref(dx), ref(dx)), mul(ref(dy), ref(dy))));
261
+
262
+ if (num(stringLengthNode.value) <= num(focalDistance)) {
263
+ return { main: { type: 'Dummy' } };
264
+ }
265
+
266
+ const cx = div(add(ref(focus1.x), ref(focus2.x)), scalar(2));
267
+ const cy = div(add(ref(focus1.y), ref(focus2.y)), scalar(2));
268
+ const center = { type: 'Point', x: cx, y: cy };
269
+
270
+ const c = div(ref(focalDistance), scalar(2));
271
+ const a = div(ref(stringLengthNode.value), scalar(2));
272
+
273
+ const cSquared = mul(ref(c), ref(c));
274
+ const aSquared = mul(ref(a), ref(a));
275
+ const bSquared = sub(ref(aSquared), ref(cSquared));
276
+ const b = sqrt(ref(bSquared));
277
+
278
+ const angleRad = scalar(Math.atan2(num(dy), num(dx)));
279
+ const angleDeg = div(mul(ref(angleRad), scalar(180)), scalar(Math.PI));
280
+
281
+ const radiusX = { type: 'Scalar', value: a };
282
+ const radiusY = { type: 'Scalar', value: b };
283
+ const rotation = { type: 'Scalar', value: angleDeg };
284
+
285
+ return {
286
+ center,
287
+ radiusX,
288
+ radiusY,
289
+ rotation,
290
+ main: { type: 'Ellipse', center, radiusX, radiusY, rotation }
291
+ };
292
+ };
293
+ return inner(inputs);
294
+ }
295
+ };
296
+
297
+ /** Docs category for the functions defined in this file. */
298
+ export const category = 'Basic Figures';
@@ -0,0 +1,46 @@
1
+ import { Node2NodeFunc, SideEffectFunc } from './Node2NodeFunc';
2
+ import * as basicNodes from './basic';
3
+ import * as autodiffNodes from './autodiff';
4
+ import * as arrayNodes from './array';
5
+ import * as sideEffects from './sideEffects';
6
+
7
+ const isNode2NodeFunc = (fn: unknown): fn is Node2NodeFunc =>
8
+ typeof fn === 'object' && fn !== null && typeof (fn as Node2NodeFunc).compute === 'function';
9
+
10
+ const isSideEffectFunc = (fn: unknown): fn is SideEffectFunc =>
11
+ typeof fn === 'object' && fn !== null && typeof (fn as SideEffectFunc).run === 'function';
12
+
13
+ // The site's own functions: each `functions/**/*.ts` exports `Node2NodeFunc`s, like `basic.ts`.
14
+ const customModules = import.meta.glob<Record<string, unknown>>('/functions/**/*.ts', { eager: true });
15
+
16
+ // File path -> its exports. Built-ins first.
17
+ const modules: [string, Record<string, unknown>][] = [
18
+ ['src/lib/nodes/functions/basic.ts', basicNodes],
19
+ ['src/lib/nodes/functions/autodiff.ts', autodiffNodes],
20
+ ['src/lib/nodes/functions/array.ts', arrayNodes],
21
+ ['src/lib/nodes/functions/sideEffects.ts', sideEffects],
22
+ ...Object.entries(customModules).map(([path, mod]): [string, Record<string, unknown>] => [path.slice(1), mod]),
23
+ ];
24
+
25
+ export const functionRegistry = new Map<string, Node2NodeFunc>();
26
+ export const sideEffectRegistry = new Map<string, SideEffectFunc>();
27
+ const sourceFile = new Map<string, string>();
28
+ for (const [file, mod] of modules) {
29
+ for (const fn of Object.values(mod).filter(fn => isNode2NodeFunc(fn) || isSideEffectFunc(fn))) {
30
+ const other = sourceFile.get(fn.keyword);
31
+ if (other !== undefined) {
32
+ throw new Error(`Duplicate function keyword '${fn.keyword}': ${file} and ${other}`);
33
+ }
34
+ if (isSideEffectFunc(fn)) sideEffectRegistry.set(fn.keyword, fn);
35
+ else functionRegistry.set(fn.keyword, fn);
36
+ sourceFile.set(fn.keyword, file);
37
+ }
38
+ }
39
+
40
+ export function getFunction(keyword: string): Node2NodeFunc | undefined {
41
+ return functionRegistry.get(keyword);
42
+ }
43
+
44
+ export function getSideEffect(keyword: string): SideEffectFunc | undefined {
45
+ return sideEffectRegistry.get(keyword);
46
+ }
@@ -0,0 +1,193 @@
1
+ import { createMemo, createRoot, createSignal, onCleanup } from 'solid-js';
2
+ import { SideEffectFunc } from './Node2NodeFunc';
3
+ import { AnyNode, PointNode, ScalarNode, TextNode, checkColor, getNodeTypes } from '../state/Node';
4
+ import { NodeStore } from '../state/NodeStore';
5
+ import { Dtype, add, num, ref, scalar } from '../jax';
6
+
7
+ /** How long an animated change takes. */
8
+ const ANIMATION_MS = 500;
9
+
10
+ /** Slow at the start and end, fast in the middle. `t` and the result go from 0 to 1. */
11
+ const easeInOut = (t: number) => (t < 0.5 ? 2 * t * t : 1 - 2 * (1 - t) ** 2);
12
+
13
+ /**
14
+ * Saves `node` with `changes`, new values for some of its numbers, in place of `node`.
15
+ * It has no inputs, so it no longer follows the old node's.
16
+ * With `animate`, the numbers move to their new values over `ANIMATION_MS` instead of jumping.
17
+ * Resolves when they arrive, or when the node is replaced or removed first.
18
+ */
19
+ function replaceNode<N extends AnyNode>(store: NodeStore, node: N, changes: Partial<N>, animate: boolean): Promise<void> {
20
+ const { __graph_deps, ...unchanged } = node;
21
+ const end = { ...unchanged, ...changes } as AnyNode;
22
+ if (!animate) {
23
+ createRoot((dispose) => store.saveNode(node.id, createMemo(() => end), dispose));
24
+ return Promise.resolve();
25
+ }
26
+
27
+ // [key, start, end] for each changed number.
28
+ const moves = Object.entries(changes).map(([key, value]) => [key, num(node[key as keyof N] as Dtype), num(value)] as const);
29
+ const at = (t: number) => ({ ...end, ...Object.fromEntries(moves.map(([key, a, b]) => [key, scalar(a + (b - a) * t)])) } as AnyNode);
30
+
31
+ return new Promise((resolve) => createRoot((dispose) => {
32
+ const start = performance.now();
33
+ const [progress, setProgress] = createSignal(0);
34
+ store.saveNode(node.id, createMemo(() => (progress() < 1 ? at(easeInOut(progress())) : end)), dispose);
35
+ let frame = requestAnimationFrame(function step() {
36
+ setProgress(Math.min((performance.now() - start) / ANIMATION_MS, 1));
37
+ if (progress() < 1) frame = requestAnimationFrame(step);
38
+ else resolve();
39
+ });
40
+ // Another change to this node, or `clear`, disposes this one: stop moving and let the call return.
41
+ onCleanup(() => {
42
+ cancelAnimationFrame(frame);
43
+ resolve();
44
+ });
45
+ }));
46
+ }
47
+
48
+ // Any node, but not a whole Array: an Array is looped over, so each item is hidden or shown.
49
+ const anyItem = getNodeTypes().filter(type => type !== 'Array');
50
+
51
+ /**
52
+ * Hide a node: it is not drawn on the canvas.
53
+ * @param node - Node to hide
54
+ */
55
+ export const Hide: SideEffectFunc = {
56
+ keyword: 'hide',
57
+ parameters: [{ name: 'node', type: anyItem }],
58
+ run(inputs, store) {
59
+ store.updateNode(inputs[0].id, { hidden: true });
60
+ },
61
+ };
62
+
63
+ /**
64
+ * Show a hidden node.
65
+ * @param node - Node to show
66
+ */
67
+ export const Show: SideEffectFunc = {
68
+ keyword: 'show',
69
+ parameters: [{ name: 'node', type: anyItem }],
70
+ run(inputs, store) {
71
+ store.updateNode(inputs[0].id, { hidden: false });
72
+ },
73
+ };
74
+
75
+ /**
76
+ * Remove a node from the canvas.
77
+ * @param node - Node to remove
78
+ */
79
+ export const Remove: SideEffectFunc = {
80
+ keyword: 'remove',
81
+ parameters: [{ name: 'node', type: anyItem }],
82
+ run(inputs, store) {
83
+ store.removeNode(inputs[0].id);
84
+ },
85
+ };
86
+
87
+ /**
88
+ * Set a node's fill.
89
+ * @param node - Node to change
90
+ * @param color - A CSS color, like 'red' or '#ff0000', or 'none'
91
+ */
92
+ export const SetFill: SideEffectFunc = {
93
+ keyword: 'set_fill',
94
+ parameters: [{ name: 'node', type: anyItem }, { name: 'color', type: 'Text' }],
95
+ run(inputs, store) {
96
+ const fill = (inputs[1] as TextNode).value;
97
+ checkColor('set_fill', 'fill', fill);
98
+ store.updateNode(inputs[0].id, { fill });
99
+ },
100
+ };
101
+
102
+ /**
103
+ * Set a node's stroke.
104
+ * @param node - Node to change
105
+ * @param color - A CSS color, like 'red' or '#ff0000', or 'none'
106
+ */
107
+ export const SetStroke: SideEffectFunc = {
108
+ keyword: 'set_stroke',
109
+ parameters: [{ name: 'node', type: anyItem }, { name: 'color', type: 'Text' }],
110
+ run(inputs, store) {
111
+ const stroke = (inputs[1] as TextNode).value;
112
+ checkColor('set_stroke', 'stroke', stroke);
113
+ store.updateNode(inputs[0].id, { stroke });
114
+ },
115
+ };
116
+
117
+ /**
118
+ * Clear the canvas and reset the system nodes.
119
+ */
120
+ export const Clear: SideEffectFunc = {
121
+ keyword: 'clear',
122
+ parameters: [],
123
+ run(_inputs, store) {
124
+ store.clearNodes();
125
+ },
126
+ };
127
+
128
+ /**
129
+ * Move a point to (x, y). With `animate=True` it moves there over half a second.
130
+ * @param point - Point to move
131
+ * @param x - New x coordinate
132
+ * @param y - New y coordinate
133
+ */
134
+ export const TranslatePointTo: SideEffectFunc = {
135
+ keyword: 'translate_point_to',
136
+ parameters: [{ name: 'point', type: 'Point' }, { name: 'x', type: 'Scalar' }, { name: 'y', type: 'Scalar' }],
137
+ animatable: true,
138
+ run(inputs, store, animate) {
139
+ const [point, x, y] = inputs as [PointNode, ScalarNode, ScalarNode];
140
+ return replaceNode(store, point, { x: ref(x.value), y: ref(y.value) }, animate);
141
+ },
142
+ };
143
+
144
+ /**
145
+ * Move a point by (dx, dy). With `animate=True` it moves there over half a second.
146
+ * @param point - Point to move
147
+ * @param dx - Change in x
148
+ * @param dy - Change in y
149
+ */
150
+ export const TranslatePointBy: SideEffectFunc = {
151
+ keyword: 'translate_point_by',
152
+ parameters: [{ name: 'point', type: 'Point' }, { name: 'dx', type: 'Scalar' }, { name: 'dy', type: 'Scalar' }],
153
+ animatable: true,
154
+ run(inputs, store, animate) {
155
+ const [point, dx, dy] = inputs as [PointNode, ScalarNode, ScalarNode];
156
+ const x = add(ref(point.x), ref(dx.value));
157
+ const y = add(ref(point.y), ref(dy.value));
158
+ return replaceNode(store, point, { x, y }, animate);
159
+ },
160
+ };
161
+
162
+ /**
163
+ * Set a scalar to a new value. With `animate=True` it gets there over half a second.
164
+ * @param target - Scalar to change
165
+ * @param value - New value
166
+ */
167
+ export const TranslateScalarTo: SideEffectFunc = {
168
+ keyword: 'translate_scalar_to',
169
+ parameters: [{ name: 'target', type: 'Scalar' }, { name: 'value', type: 'Scalar' }],
170
+ animatable: true,
171
+ run(inputs, store, animate) {
172
+ const [target, value] = inputs as [ScalarNode, ScalarNode];
173
+ return replaceNode(store, target, { value: ref(value.value) }, animate);
174
+ },
175
+ };
176
+
177
+ /**
178
+ * Add `delta` to a scalar. With `animate=True` it gets there over half a second.
179
+ * @param target - Scalar to change
180
+ * @param delta - Amount to add
181
+ */
182
+ export const TranslateScalarBy: SideEffectFunc = {
183
+ keyword: 'translate_scalar_by',
184
+ parameters: [{ name: 'target', type: 'Scalar' }, { name: 'delta', type: 'Scalar' }],
185
+ animatable: true,
186
+ run(inputs, store, animate) {
187
+ const [target, delta] = inputs as [ScalarNode, ScalarNode];
188
+ return replaceNode(store, target, { value: add(ref(target.value), ref(delta.value)) }, animate);
189
+ },
190
+ };
191
+
192
+ /** Docs category for the functions defined in this file. */
193
+ export const category = 'Side Effects';
@@ -0,0 +1,40 @@
1
+ import path from 'node:path';
2
+ import { runnerImport, type Plugin } from 'vite';
3
+ import { APP_DIR } from '../../appDir';
4
+
5
+ /**
6
+ * Loads `registry.ts` and `state/Node.ts` once at build start, so a bad `functions/**\/*.ts`
7
+ * (e.g. a keyword used twice) or `nodes/**\/*.{ts,tsx}` (e.g. a type used twice) fails the build.
8
+ * Without this, the error only stops pages from prerendering.
9
+ */
10
+ export function checkFunctions(): Plugin {
11
+ let root = '';
12
+ let checked = false;
13
+
14
+ return {
15
+ name: 'check-functions',
16
+ apply: 'build',
17
+ configResolved(config) {
18
+ root = config.root;
19
+ },
20
+ // Runs once per build environment (client, ssr, ...); one check is enough.
21
+ async buildStart() {
22
+ if (checked) return;
23
+ checked = true;
24
+ try {
25
+ const options = {
26
+ configFile: false as const,
27
+ root,
28
+ logLevel: 'silent' as const,
29
+ resolve: { alias: { '~': APP_DIR } },
30
+ // Render functions use JSX. They never run here, so any JSX runtime that loads will do.
31
+ oxc: { jsx: { runtime: 'automatic' as const, importSource: 'solid-js/h' } },
32
+ };
33
+ await runnerImport(path.join(APP_DIR, 'lib/nodes/functions/registry.ts'), options);
34
+ await runnerImport(path.join(APP_DIR, 'lib/nodes/state/Node.ts'), options);
35
+ } catch (e) {
36
+ this.error((e as Error).message);
37
+ }
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,38 @@
1
+ import { onMount, onCleanup } from 'solid-js';
2
+ import { NodeStore } from '../state/NodeStore';
3
+
4
+ export function useCanvasResize(svgContainerRef: () => HTMLDivElement | undefined, store: NodeStore) {
5
+ let resizeObserver: ResizeObserver | undefined;
6
+
7
+ const updateCanvasSize = () => {
8
+ const container = svgContainerRef();
9
+ if (container) {
10
+ store.setCanvasBounds({
11
+ width: container.clientWidth,
12
+ height: container.clientHeight
13
+ });
14
+ }
15
+ };
16
+
17
+ onMount(() => {
18
+ updateCanvasSize();
19
+ if (typeof window !== 'undefined') {
20
+ window.addEventListener('resize', updateCanvasSize);
21
+ }
22
+ // Observe the container directly so the canvas re-fits on any layout change
23
+ // (e.g. dragging the split pane), not just window resizes.
24
+ const container = svgContainerRef();
25
+ if (container && typeof ResizeObserver !== 'undefined') {
26
+ resizeObserver = new ResizeObserver(() => updateCanvasSize());
27
+ resizeObserver.observe(container);
28
+ }
29
+ });
30
+
31
+ onCleanup(() => {
32
+ if (typeof window !== 'undefined') {
33
+ window.removeEventListener('resize', updateCanvasSize);
34
+ }
35
+ resizeObserver?.disconnect();
36
+ });
37
+ return updateCanvasSize;
38
+ }