@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.
- package/.nvmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +56 -2
- package/functions/midpoint.ts +29 -0
- package/functions/square.ts +20 -0
- package/init/index.js +87 -0
- package/init/site-agents.md +253 -0
- package/init/site-gitignore +28 -0
- package/init/site-tsconfig.json +20 -0
- package/init/site-vite.config.ts +6 -0
- package/layouts/header.tsx +16 -0
- package/layouts/index.tsx +10 -0
- package/nodes/square.tsx +36 -0
- package/package.json +59 -4
- package/pages/example.md +7 -0
- package/pages/python-editor.md +29 -0
- package/public/favicon.ico +0 -0
- package/public/logo.svg +5 -0
- package/site.yml +1 -0
- package/src/app.css +343 -0
- package/src/app.tsx +30 -0
- package/src/authorStyles.ts +3 -0
- package/src/components/Header.tsx +16 -0
- package/src/components/PageTitle.tsx +10 -0
- package/src/components/SplitPane.tsx +170 -0
- package/src/components/icons.tsx +22 -0
- package/src/entry-client.tsx +4 -0
- package/src/entry-server.tsx +21 -0
- package/src/global.d.ts +7 -0
- package/src/lib/appDir.ts +4 -0
- package/src/lib/autodiff/index.ts +49 -0
- package/src/lib/layouts/layout.ts +20 -0
- package/src/lib/layouts/vitePlugin.ts +39 -0
- package/src/lib/markdown/components/CanvasPage.tsx +25 -0
- package/src/lib/markdown/components/MarkdownBody.tsx +41 -0
- package/src/lib/markdown/components/PythonEditor.tsx +166 -0
- package/src/lib/markdown/frontmatter.ts +81 -0
- package/src/lib/markdown/pages.ts +23 -0
- package/src/lib/markdown/render.ts +54 -0
- package/src/lib/markdown/vitePlugin.ts +94 -0
- package/src/lib/nodes/components/CenteredGrid.tsx +59 -0
- package/src/lib/nodes/components/NodeCanvas.tsx +58 -0
- package/src/lib/nodes/execute.ts +166 -0
- package/src/lib/nodes/functions/Node2NodeFunc.ts +307 -0
- package/src/lib/nodes/functions/array.ts +36 -0
- package/src/lib/nodes/functions/autodiff.ts +64 -0
- package/src/lib/nodes/functions/basic.ts +298 -0
- package/src/lib/nodes/functions/registry.ts +46 -0
- package/src/lib/nodes/functions/sideEffects.ts +193 -0
- package/src/lib/nodes/functions/vitePlugin.ts +40 -0
- package/src/lib/nodes/hooks/useCanvasResize.ts +38 -0
- package/src/lib/nodes/jax.ts +191 -0
- package/src/lib/nodes/render/renderLogic.tsx +306 -0
- package/src/lib/nodes/state/NameGenerator.ts +31 -0
- package/src/lib/nodes/state/Node.ts +211 -0
- package/src/lib/nodes/state/NodeStore.ts +110 -0
- package/src/lib/nodes/tensors/broadcast.ts +101 -0
- package/src/lib/nodes/tensors/tensor.ts +42 -0
- package/src/lib/python/lift.ts +105 -0
- package/src/lib/python/py/__pycache__/pie.cpython-314.pyc +0 -0
- package/src/lib/python/py/pie.py +231 -0
- package/src/lib/python/py/pie_mpl.py +15 -0
- package/src/lib/python/py/show_figures.py +5 -0
- package/src/lib/python/runner.ts +202 -0
- package/src/lib/python/worker.ts +109 -0
- package/src/lib/site/vitePlugin.ts +43 -0
- package/src/routes/[...slug].tsx +38 -0
- package/src/routes/about.tsx +10 -0
- package/src/routes/index.tsx +14 -0
- package/src/routes/pages/index.tsx +20 -0
- package/styles/theme.css +27 -0
- 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
|
+
}
|