@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,211 @@
1
+ import type { Accessor, JSXElement } from 'solid-js';
2
+ import type { Dtype } from '../jax';
3
+ import {
4
+ renderPoint,
5
+ renderLine,
6
+ renderTriangle,
7
+ renderCircle,
8
+ renderEllipse,
9
+ renderArc,
10
+ renderBezierQuadratic,
11
+ renderBezierCubic,
12
+ renderPointGrad,
13
+ renderText,
14
+ } from '../render/renderLogic';
15
+
16
+ export interface Node {
17
+ id: string;
18
+ type: string;
19
+ hidden?: boolean;
20
+ /** A CSS color or 'none'. */
21
+ fill?: string;
22
+ /** A CSS color or 'none'. */
23
+ stroke?: string;
24
+ __graph_deps?: {
25
+ funcKeyword: string;
26
+ inputNodes: AnyNode[];
27
+ blueprintKey?: string;
28
+ // ID of the 'main' output node, set on a function's auxiliary nodes.
29
+ parentId?: string;
30
+ };
31
+ }
32
+
33
+ export function checkColor(keyword: string, name: string, value?: string) {
34
+ if (value !== undefined && value !== 'none' && !CSS.supports('color', value)) {
35
+ throw new Error(`${keyword}: ${name} '${value}' is not a valid color`);
36
+ }
37
+ }
38
+
39
+ export interface NodeDefinition<N extends Node = AnyNode> {
40
+ type: N['type'];
41
+ /** Start of generated ids: 'p' gives p0, p1, …. Default: `type` in lower case. */
42
+ prefix?: string;
43
+ /** Draws the node, in grid units. `unit` is pixels per grid unit. Without it the node is not drawn. */
44
+ render?(node: Accessor<N>, unit: Accessor<number>): JSXElement | null;
45
+ }
46
+
47
+ /**
48
+ * Drawn only when it has both `x` and `y`, the center of its box.
49
+ * `w`, `h`: the box size in grid units. 0 and 0: one line, no box. Above 0: fixed. -1: fits the text, at most 5.
50
+ */
51
+ export interface TextNode extends Node {
52
+ type: 'Text';
53
+ value: string;
54
+ x?: ScalarNode;
55
+ y?: ScalarNode;
56
+ /** In pixels. */
57
+ fontSize?: ScalarNode;
58
+ w?: ScalarNode;
59
+ h?: ScalarNode;
60
+ }
61
+ const TextDefinition: NodeDefinition<TextNode> = { type: 'Text', prefix: 'text', render: renderText };
62
+
63
+ export interface PointNode extends Node {
64
+ type: 'Point';
65
+ x: Dtype;
66
+ y: Dtype;
67
+ }
68
+ const PointDefinition: NodeDefinition<PointNode> = { type: 'Point', prefix: 'p', render: renderPoint };
69
+
70
+ export interface ScalarNode extends Node {
71
+ type: 'Scalar';
72
+ value: Dtype;
73
+ }
74
+ const ScalarDefinition: NodeDefinition<ScalarNode> = { type: 'Scalar', prefix: 'num' };
75
+
76
+ export interface TriangleNode extends Node {
77
+ type: 'Triangle';
78
+ vertices: PointNode[];
79
+ }
80
+ const TriangleDefinition: NodeDefinition<TriangleNode> = { type: 'Triangle', prefix: 'tr', render: renderTriangle };
81
+
82
+ export interface LineNode extends Node {
83
+ type: 'Line';
84
+ p1: PointNode;
85
+ p2: PointNode;
86
+ }
87
+ const LineDefinition: NodeDefinition<LineNode> = { type: 'Line', prefix: 'line', render: renderLine };
88
+
89
+ export interface CircleNode extends Node {
90
+ type: 'Circle';
91
+ center: PointNode;
92
+ radius: ScalarNode;
93
+ }
94
+ const CircleDefinition: NodeDefinition<CircleNode> = { type: 'Circle', prefix: 'circ', render: renderCircle };
95
+
96
+ export interface EllipseNode extends Node {
97
+ type: 'Ellipse';
98
+ center: PointNode;
99
+ radiusX: ScalarNode;
100
+ radiusY: ScalarNode;
101
+ rotation: ScalarNode;
102
+ }
103
+ const EllipseDefinition: NodeDefinition<EllipseNode> = { type: 'Ellipse', prefix: 'ell', render: renderEllipse };
104
+
105
+ export interface BezierQuadraticNode extends Node {
106
+ type: 'BezierQuadratic';
107
+ p1: PointNode;
108
+ control: PointNode;
109
+ p2: PointNode;
110
+ }
111
+ const BezierQuadraticDefinition: NodeDefinition<BezierQuadraticNode> = { type: 'BezierQuadratic', prefix: 'bq', render: renderBezierQuadratic };
112
+
113
+ export interface BezierCubicNode extends Node {
114
+ type: 'BezierCubic';
115
+ p1: PointNode;
116
+ control1: PointNode;
117
+ control2: PointNode;
118
+ p2: PointNode;
119
+ }
120
+ const BezierCubicDefinition: NodeDefinition<BezierCubicNode> = { type: 'BezierCubic', prefix: 'bc', render: renderBezierCubic };
121
+
122
+ export interface ArcNode extends Node {
123
+ type: 'Arc';
124
+ center: PointNode;
125
+ radius: ScalarNode;
126
+ startAngle: ScalarNode;
127
+ endAngle: ScalarNode;
128
+ }
129
+ const ArcDefinition: NodeDefinition<ArcNode> = { type: 'Arc', prefix: 'arc', render: renderArc };
130
+
131
+ /** d target / d point, drawn as an arrow (dx, dy) starting at `point`. */
132
+ export interface PointGradNode extends Node {
133
+ type: 'PointGrad';
134
+ point: PointNode;
135
+ target: ScalarNode;
136
+ dx: Dtype;
137
+ dy: Dtype;
138
+ }
139
+ const PointGradDefinition: NodeDefinition<PointGradNode> = { type: 'PointGrad', prefix: 'pointgrad', render: renderPointGrad };
140
+
141
+ /** `items` in row-major order: shape [2, 3] holds row 0's three items, then row 1's. */
142
+ export interface ArrayNode extends Node {
143
+ type: 'Array';
144
+ itemType: NodeType;
145
+ shape: number[];
146
+ items: AnyNode[];
147
+ }
148
+ const ArrayDefinition: NodeDefinition<ArrayNode> = { type: 'Array', prefix: 'arr' };
149
+
150
+ export interface DummyNode extends Node {
151
+ type: 'Dummy';
152
+ }
153
+ /** An empty prefix gives an empty id. */
154
+ const DummyDefinition: NodeDefinition<DummyNode> = { type: 'Dummy', prefix: '' };
155
+
156
+ export interface NodeTypeMap {
157
+ Text: TextNode;
158
+ Point: PointNode;
159
+ Scalar: ScalarNode;
160
+ Triangle: TriangleNode;
161
+ Line: LineNode;
162
+ Circle: CircleNode;
163
+ Ellipse: EllipseNode;
164
+ BezierQuadratic: BezierQuadraticNode;
165
+ BezierCubic: BezierCubicNode;
166
+ Arc: ArcNode;
167
+ PointGrad: PointGradNode;
168
+ Array: ArrayNode;
169
+ Dummy: DummyNode;
170
+ }
171
+
172
+ export type AnyNode = NodeTypeMap[keyof NodeTypeMap];
173
+ export type NodeType = keyof NodeTypeMap;
174
+
175
+ const isNodeDefinition = (def: unknown): def is NodeDefinition =>
176
+ typeof def === 'object' && def !== null && typeof (def as NodeDefinition).type === 'string';
177
+
178
+ // The site's own node types: each `nodes/**/*.{ts,tsx}` exports `NodeDefinition`s, like `nodes/square.tsx`.
179
+ const customModules = import.meta.glob<Record<string, unknown>>('/nodes/**/*.{ts,tsx}', { eager: true });
180
+
181
+ // File path -> its definitions. Built-ins first.
182
+ const definitionsByFile: [string, unknown[]][] = [
183
+ ['src/lib/nodes/state/Node.ts', [
184
+ TextDefinition, PointDefinition, ScalarDefinition, TriangleDefinition, LineDefinition, CircleDefinition,
185
+ EllipseDefinition, BezierQuadraticDefinition, BezierCubicDefinition, ArcDefinition, PointGradDefinition,
186
+ ArrayDefinition, DummyDefinition,
187
+ ]],
188
+ ...Object.entries(customModules).map(([path, mod]): [string, unknown[]] => [path.slice(1), Object.values(mod)]),
189
+ ];
190
+
191
+ const nodeDefinitions = new Map<string, NodeDefinition>();
192
+ const sourceFile = new Map<string, string>();
193
+ for (const [file, defs] of definitionsByFile) {
194
+ for (const def of defs.filter(isNodeDefinition)) {
195
+ const other = sourceFile.get(def.type);
196
+ if (other !== undefined) {
197
+ throw new Error(`Duplicate node type '${def.type}': ${file} and ${other}`);
198
+ }
199
+ nodeDefinitions.set(def.type, def);
200
+ sourceFile.set(def.type, file);
201
+ }
202
+ }
203
+
204
+ export function getNodeDefinition(type: string): NodeDefinition | undefined {
205
+ return nodeDefinitions.get(type);
206
+ }
207
+
208
+ /** Every node type, built-in and the site's own. */
209
+ export function getNodeTypes(): NodeType[] {
210
+ return [...nodeDefinitions.keys()] as NodeType[];
211
+ }
@@ -0,0 +1,110 @@
1
+ import { Accessor, batch, createMemo, createRoot, createSignal, untrack } from 'solid-js';
2
+ import { createStore, reconcile } from 'solid-js/store';
3
+ import { AnyNode, ScalarNode } from './Node';
4
+ import { NameGenerator } from './NameGenerator';
5
+ import { num, scalar } from '../jax';
6
+
7
+ /** Below this many pixels per unit, the grid hides its lines and keeps only the axes. */
8
+ export const MIN_GRID_UNIT = 20;
9
+
10
+ export type NodeSignal = {
11
+ signal: Accessor<AnyNode>;
12
+ dispose?: () => void;
13
+ };
14
+
15
+ export class NodeStore {
16
+ public nodes: Record<string, NodeSignal>;
17
+ public setNodes: (id: string, value: NodeSignal | undefined) => void;
18
+ public reconcileNodes: (value: Record<string, NodeSignal>) => void;
19
+
20
+ public canvasBounds: Accessor<{ width: number; height: number }>;
21
+ public setCanvasBounds: (bounds: { width: number; height: number }) => void;
22
+
23
+ /** Pixels per grid unit, read from the `unit` node. */
24
+ public unit!: Accessor<number>;
25
+
26
+ public nameGenerator: NameGenerator;
27
+
28
+ constructor() {
29
+ const [nodesState, setNodesState] = createStore<Record<string, NodeSignal>>({});
30
+ this.nodes = nodesState;
31
+ this.setNodes = (id, value) => { setNodesState(id, value as any); };
32
+ this.reconcileNodes = (value) => { setNodesState(reconcile(value)); };
33
+
34
+ const [canvasBounds, setCanvasBounds] = createSignal({ width: 500, height: 500 });
35
+ this.canvasBounds = canvasBounds;
36
+ this.setCanvasBounds = setCanvasBounds;
37
+
38
+ createRoot(() => {
39
+ this.unit = createMemo(() => {
40
+ const node = this.nodes['unit']?.signal() as ScalarNode | undefined;
41
+ // max(1, ...): a zero, negative or NaN unit would break the viewBox.
42
+ return node ? Math.max(1, num(node.value)) : 50;
43
+ });
44
+ });
45
+
46
+ // untrack: names are generated inside reactive computations, which must not rerun when the checked node changes.
47
+ this.nameGenerator = new NameGenerator((name) => untrack(() => this.nodes[name] !== undefined));
48
+
49
+ this.initSystemNodes();
50
+ }
51
+
52
+ public initSystemNodes() {
53
+ const unitId = 'unit';
54
+ createRoot((dispose) => {
55
+ const signal = createMemo(() => ({ id: unitId, type: 'Scalar', value: scalar(50) } as ScalarNode));
56
+ this.saveNode(unitId, signal, dispose);
57
+ });
58
+ }
59
+
60
+ public clearNodes() {
61
+ batch(() => {
62
+ for (const key in this.nodes) {
63
+ if (this.nodes[key] && typeof this.nodes[key].dispose === 'function') {
64
+ this.nodes[key].dispose!();
65
+ }
66
+ }
67
+ this.reconcileNodes({});
68
+ this.nameGenerator.resetCounters();
69
+ });
70
+
71
+ this.initSystemNodes();
72
+ }
73
+
74
+ public saveNode(id: string, signal: Accessor<AnyNode>, dispose?: () => void) {
75
+ if (this.nodes[id] && typeof this.nodes[id].dispose === 'function') {
76
+ this.nodes[id].dispose!();
77
+ }
78
+ this.setNodes(id, { signal, dispose });
79
+ }
80
+
81
+ /** Sets `props` on node `id`. Unlike `saveNode`, the node keeps following its inputs. */
82
+ public updateNode(id: string, props: Partial<AnyNode>) {
83
+ // Read out now: `setNodes` merges into the stored entry, so it would overwrite them.
84
+ const { signal: oldSignal, dispose: oldDispose } = this.nodes[id];
85
+ createRoot((dispose) => {
86
+ const signal = createMemo(() => ({ ...oldSignal(), ...props } as AnyNode));
87
+ // Not `saveNode`: that would dispose the old node, which the new one reads.
88
+ this.setNodes(id, { signal, dispose: () => { dispose(); oldDispose?.(); } });
89
+ });
90
+ }
91
+
92
+ public getNodeValue(id: string): AnyNode | undefined {
93
+ return this.nodes[id]?.signal();
94
+ }
95
+
96
+ public removeNode(id: string) {
97
+ const nodeSignal = this.nodes[id];
98
+ if (nodeSignal) {
99
+ this.setNodes(id, undefined);
100
+
101
+ if (typeof nodeSignal.dispose === 'function') {
102
+ queueMicrotask(() => {
103
+ if (nodeSignal.dispose) {
104
+ nodeSignal.dispose();
105
+ }
106
+ });
107
+ }
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,101 @@
1
+ import type { BlueprintNode, Node2NodeFunc, ParameterDefinition } from '../functions/Node2NodeFunc';
2
+ import { AnyNode, ArrayNode, DummyNode } from '../state/Node';
3
+
4
+ const DUMMY: DummyNode = { id: '', type: 'Dummy' };
5
+
6
+ /** Whether an input of type `type` is looped over: an array going to a parameter that doesn't take whole arrays. */
7
+ export function isBroadcast(type: string, param: ParameterDefinition): boolean {
8
+ const types = Array.isArray(param.type) ? param.type : [param.type];
9
+ return type === 'Array' && !types.includes('Array');
10
+ }
11
+
12
+ const formatShape = (shape: number[]) => `(${shape.join(', ')}${shape.length === 1 ? ',' : ''})`;
13
+
14
+ /** numpy broadcasting: shapes are lined up from the right, and each dimension must match or be 1. */
15
+ export function broadcastShapes(shapes: number[][]): number[] {
16
+ const ndim = Math.max(...shapes.map(shape => shape.length));
17
+ const result: number[] = [];
18
+ for (let d = 0; d < ndim; d++) {
19
+ let size = 1;
20
+ for (const shape of shapes) {
21
+ const dim = shape[d - ndim + shape.length] ?? 1;
22
+ if (dim === 1) continue;
23
+ if (size !== 1 && size !== dim) {
24
+ throw new Error(`shapes ${shapes.map(formatShape).join(' and ')} can't be broadcast together`);
25
+ }
26
+ size = dim;
27
+ }
28
+ result.push(size);
29
+ }
30
+ return result;
31
+ }
32
+
33
+ /** Row-major position `flat` as one index per dimension of `shape`. */
34
+ function unravel(flat: number, shape: number[]): number[] {
35
+ const index: number[] = [];
36
+ for (let d = shape.length - 1; d >= 0; d--) {
37
+ index[d] = flat % shape[d];
38
+ flat = Math.floor(flat / shape[d]);
39
+ }
40
+ return index;
41
+ }
42
+
43
+ /** The row-major position in an array of `shape` that broadcast output index `index` reads. */
44
+ function sourceIndex(shape: number[], index: number[]): number {
45
+ const offset = index.length - shape.length;
46
+ return shape.reduce((flat, dim, d) => flat * dim + (dim === 1 ? 0 : index[d + offset]), 0);
47
+ }
48
+
49
+ /**
50
+ * The inputs of each item when an input is broadcast (see `isBroadcast`), or just `inputs`
51
+ * when none is. Like `computeBlueprint`, an item with a Dummy input is skipped.
52
+ */
53
+ export function broadcastItems(parameters: ParameterDefinition[], inputs: AnyNode[]): AnyNode[][] {
54
+ // An input is undefined for a left-out optional parameter.
55
+ const broadcasts = inputs.map((node, i) => node !== undefined && isBroadcast(node.type, parameters[i]));
56
+ if (!broadcasts.includes(true)) return [inputs];
57
+
58
+ const shapes = inputs.map((node, i) => broadcasts[i] ? (node as ArrayNode).shape : []);
59
+ const shape = broadcastShapes(shapes);
60
+ const size = shape.reduce((a, b) => a * b, 1);
61
+
62
+ const items: AnyNode[][] = [];
63
+ for (let k = 0; k < size; k++) {
64
+ const index = unravel(k, shape);
65
+ const itemInputs = inputs.map((node, i) => broadcasts[i] ? (node as ArrayNode).items[sourceIndex(shapes[i], index)] : node);
66
+ if (!itemInputs.some(node => node?.type === 'Dummy')) items.push(itemInputs);
67
+ }
68
+ return items;
69
+ }
70
+
71
+ /**
72
+ * `func.compute`, run once per item when an input is broadcast (see `isBroadcast`).
73
+ * Item k's nodes are saved under keys `k.main`, `k.l_0`, ..., and `main` is the array of
74
+ * every item's main node. An item is Dummy, and not saved, when one of its inputs is
75
+ * Dummy or its own main node is.
76
+ */
77
+ export function computeBlueprint(func: Node2NodeFunc, inputs: AnyNode[]): Record<string, BlueprintNode> {
78
+ // An input is undefined for a left-out optional parameter.
79
+ const broadcasts = inputs.map((node, i) => node !== undefined && isBroadcast(node.type, func.parameters[i]));
80
+ if (!broadcasts.includes(true)) return func.compute(inputs);
81
+
82
+ const shapes = inputs.map((node, i) => broadcasts[i] ? (node as ArrayNode).shape : []);
83
+ const shape = broadcastShapes(shapes);
84
+ const size = shape.reduce((a, b) => a * b, 1);
85
+
86
+ const blueprint: Record<string, BlueprintNode> = {};
87
+ const items: BlueprintNode[] = [];
88
+ for (let k = 0; k < size; k++) {
89
+ const index = unravel(k, shape);
90
+ const itemInputs = inputs.map((node, i) => broadcasts[i] ? (node as ArrayNode).items[sourceIndex(shapes[i], index)] : node);
91
+ const item = itemInputs.some(node => node?.type === 'Dummy') ? { main: DUMMY } : func.compute(itemInputs);
92
+ if (item.main.type === 'Dummy') {
93
+ items.push(DUMMY);
94
+ continue;
95
+ }
96
+ for (const key in item) blueprint[`${k}.${key}`] = item[key];
97
+ items.push(item.main);
98
+ }
99
+ blueprint.main = { type: 'Array', itemType: func.outputType, shape, items };
100
+ return blueprint;
101
+ }
@@ -0,0 +1,42 @@
1
+ import type { Arg, ExecuteResult } from '../execute';
2
+ import type { NodeStore } from '../state/NodeStore';
3
+ import { NodeType } from '../state/Node';
4
+
5
+ export type LeafArg = Exclude<Arg, Arg[]>;
6
+
7
+ /** The items of a nested list, in row-major order. */
8
+ export const leaves = (list: Arg[]): LeafArg[] => list.flatMap(item => Array.isArray(item) ? leaves(item) : [item]);
9
+
10
+ /** A nested list's shape: [[a, b], [c, d]] has shape [2, 2]. */
11
+ export function listShape(list: Arg[]): number[] {
12
+ if (list.length === 0) throw new Error('a list must not be empty');
13
+ const shapes = list.map(item => Array.isArray(item) ? listShape(item) : []);
14
+ if (shapes.some(shape => shape.join() !== shapes[0].join())) {
15
+ throw new Error('nested lists must all have the same length');
16
+ }
17
+ return [list.length, ...shapes[0]];
18
+ }
19
+
20
+ export async function sha256(text: string): Promise<string> {
21
+ const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text));
22
+ return Array.from(new Uint8Array(digest), byte => byte.toString(16).padStart(2, '0')).join('');
23
+ }
24
+
25
+ /** The node at `position` of array `arrayId`'s items. Creates nothing. */
26
+ export function getItem(store: NodeStore, arrayId: string, position: number): ExecuteResult {
27
+ const array = store.getNodeValue(arrayId);
28
+ if (array?.type !== 'Array') throw new Error(`'${arrayId}' is not an array`);
29
+ const item = array.items[position];
30
+ if (item.type === 'Dummy') throw new Error(`item ${position} of '${arrayId}' is Dummy`);
31
+ return { id: item.id, type: item.type };
32
+ }
33
+
34
+ /** The one type all items of a nested list share. `typeOf` gives an item's type. */
35
+ export function listItemType(list: Arg[], typeOf: (item: LeafArg) => string): NodeType {
36
+ listShape(list);
37
+ const types = new Set(leaves(list).map(typeOf));
38
+ if (types.size > 1) {
39
+ throw new Error(`all items of a list must have the same type, got ${[...types].join(', ')}`);
40
+ }
41
+ return [...types][0] as NodeType;
42
+ }
@@ -0,0 +1,105 @@
1
+ import { createSignal } from 'solid-js';
2
+ import { NodeReconciler, PYTHON_KEYWORD_PREFIX, type BlueprintNode, type Node2NodeFunc } from '~/lib/nodes/functions/Node2NodeFunc';
3
+ import { checkOut } from '~/lib/nodes/execute';
4
+ import type { NodeStore } from '~/lib/nodes/state/NodeStore';
5
+ import { checkColor, type AnyNode } from '~/lib/nodes/state/Node';
6
+ import { num, scalar } from '~/lib/nodes/jax';
7
+
8
+ /** The type passed to `pie.lift`: `pie.Scalar`, `pie.Point`, `pie.Text`, or `pie.Array[...]` of `itemType`. */
9
+ type Kind = 'Scalar' | 'Point' | 'Text' | 'Array';
10
+ type ItemType = 'Scalar' | 'Point';
11
+
12
+ /** An input node's value as Python gets it: a Scalar's number, a Point's [x, y], a Text's string, or an Array's values in row-major order. */
13
+ export type InputValue = number | [number, number] | string | PyResult;
14
+
15
+ /** A result from Python. `values`: every number in row-major order, a Point as x, y; or a Text's string. A Scalar, Point or Text has shape []. */
16
+ export interface PyResult {
17
+ shape: number[];
18
+ values: number[] | [string];
19
+ }
20
+
21
+ /** Sent by Python once `g(a)`, for `g = pie.lift(f, ...)`, has its first result. */
22
+ export interface LiftSave extends PyResult {
23
+ id: string;
24
+ name: string;
25
+ inputIds: string[];
26
+ kind: Kind;
27
+ itemType?: ItemType;
28
+ hidden: boolean;
29
+ fill?: string;
30
+ stroke?: string;
31
+ }
32
+
33
+ export type Rerun = (nodeId: string, values: InputValue[], done: (result?: PyResult, error?: string) => void) => void;
34
+
35
+ function inputValue(node: AnyNode | undefined, id: string): InputValue {
36
+ if (!node) throw new Error(`Unknown node '${id}'`);
37
+ if (node.type === 'Scalar') return num(node.value);
38
+ if (node.type === 'Point') return [num(node.x), num(node.y)];
39
+ if (node.type === 'Text') return node.value;
40
+ if (node.type === 'Array') {
41
+ const values = node.items.flatMap(item => inputValue(item, item.id)) as number[];
42
+ return { shape: node.itemType === 'Point' ? [...node.shape, 2] : node.shape, values };
43
+ }
44
+ throw new Error(`'${id}' is a ${node.type}; only Scalar, Point and Text nodes and Arrays of them can be inputs`);
45
+ }
46
+
47
+ /** First half of `g(a)`: checks the inputs, `out`, `fill` and `stroke`, and reserves the output id. Python then calls `f` with `values`. */
48
+ export function startLift(store: NodeStore, name: string, inputIds: string[], kind: Kind, out?: string, fill?: string, stroke?: string) {
49
+ const values = inputIds.map(id => inputValue(store.getNodeValue(id), id));
50
+ checkOut(name, out, inputIds.map(id => ({ id })));
51
+ checkColor(name, 'fill', fill);
52
+ checkColor(name, 'stroke', stroke);
53
+ const id = out ?? store.nameGenerator.generateAutoName(store.nameGenerator.getPrefixForType(kind));
54
+ return { id, values };
55
+ }
56
+
57
+ /** Item k of an Array is saved under key `k`, so a rerun with a new shape keeps, adds or removes items by position. */
58
+ function toBlueprint(kind: Kind, itemType: ItemType | undefined, result: PyResult): Record<string, BlueprintNode> {
59
+ if (kind === 'Text') return { main: { type: 'Text', value: result.values[0] } };
60
+ const { shape } = result;
61
+ const values = result.values as number[];
62
+ const item = (type: ItemType, k: number) => type === 'Point'
63
+ ? { type: 'Point', x: scalar(values[2 * k]), y: scalar(values[2 * k + 1]) }
64
+ : { type: 'Scalar', value: scalar(values[k]) };
65
+ if (kind !== 'Array') return { main: item(kind, 0) };
66
+ const blueprint: Record<string, BlueprintNode> = {};
67
+ const items: BlueprintNode[] = [];
68
+ const size = shape.reduce((a, b) => a * b, 1);
69
+ for (let k = 0; k < size; k++) items.push(blueprint[k] = item(itemType!, k));
70
+ blueprint.main = { type: 'Array', itemType, shape, items };
71
+ return blueprint;
72
+ }
73
+
74
+ /**
75
+ * Second half of `g(a)`: saves the node with Python's first result. When an input's value changes,
76
+ * the node keeps its value and asks `rerun` for a new one. Rerun errors go to the console.
77
+ */
78
+ export function saveLift(store: NodeStore, save: LiftSave, rerun: Rerun) {
79
+ const { id, name, inputIds, kind, itemType, hidden, fill, stroke } = save;
80
+ const [blueprint, setBlueprint] = createSignal(toBlueprint(kind, itemType, save));
81
+ // The input values of the latest result or rerun request. Unset until the first compute, which uses the first result.
82
+ let requested: string | undefined;
83
+
84
+ const func: Node2NodeFunc = {
85
+ keyword: `${PYTHON_KEYWORD_PREFIX}${name}`,
86
+ outputType: kind,
87
+ // 'Array' too, so `computeBlueprint` passes an Array input whole instead of looping over it.
88
+ parameters: inputIds.map((_, i) => ({ name: `input${i}`, type: ['Scalar', 'Point', 'Text', 'Array'] })),
89
+ broadcastable: false,
90
+ compute(inputs) {
91
+ const values = inputs.map((node, i) => inputValue(node, inputIds[i]));
92
+ const key = JSON.stringify(values);
93
+ if (requested !== undefined && key !== requested) {
94
+ rerun(id, values, (result, error) => {
95
+ if (error !== undefined) console.error(`${name}: ${error}`);
96
+ // A reply for an older request is dropped.
97
+ else if (key === requested) setBlueprint(toBlueprint(kind, itemType, result!));
98
+ });
99
+ }
100
+ requested = key;
101
+ return blueprint();
102
+ },
103
+ };
104
+ new NodeReconciler().evaluate(func, inputIds, store, id, hidden, fill, stroke);
105
+ }