@hatiolab/figure-model 0.1.30 → 0.1.31
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/README.md +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/release-contract.d.ts +2 -2
- package/dist/release-contract.d.ts.map +1 -1
- package/dist/release-contract.js +45 -3
- package/dist/release-contract.js.map +1 -1
- package/dist/types.d.ts +15 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +15 -4
- package/dist/types.js.map +1 -1
- package/dist/v3-asset-types.d.ts +116 -0
- package/dist/v3-asset-types.d.ts.map +1 -0
- package/dist/v3-asset-types.js +2 -0
- package/dist/v3-asset-types.js.map +1 -0
- package/dist/v3-asset.d.ts +8 -0
- package/dist/v3-asset.d.ts.map +1 -0
- package/dist/v3-asset.js +135 -0
- package/dist/v3-asset.js.map +1 -0
- package/dist/v3-authoring-session.d.ts +119 -0
- package/dist/v3-authoring-session.d.ts.map +1 -0
- package/dist/v3-authoring-session.js +132 -0
- package/dist/v3-authoring-session.js.map +1 -0
- package/dist/v3-capabilities.d.ts +3 -0
- package/dist/v3-capabilities.d.ts.map +1 -0
- package/dist/v3-capabilities.js +221 -0
- package/dist/v3-capabilities.js.map +1 -0
- package/dist/v3-contract.d.ts +7 -0
- package/dist/v3-contract.d.ts.map +1 -0
- package/dist/v3-contract.js +12 -0
- package/dist/v3-contract.js.map +1 -0
- package/dist/v3-create.d.ts +25 -0
- package/dist/v3-create.d.ts.map +1 -0
- package/dist/v3-create.js +50 -0
- package/dist/v3-create.js.map +1 -0
- package/dist/v3-document.d.ts +5 -0
- package/dist/v3-document.d.ts.map +1 -0
- package/dist/v3-document.js +31 -0
- package/dist/v3-document.js.map +1 -0
- package/dist/v3-editor-workspace.d.ts +334 -0
- package/dist/v3-editor-workspace.d.ts.map +1 -0
- package/dist/v3-editor-workspace.js +130 -0
- package/dist/v3-editor-workspace.js.map +1 -0
- package/dist/v3-graph-types.d.ts +91 -0
- package/dist/v3-graph-types.d.ts.map +1 -0
- package/dist/v3-graph-types.js +2 -0
- package/dist/v3-graph-types.js.map +1 -0
- package/dist/v3-graph.d.ts +5 -0
- package/dist/v3-graph.d.ts.map +1 -0
- package/dist/v3-graph.js +631 -0
- package/dist/v3-graph.js.map +1 -0
- package/dist/v3-json.d.ts +13 -0
- package/dist/v3-json.d.ts.map +1 -0
- package/dist/v3-json.js +94 -0
- package/dist/v3-json.js.map +1 -0
- package/dist/v3-part-edit.d.ts +12 -0
- package/dist/v3-part-edit.d.ts.map +1 -0
- package/dist/v3-part-edit.js +53 -0
- package/dist/v3-part-edit.js.map +1 -0
- package/docs/assembly-constraints-v3.md +141 -0
- package/docs/prototypes/README.md +73 -0
- package/docs/prototypes/render-dryroom.py +60 -0
- package/docs/prototypes/render-full-dryroom.mjs +86 -0
- package/docs/prototypes/shapeassembly-probe-results.json +325 -0
- package/docs/prototypes/shapeassembly-probe.py +84 -0
- package/docs/prototypes/v3-asset-samples.mjs +15 -0
- package/docs/prototypes/v3-asset-store.mjs +52 -0
- package/docs/prototypes/v3-asset-store.test.mjs +59 -0
- package/docs/prototypes/v3-asset.mjs +1 -0
- package/docs/prototypes/v3-asset.schema.json +44 -0
- package/docs/prototypes/v3-asset.test.mjs +68 -0
- package/docs/prototypes/v3-authoring-session.mjs +1 -0
- package/docs/prototypes/v3-authoring-session.test.mjs +65 -0
- package/docs/prototypes/v3-capability-plan.mjs +1 -0
- package/docs/prototypes/v3-capability-plan.test.mjs +56 -0
- package/docs/prototypes/v3-capability-samples.mjs +23 -0
- package/docs/prototypes/v3-document.mjs +2 -0
- package/docs/prototypes/v3-document.schema.json +66 -0
- package/docs/prototypes/v3-document.test.mjs +55 -0
- package/docs/prototypes/v3-dryroom-mesh.mjs +60 -0
- package/docs/prototypes/v3-dryroom-mesh.test.mjs +30 -0
- package/docs/prototypes/v3-editor-browser.mjs +97 -0
- package/docs/prototypes/v3-editor-workspace.mjs +1 -0
- package/docs/prototypes/v3-editor-workspace.test.mjs +35 -0
- package/docs/prototypes/v3-feature-pattern.test.mjs +131 -0
- package/docs/prototypes/v3-full-conveyor.mjs +60 -0
- package/docs/prototypes/v3-full-conveyor.test.mjs +61 -0
- package/docs/prototypes/v3-full-dryroom.mjs +67 -0
- package/docs/prototypes/v3-full-dryroom.test.mjs +54 -0
- package/docs/prototypes/v3-full-forklift.mjs +40 -0
- package/docs/prototypes/v3-full-forklift.test.mjs +37 -0
- package/docs/prototypes/v3-full-robot.mjs +35 -0
- package/docs/prototypes/v3-full-robot.test.mjs +68 -0
- package/docs/prototypes/v3-kernel.mjs +147 -0
- package/docs/prototypes/v3-kernel.test.mjs +144 -0
- package/docs/prototypes/v3-layout.test.mjs +281 -0
- package/docs/prototypes/v3-linear-comparison.py +183 -0
- package/docs/prototypes/v3-more-samples.test.mjs +182 -0
- package/docs/prototypes/v3-payload-collision.mjs +54 -0
- package/docs/prototypes/v3-payload-collision.test.mjs +68 -0
- package/docs/prototypes/v3-payload-fit.mjs +58 -0
- package/docs/prototypes/v3-payload-fit.test.mjs +59 -0
- package/docs/prototypes/v3-repeated-binding.mjs +13 -0
- package/docs/prototypes/v3-repeated-binding.test.mjs +46 -0
- package/docs/prototypes/v3-runtime-browser.mjs +76 -0
- package/docs/prototypes/v3-samples.test.mjs +144 -0
- package/docs/prototypes/v3-semantics.test.mjs +55 -0
- package/docs/prototypes/v3-spatial-plan.test.mjs +102 -0
- package/docs/prototypes/v3-typed-plan.mjs +3 -0
- package/docs/prototypes/v3-typed-plan.test.mjs +70 -0
- package/docs/prototypes/v3-typed-repeat.test.mjs +96 -0
- package/docs/v3-asset-persistence.md +66 -0
- package/docs/v3-authoring-transactions.md +57 -0
- package/docs/v3-capability-bindings.md +109 -0
- package/docs/v3-common-kernel-experiment.md +116 -0
- package/docs/v3-core-semantics.md +303 -0
- package/docs/v3-cutover.md +27 -0
- package/docs/v3-design.md +388 -0
- package/docs/v3-document-contract.md +55 -0
- package/docs/v3-dryroom-profile-validation.md +87 -0
- package/docs/v3-editor-integration.md +59 -0
- package/docs/v3-full-conveyor-validation.md +69 -0
- package/docs/v3-full-dryroom-validation.md +55 -0
- package/docs/v3-full-forklift-validation.md +41 -0
- package/docs/v3-full-robot-validation.md +64 -0
- package/docs/v3-layout-system.md +318 -0
- package/docs/v3-minimal-model.md +255 -0
- package/docs/v3-operator-contracts.md +148 -0
- package/docs/v3-payload-collision-validation.md +42 -0
- package/docs/v3-payload-contact-validation.md +56 -0
- package/docs/v3-repeated-binding-validation.md +42 -0
- package/docs/v3-research-review.md +160 -0
- package/docs/v3-runtime-extraction.md +107 -0
- package/docs/v3-sample-validation-expanded.md +106 -0
- package/docs/v3-sample-validation.md +97 -0
- package/docs/v3-shapeassembly-reproduction.md +99 -0
- package/docs/v3-spatial-contracts.md +84 -0
- package/docs/v3-status.md +41 -0
- package/docs/v3-storage-integration.md +58 -0
- package/docs/v3-typed-repeat-contracts.md +79 -0
- package/package.json +1 -1
package/dist/v3-graph.js
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
// V3 experimental graph evaluator. Operator semantics are checked at compile and evaluation boundaries.
|
|
2
|
+
import { V3ContractError as ContractError } from "./v3-contract.js";
|
|
3
|
+
export { ContractError };
|
|
4
|
+
const fail = (c, p, d) => {
|
|
5
|
+
throw new ContractError(c, p, d);
|
|
6
|
+
};
|
|
7
|
+
const units = {
|
|
8
|
+
mm: [[1, 0, 0], 1],
|
|
9
|
+
cm: [[1, 0, 0], 10],
|
|
10
|
+
m: [[1, 0, 0], 1000],
|
|
11
|
+
rad: [[0, 1, 0], 1],
|
|
12
|
+
deg: [[0, 1, 0], Math.PI / 180],
|
|
13
|
+
s: [[0, 0, 1], 1],
|
|
14
|
+
ratio: [[0, 0, 0], 1],
|
|
15
|
+
percent: [[0, 0, 0], 0.01]
|
|
16
|
+
};
|
|
17
|
+
const length = [1, 0, 0], ratio = [0, 0, 0], angle = [0, 1, 0];
|
|
18
|
+
const countType = { kind: 'count' };
|
|
19
|
+
units.count = [countType, 1];
|
|
20
|
+
const eq = (a, b) => Array.isArray(a) && Array.isArray(b)
|
|
21
|
+
? a.length === b.length && a.every((v, i) => v === b[i])
|
|
22
|
+
: JSON.stringify(a) === JSON.stringify(b);
|
|
23
|
+
function fields(o, required, optional, path) {
|
|
24
|
+
if (!o || typeof o !== 'object' || Array.isArray(o))
|
|
25
|
+
fail('SCHEMA', path, 'object required');
|
|
26
|
+
for (const k of required)
|
|
27
|
+
if (!Object.hasOwn(o, k))
|
|
28
|
+
fail('SCHEMA', path, `missing ${k}`);
|
|
29
|
+
for (const k of Object.keys(o))
|
|
30
|
+
if (![...required, ...optional].includes(k))
|
|
31
|
+
fail('SCHEMA', path, `unknown ${k}`);
|
|
32
|
+
}
|
|
33
|
+
function id(x, path) {
|
|
34
|
+
if (typeof x !== 'string' || !/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(x))
|
|
35
|
+
fail('ID', path, 'invalid id');
|
|
36
|
+
return x;
|
|
37
|
+
}
|
|
38
|
+
function num(x, path) {
|
|
39
|
+
if (typeof x !== 'number' || !Number.isFinite(x))
|
|
40
|
+
fail('NON_FINITE', path, String(x));
|
|
41
|
+
return x;
|
|
42
|
+
}
|
|
43
|
+
function unit(x, path) {
|
|
44
|
+
if (!Object.hasOwn(units, x))
|
|
45
|
+
fail('UNIT', path, String(x));
|
|
46
|
+
return units[x];
|
|
47
|
+
}
|
|
48
|
+
function requireTypes(actual, expected, path) {
|
|
49
|
+
if (actual.length !== expected.length || actual.some((t, i) => !eq(t, expected[i])))
|
|
50
|
+
fail('TYPE', path, `expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
51
|
+
}
|
|
52
|
+
// Output keys are part of each versioned operator contract.
|
|
53
|
+
const ops = {
|
|
54
|
+
'add@1': {
|
|
55
|
+
ports: ['value'],
|
|
56
|
+
types(t, p) {
|
|
57
|
+
if (t.length !== 2 || !t.every(Array.isArray) || !eq(t[0], t[1]))
|
|
58
|
+
fail('TYPE', p, 'addition needs same scalar dimensions');
|
|
59
|
+
return [t[0]];
|
|
60
|
+
},
|
|
61
|
+
run: ([a, b]) => [a + b]
|
|
62
|
+
},
|
|
63
|
+
'mul@1': {
|
|
64
|
+
ports: ['value'],
|
|
65
|
+
types(t, p) {
|
|
66
|
+
if (t.length !== 2 || !t.every(Array.isArray))
|
|
67
|
+
fail('TYPE', p, 'two scalar arguments');
|
|
68
|
+
return [t[0].map((x, i) => x + t[1][i])];
|
|
69
|
+
},
|
|
70
|
+
run: ([a, b]) => [a * b]
|
|
71
|
+
},
|
|
72
|
+
'div@1': {
|
|
73
|
+
ports: ['value'],
|
|
74
|
+
types(t, p) {
|
|
75
|
+
if (t.length !== 2 || !t.every(Array.isArray))
|
|
76
|
+
fail('TYPE', p, 'two scalar arguments');
|
|
77
|
+
return [t[0].map((x, i) => x - t[1][i])];
|
|
78
|
+
},
|
|
79
|
+
run: ([a, b], p) => {
|
|
80
|
+
if (b === 0)
|
|
81
|
+
fail('DOMAIN', p, 'zero denominator');
|
|
82
|
+
return [a / b];
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
'sin@1': {
|
|
86
|
+
ports: ['value'],
|
|
87
|
+
types(t, p) {
|
|
88
|
+
requireTypes(t, [angle], p);
|
|
89
|
+
return [ratio];
|
|
90
|
+
},
|
|
91
|
+
run: ([a]) => [Math.sin(a)]
|
|
92
|
+
},
|
|
93
|
+
'between@1': {
|
|
94
|
+
ports: ['length', 'center'],
|
|
95
|
+
types(t, p) {
|
|
96
|
+
requireTypes(t, [length, length, length, length], p);
|
|
97
|
+
return [length, length];
|
|
98
|
+
},
|
|
99
|
+
run: ([lo, hi, a, b], p) => {
|
|
100
|
+
if (a < 0 || b < 0)
|
|
101
|
+
fail('DOMAIN', p, 'negative margin');
|
|
102
|
+
const size = hi - lo - a - b;
|
|
103
|
+
if (!(size > 0))
|
|
104
|
+
fail('NO_SPACE', p, `available=${hi - lo}; margins=${a + b}; deficit=${Math.max(0, -size)}; positive length required`);
|
|
105
|
+
return [size, (lo + hi + a - b) / 2];
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
'box@1': {
|
|
109
|
+
ports: ['xMin', 'xMax', 'yMin', 'yMax', 'zMin', 'zMax'],
|
|
110
|
+
types(t, p) {
|
|
111
|
+
requireTypes(t, [length, length, length], p);
|
|
112
|
+
return Array(6).fill(length);
|
|
113
|
+
},
|
|
114
|
+
run: ([w, h, d], p) => {
|
|
115
|
+
if ([w, h, d].some(x => !(x > 0)))
|
|
116
|
+
fail('GEOMETRY_DOMAIN', p, 'positive width,height,depth required');
|
|
117
|
+
return [-w / 2, w / 2, -h / 2, h / 2, -d / 2, d / 2];
|
|
118
|
+
},
|
|
119
|
+
geometry: ([width, height, depth]) => ({ provider: 'box@1', dimensions: { width, height, depth } })
|
|
120
|
+
},
|
|
121
|
+
'cylinder@1': {
|
|
122
|
+
ports: ['start', 'end', 'radialMin', 'radialMax'],
|
|
123
|
+
types(t, p) {
|
|
124
|
+
requireTypes(t, [length, length], p);
|
|
125
|
+
return Array(4).fill(length);
|
|
126
|
+
},
|
|
127
|
+
run: ([r, l], p) => {
|
|
128
|
+
if (!(r > 0 && l > 0))
|
|
129
|
+
fail('GEOMETRY_DOMAIN', p, 'positive radius,length required');
|
|
130
|
+
return [-l / 2, l / 2, -r, r];
|
|
131
|
+
},
|
|
132
|
+
geometry: ([radius, length]) => ({ provider: 'cylinder@1', axis: 'Y', dimensions: { radius, length } })
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
const transformType = (from, to) => ({ kind: 'transform', from, to });
|
|
136
|
+
const identity = () => [
|
|
137
|
+
[1, 0, 0],
|
|
138
|
+
[0, 1, 0],
|
|
139
|
+
[0, 0, 1]
|
|
140
|
+
];
|
|
141
|
+
const transpose = (a) => a[0].map((_, i) => a.map(r => r[i]));
|
|
142
|
+
const dot = (a, b) => a.reduce((s, x, i) => s + x * b[i], 0);
|
|
143
|
+
const mv = (a, v) => a.map(r => dot(r, v));
|
|
144
|
+
const mm = (a, b) => a.map(r => transpose(b).map(c => dot(r, c)));
|
|
145
|
+
const plus = (a, b) => a.map((x, i) => x + b[i]);
|
|
146
|
+
const compose = (a, b) => ({ r: mm(a.r, b.r), t: plus(mv(a.r, b.t), a.t) });
|
|
147
|
+
const inverse = (a) => {
|
|
148
|
+
const r = transpose(a.r);
|
|
149
|
+
return {
|
|
150
|
+
r,
|
|
151
|
+
t: mv(r, a.t.map((x) => -x))
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
function config(n, keys) {
|
|
155
|
+
fields(n.params, keys, [], n.id);
|
|
156
|
+
for (const k of keys)
|
|
157
|
+
id(n.params[k], n.id);
|
|
158
|
+
}
|
|
159
|
+
function transforms(t, p, count) {
|
|
160
|
+
if (t.length !== count || t.some(x => x.kind !== 'transform'))
|
|
161
|
+
fail('TYPE', p, 'rigid transforms required');
|
|
162
|
+
}
|
|
163
|
+
Object.assign(ops, {
|
|
164
|
+
'rounded-box@1': {
|
|
165
|
+
ports: ['shape'],
|
|
166
|
+
params: ['frame'],
|
|
167
|
+
types(t, p, n) {
|
|
168
|
+
requireTypes(t, Array(4).fill(length), p);
|
|
169
|
+
return [{ kind: 'shape', provider: 'roundedBox', frame: n.params.frame }];
|
|
170
|
+
},
|
|
171
|
+
run: ([width, height, depth, round], p) => {
|
|
172
|
+
if (!(width > 0 && height > 0 && depth > 0 && round >= 0 && round <= Math.min(width, depth) / 2))
|
|
173
|
+
fail('GEOMETRY_DOMAIN', p, 'positive dimensions and valid XZ corner radius required');
|
|
174
|
+
return [{ provider: 'roundedBox', dimensions: { width, height, depth, round } }];
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
'fit-pitch@1': {
|
|
178
|
+
ports: ['layout'],
|
|
179
|
+
types(t, p) {
|
|
180
|
+
requireTypes(t, [length, length, length, length, length, ratio, countType], p);
|
|
181
|
+
return [{ kind: 'layout' }];
|
|
182
|
+
},
|
|
183
|
+
run: ([L, a, b, e, pitch, alpha, limit], p) => {
|
|
184
|
+
if (!(L >= 0 &&
|
|
185
|
+
a >= 0 &&
|
|
186
|
+
b >= 0 &&
|
|
187
|
+
e > 0 &&
|
|
188
|
+
pitch > 0 &&
|
|
189
|
+
alpha >= 0 &&
|
|
190
|
+
alpha <= 1 &&
|
|
191
|
+
Number.isSafeInteger(limit) &&
|
|
192
|
+
limit >= 0 &&
|
|
193
|
+
limit <= 10000))
|
|
194
|
+
fail('LAYOUT_DOMAIN', p, 'invalid length, margins, extent, pitch, alignment or limit');
|
|
195
|
+
const usable = L - a - b, count = usable < e ? 0 : 1 + Math.floor((usable - e) / pitch);
|
|
196
|
+
if (count > limit)
|
|
197
|
+
fail('RESOURCE_LIMIT', p, `count=${count}, limit=${limit}`);
|
|
198
|
+
const slack = count ? usable - e - (count - 1) * pitch : 0;
|
|
199
|
+
return [{ centers: Array.from({ length: count }, (_, i) => a + e / 2 + alpha * slack + i * pitch), count }];
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
'fixed-count@1': {
|
|
203
|
+
ports: ['layout'],
|
|
204
|
+
types(t, p) {
|
|
205
|
+
requireTypes(t, [countType, length, length, length, length, ratio, countType], p);
|
|
206
|
+
return [{ kind: 'layout' }];
|
|
207
|
+
},
|
|
208
|
+
run: ([count, L, a, b, e, alpha, limit], p) => {
|
|
209
|
+
if (!(Number.isSafeInteger(count) &&
|
|
210
|
+
count >= 0 &&
|
|
211
|
+
Number.isSafeInteger(limit) &&
|
|
212
|
+
limit >= 0 &&
|
|
213
|
+
limit <= 10000 &&
|
|
214
|
+
L >= 0 &&
|
|
215
|
+
a >= 0 &&
|
|
216
|
+
b >= 0 &&
|
|
217
|
+
e > 0 &&
|
|
218
|
+
alpha >= 0 &&
|
|
219
|
+
alpha <= 1))
|
|
220
|
+
fail('LAYOUT_DOMAIN', p, 'invalid fixed count layout');
|
|
221
|
+
if (count > limit)
|
|
222
|
+
fail('RESOURCE_LIMIT', p, 'fixed count exceeds limit');
|
|
223
|
+
if (!count)
|
|
224
|
+
return [{ centers: [], count: 0 }];
|
|
225
|
+
const room = L - a - b - e;
|
|
226
|
+
if (room < 0 || (count > 1 && room / (count - 1) < e))
|
|
227
|
+
fail('NO_SPACE', p, 'fixed count non-overlapping items do not fit');
|
|
228
|
+
return [
|
|
229
|
+
{
|
|
230
|
+
centers: count === 1
|
|
231
|
+
? [a + e / 2 + alpha * room]
|
|
232
|
+
: Array.from({ length: count }, (_, i) => a + e / 2 + (i * room) / (count - 1)),
|
|
233
|
+
count
|
|
234
|
+
}
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
'assembly@1': {
|
|
239
|
+
ports: ['assembly'],
|
|
240
|
+
types(t, p) {
|
|
241
|
+
if (!t.length || t.some(x => x.kind !== 'placed' || x.frame !== t[0].frame))
|
|
242
|
+
fail('FRAME_TYPE', p, 'assembly members need one common frame');
|
|
243
|
+
return [{ kind: 'assembly', frame: t[0].frame }];
|
|
244
|
+
},
|
|
245
|
+
run: (a, p, n) => [{ members: a.map((placed, i) => ({ key: n.args[i], ...placed })) }]
|
|
246
|
+
},
|
|
247
|
+
'repeat@1': {
|
|
248
|
+
ports: ['collection'],
|
|
249
|
+
params: ['axis'],
|
|
250
|
+
types(t, p, n) {
|
|
251
|
+
if (!['x', 'y', 'z'].includes(n.params.axis))
|
|
252
|
+
fail('AXIS', p, n.params.axis);
|
|
253
|
+
if (t.length !== 3 ||
|
|
254
|
+
t[0].kind !== 'layout' ||
|
|
255
|
+
t[1].kind !== 'assembly' ||
|
|
256
|
+
t[2].kind !== 'transform' ||
|
|
257
|
+
t[2].from !== t[1].frame)
|
|
258
|
+
fail('FRAME_TYPE', p, 'layout, assembly and compatible placement required');
|
|
259
|
+
return [{ kind: 'collection', frame: t[2].to }];
|
|
260
|
+
},
|
|
261
|
+
run: ([layout, assembly, pose], p, n) => {
|
|
262
|
+
if (layout.count * assembly.members.length > 10000)
|
|
263
|
+
fail('RESOURCE_LIMIT', p, 'expanded geometry count > 10000');
|
|
264
|
+
const axis = ['x', 'y', 'z'].indexOf(n.params.axis), items = [];
|
|
265
|
+
for (let i = 0; i < layout.count; i++)
|
|
266
|
+
for (const member of assembly.members) {
|
|
267
|
+
const t = [0, 0, 0];
|
|
268
|
+
t[axis] = layout.centers[i];
|
|
269
|
+
items.push({
|
|
270
|
+
id: JSON.stringify([n.id, i, member.key]),
|
|
271
|
+
key: member.key,
|
|
272
|
+
index: i,
|
|
273
|
+
shape: member.shape,
|
|
274
|
+
pose: compose(compose(pose, { r: identity(), t }), member.pose)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return [{ items }];
|
|
278
|
+
},
|
|
279
|
+
geometry: (_, out) => out[0].items
|
|
280
|
+
},
|
|
281
|
+
'select-item@1': {
|
|
282
|
+
ports: ['placed'],
|
|
283
|
+
params: ['member'],
|
|
284
|
+
types(t, p) {
|
|
285
|
+
if (t.length !== 2 || t[0].kind !== 'collection' || !eq(t[1], countType))
|
|
286
|
+
fail('TYPE', p, 'collection and count index required');
|
|
287
|
+
return [{ kind: 'placed', frame: t[0].frame }];
|
|
288
|
+
},
|
|
289
|
+
run: ([collection, index], p, n) => {
|
|
290
|
+
const item = collection.items.find((x) => x.index === index && x.key === n.params.member);
|
|
291
|
+
if (!item)
|
|
292
|
+
fail('TARGET_ABSENT', p, `${n.params.member}[${index}]`);
|
|
293
|
+
return [{ shape: item.shape, pose: item.pose }];
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
'portal-profile@1': {
|
|
297
|
+
ports: ['shape'],
|
|
298
|
+
params: ['frame'],
|
|
299
|
+
types(t, p, n) {
|
|
300
|
+
requireTypes(t, Array(8).fill(length), p);
|
|
301
|
+
return [{ kind: 'shape', provider: 'portal', frame: n.params.frame }];
|
|
302
|
+
},
|
|
303
|
+
run: ([left, right, bottom, top, openingLeft, openingRight, openingTop, depth], p) => {
|
|
304
|
+
if (!(left < openingLeft &&
|
|
305
|
+
openingLeft < openingRight &&
|
|
306
|
+
openingRight < right &&
|
|
307
|
+
bottom < openingTop &&
|
|
308
|
+
openingTop < top &&
|
|
309
|
+
depth > 0))
|
|
310
|
+
fail('GEOMETRY_DOMAIN', p, 'ordered outer/opening boundaries and positive depth required');
|
|
311
|
+
const path = [
|
|
312
|
+
[left, bottom],
|
|
313
|
+
[openingLeft, bottom],
|
|
314
|
+
[openingLeft, openingTop],
|
|
315
|
+
[openingRight, openingTop],
|
|
316
|
+
[openingRight, bottom],
|
|
317
|
+
[right, bottom],
|
|
318
|
+
[right, top],
|
|
319
|
+
[left, top]
|
|
320
|
+
];
|
|
321
|
+
return [
|
|
322
|
+
{
|
|
323
|
+
provider: 'portal',
|
|
324
|
+
dimensions: { left, right, bottom, top, openingLeft, openingRight, openingTop, depth },
|
|
325
|
+
profile: path.map(([x, y], i) => ({
|
|
326
|
+
id: [
|
|
327
|
+
'outerLeftBottom',
|
|
328
|
+
'innerLeftBottom',
|
|
329
|
+
'innerLeftTop',
|
|
330
|
+
'innerRightTop',
|
|
331
|
+
'innerRightBottom',
|
|
332
|
+
'outerRightBottom',
|
|
333
|
+
'outerRightTop',
|
|
334
|
+
'outerLeftTop'
|
|
335
|
+
][i],
|
|
336
|
+
x,
|
|
337
|
+
y
|
|
338
|
+
}))
|
|
339
|
+
}
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
'rigid@1': {
|
|
344
|
+
ports: ['pose'],
|
|
345
|
+
params: ['from', 'to'],
|
|
346
|
+
types(t, p, n) {
|
|
347
|
+
requireTypes(t, [length, length, length, angle, angle, angle], p);
|
|
348
|
+
return [transformType(n.params.from, n.params.to)];
|
|
349
|
+
},
|
|
350
|
+
run: ([x, y, z, rx, ry, rz]) => {
|
|
351
|
+
const cx = Math.cos(rx), sx = Math.sin(rx), cy = Math.cos(ry), sy = Math.sin(ry), cz = Math.cos(rz), sz = Math.sin(rz);
|
|
352
|
+
return [
|
|
353
|
+
{
|
|
354
|
+
r: mm(mm([
|
|
355
|
+
[1, 0, 0],
|
|
356
|
+
[0, cx, -sx],
|
|
357
|
+
[0, sx, cx]
|
|
358
|
+
], [
|
|
359
|
+
[cy, 0, sy],
|
|
360
|
+
[0, 1, 0],
|
|
361
|
+
[-sy, 0, cy]
|
|
362
|
+
]), [
|
|
363
|
+
[cz, -sz, 0],
|
|
364
|
+
[sz, cz, 0],
|
|
365
|
+
[0, 0, 1]
|
|
366
|
+
]),
|
|
367
|
+
t: [x, y, z]
|
|
368
|
+
}
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
'compose@1': {
|
|
373
|
+
ports: ['pose'],
|
|
374
|
+
types(t, p) {
|
|
375
|
+
transforms(t, p, 2);
|
|
376
|
+
if (t[1].to !== t[0].from)
|
|
377
|
+
fail('FRAME_TYPE', p, 'composition intermediate frame mismatch');
|
|
378
|
+
return [transformType(t[1].from, t[0].to)];
|
|
379
|
+
},
|
|
380
|
+
run: ([a, b]) => [compose(a, b)]
|
|
381
|
+
},
|
|
382
|
+
'attach@1': {
|
|
383
|
+
ports: ['pose'],
|
|
384
|
+
types(t, p) {
|
|
385
|
+
transforms(t, p, 3);
|
|
386
|
+
const [a, o, b] = t;
|
|
387
|
+
if (a.from !== o.to || o.from !== b.from)
|
|
388
|
+
fail('FRAME_TYPE', p, 'A O inverse(B) frame mismatch');
|
|
389
|
+
return [transformType(b.to, a.to)];
|
|
390
|
+
},
|
|
391
|
+
run: ([a, o, b]) => [compose(compose(a, o), inverse(b))]
|
|
392
|
+
},
|
|
393
|
+
'point@1': {
|
|
394
|
+
ports: ['point'],
|
|
395
|
+
params: ['frame'],
|
|
396
|
+
types(t, p, n) {
|
|
397
|
+
requireTypes(t, [length, length, length], p);
|
|
398
|
+
return [{ kind: 'point', frame: n.params.frame }];
|
|
399
|
+
},
|
|
400
|
+
run: a => [a]
|
|
401
|
+
},
|
|
402
|
+
'map-point@1': {
|
|
403
|
+
ports: ['point'],
|
|
404
|
+
types(t, p) {
|
|
405
|
+
if (t.length !== 2 || t[0].kind !== 'transform' || t[1].kind !== 'point' || t[0].from !== t[1].frame)
|
|
406
|
+
fail('FRAME_TYPE', p, 'point frame must match transform source');
|
|
407
|
+
return [{ kind: 'point', frame: t[0].to }];
|
|
408
|
+
},
|
|
409
|
+
run: ([pose, pt]) => [plus(mv(pose.r, pt), pose.t)]
|
|
410
|
+
},
|
|
411
|
+
'distance@1': {
|
|
412
|
+
ports: ['length'],
|
|
413
|
+
types(t, p) {
|
|
414
|
+
if (t.length !== 2 || t[0].kind !== 'point' || !eq(t[0], t[1]))
|
|
415
|
+
fail('FRAME_TYPE', p, 'distance requires points in same frame');
|
|
416
|
+
return [length];
|
|
417
|
+
},
|
|
418
|
+
run: ([a, b]) => [Math.hypot(...a.map((x, i) => x - b[i]))]
|
|
419
|
+
},
|
|
420
|
+
'box-shape@1': {
|
|
421
|
+
ports: ['shape'],
|
|
422
|
+
params: ['frame'],
|
|
423
|
+
types(t, p, n) {
|
|
424
|
+
requireTypes(t, [length, length, length], p);
|
|
425
|
+
return [{ kind: 'shape', provider: 'box', frame: n.params.frame }];
|
|
426
|
+
},
|
|
427
|
+
run: ([width, height, depth], p) => {
|
|
428
|
+
if ([width, height, depth].some(x => !(x > 0)))
|
|
429
|
+
fail('GEOMETRY_DOMAIN', p, 'positive box dimensions');
|
|
430
|
+
return [{ provider: 'box', dimensions: { width, height, depth } }];
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
'cylinder-shape@1': {
|
|
434
|
+
ports: ['shape'],
|
|
435
|
+
params: ['frame'],
|
|
436
|
+
types(t, p, n) {
|
|
437
|
+
requireTypes(t, [length, length], p);
|
|
438
|
+
return [{ kind: 'shape', provider: 'cylinder', frame: n.params.frame }];
|
|
439
|
+
},
|
|
440
|
+
run: ([radius, length], p) => {
|
|
441
|
+
if (!(radius > 0 && length > 0))
|
|
442
|
+
fail('GEOMETRY_DOMAIN', p, 'positive cylinder dimensions');
|
|
443
|
+
return [{ provider: 'cylinder', dimensions: { radius, length } }];
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
'feature@1': {
|
|
447
|
+
ports: ['pose'],
|
|
448
|
+
params: ['frame', 'name'],
|
|
449
|
+
types(t, p, n) {
|
|
450
|
+
if (t.length !== 1 || t[0].kind !== 'shape')
|
|
451
|
+
fail('TYPE', p, 'shape required');
|
|
452
|
+
const names = t[0].provider === 'portal'
|
|
453
|
+
? ['top', 'bottomLeft', 'bottomRight', 'openingTop', 'frontTop']
|
|
454
|
+
: ['box', 'roundedBox'].includes(t[0].provider)
|
|
455
|
+
? ['left', 'right', 'bottom', 'top', 'back', 'front']
|
|
456
|
+
: ['start', 'end', 'radialBottom'];
|
|
457
|
+
if (!names.includes(n.params.name))
|
|
458
|
+
fail('UNKNOWN_FEATURE', p, n.params.name);
|
|
459
|
+
// Provider-aligned feature axes, not outward-normal mating axes.
|
|
460
|
+
return [transformType(n.params.frame, t[0].frame)];
|
|
461
|
+
},
|
|
462
|
+
run: ([s], p, n) => {
|
|
463
|
+
const d = s.dimensions, points = s.provider === 'portal'
|
|
464
|
+
? {
|
|
465
|
+
top: [(d.left + d.right) / 2, d.top, 0],
|
|
466
|
+
bottomLeft: [(d.left + d.openingLeft) / 2, d.bottom, 0],
|
|
467
|
+
bottomRight: [(d.right + d.openingRight) / 2, d.bottom, 0],
|
|
468
|
+
openingTop: [(d.openingLeft + d.openingRight) / 2, d.openingTop, 0],
|
|
469
|
+
frontTop: [(d.left + d.right) / 2, d.top, d.depth / 2]
|
|
470
|
+
}
|
|
471
|
+
: ['box', 'roundedBox'].includes(s.provider)
|
|
472
|
+
? {
|
|
473
|
+
left: [-d.width / 2, 0, 0],
|
|
474
|
+
right: [d.width / 2, 0, 0],
|
|
475
|
+
bottom: [0, -d.height / 2, 0],
|
|
476
|
+
top: [0, d.height / 2, 0],
|
|
477
|
+
back: [0, 0, -d.depth / 2],
|
|
478
|
+
front: [0, 0, d.depth / 2]
|
|
479
|
+
}
|
|
480
|
+
: { start: [0, -d.length / 2, 0], end: [0, d.length / 2, 0], radialBottom: [0, 0, -d.radius] };
|
|
481
|
+
return [{ r: identity(), t: points[n.params.name] }];
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
'place@1': {
|
|
485
|
+
ports: ['placed'],
|
|
486
|
+
types(t, p) {
|
|
487
|
+
if (t.length !== 2 || t[0].kind !== 'shape' || t[1].kind !== 'transform' || t[0].frame !== t[1].from)
|
|
488
|
+
fail('FRAME_TYPE', p, 'shape local frame mismatch');
|
|
489
|
+
return [{ kind: 'placed', frame: t[1].to }];
|
|
490
|
+
},
|
|
491
|
+
run: ([shape, pose]) => [{ shape, pose }],
|
|
492
|
+
geometry: ([shape, pose]) => ({ shape, pose })
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
// Template members are values, not separately rendered placements.
|
|
496
|
+
ops['member@1'] = { ...ops['place@1'], geometry: undefined };
|
|
497
|
+
function checkedValue(value, type, path) {
|
|
498
|
+
if (Array.isArray(type))
|
|
499
|
+
return num(value, path);
|
|
500
|
+
if (type.kind === 'count') {
|
|
501
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
502
|
+
fail('COUNT', path, 'nonnegative safe integer required');
|
|
503
|
+
return value;
|
|
504
|
+
}
|
|
505
|
+
const check = (v) => {
|
|
506
|
+
if (typeof v === 'number')
|
|
507
|
+
num(v, path);
|
|
508
|
+
else if (Array.isArray(v))
|
|
509
|
+
v.forEach(check);
|
|
510
|
+
else if (v && typeof v === 'object')
|
|
511
|
+
Object.values(v).forEach(check);
|
|
512
|
+
};
|
|
513
|
+
check(value);
|
|
514
|
+
return value;
|
|
515
|
+
}
|
|
516
|
+
export function compileV3Graph(source) {
|
|
517
|
+
return compileInternal(source);
|
|
518
|
+
}
|
|
519
|
+
function compileInternal(source) {
|
|
520
|
+
fields(source, ['version', 'inputs', 'constants', 'nodes'], [], 'model');
|
|
521
|
+
if (!['r3-scalar-1', 'r3-spatial-1'].includes(source.version))
|
|
522
|
+
fail('VERSION', 'model', source.version);
|
|
523
|
+
for (const k of ['inputs', 'constants', 'nodes'])
|
|
524
|
+
if (!Array.isArray(source[k]))
|
|
525
|
+
fail('SCHEMA', k, 'array required');
|
|
526
|
+
// Snapshot protects a compiled plan from later authoring mutations.
|
|
527
|
+
const m = structuredClone(source), writers = new Map(), types = new Map(), stateful = new Set(), nodes = new Map(), order = [], active = [];
|
|
528
|
+
const assign = (name, owner) => {
|
|
529
|
+
id(name, owner);
|
|
530
|
+
if (writers.has(name))
|
|
531
|
+
fail('DUPLICATE_WRITER', name, `${writers.get(name)} and ${owner}`);
|
|
532
|
+
writers.set(name, owner);
|
|
533
|
+
};
|
|
534
|
+
for (const p of m.inputs) {
|
|
535
|
+
fields(p, ['id', 'unit', 'min', 'max'], ['role'], 'input');
|
|
536
|
+
assign(p.id, 'input');
|
|
537
|
+
types.set(p.id, unit(p.unit, p.id)[0]);
|
|
538
|
+
if (p.role && !['design', 'state'].includes(p.role))
|
|
539
|
+
fail('SCHEMA', p.id, 'unknown input role');
|
|
540
|
+
if (p.role === 'state')
|
|
541
|
+
stateful.add(p.id);
|
|
542
|
+
if (num(p.min, p.id) > num(p.max, p.id))
|
|
543
|
+
fail('RANGE', p.id, 'min > max');
|
|
544
|
+
if (p.unit === 'count') {
|
|
545
|
+
checkedValue(p.min, countType, p.id);
|
|
546
|
+
checkedValue(p.max, countType, p.id);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
for (const p of m.constants) {
|
|
550
|
+
fields(p, ['id', 'unit', 'value'], [], 'constant');
|
|
551
|
+
assign(p.id, 'constant');
|
|
552
|
+
types.set(p.id, unit(p.unit, p.id)[0]);
|
|
553
|
+
checkedValue(p.value, types.get(p.id), p.id);
|
|
554
|
+
}
|
|
555
|
+
for (const n of m.nodes) {
|
|
556
|
+
fields(n, ['id', 'op', 'args', 'outputs'], ['params'], 'node');
|
|
557
|
+
id(n.id, 'node');
|
|
558
|
+
if (nodes.has(n.id))
|
|
559
|
+
fail('DUPLICATE_NODE', n.id, 'node id');
|
|
560
|
+
if (!Object.hasOwn(ops, n.op))
|
|
561
|
+
fail('OPERATOR', n.id, n.op);
|
|
562
|
+
if (ops[n.op].params)
|
|
563
|
+
config(n, ops[n.op].params);
|
|
564
|
+
else if (Object.hasOwn(n, 'params'))
|
|
565
|
+
fail('SCHEMA', n.id, 'unexpected params');
|
|
566
|
+
if (!Array.isArray(n.args))
|
|
567
|
+
fail('SCHEMA', n.id, 'args array required');
|
|
568
|
+
n.args.forEach(a => id(a, n.id));
|
|
569
|
+
fields(n.outputs, ops[n.op].ports, [], n.id);
|
|
570
|
+
for (const output of Object.values(n.outputs))
|
|
571
|
+
assign(output, n.id);
|
|
572
|
+
nodes.set(n.id, n);
|
|
573
|
+
}
|
|
574
|
+
const visited = new Set();
|
|
575
|
+
function visit(name) {
|
|
576
|
+
if (visited.has(name))
|
|
577
|
+
return;
|
|
578
|
+
if (active.includes(name))
|
|
579
|
+
fail('CYCLE', name, [...active, name].join(' -> '));
|
|
580
|
+
active.push(name);
|
|
581
|
+
const n = nodes.get(name), op = ops[n.op];
|
|
582
|
+
const inputTypes = n.args.map(ref => {
|
|
583
|
+
if (!writers.has(ref))
|
|
584
|
+
fail('UNKNOWN_REFERENCE', n.id, ref);
|
|
585
|
+
if (!types.has(ref))
|
|
586
|
+
visit(writers.get(ref));
|
|
587
|
+
return types.get(ref);
|
|
588
|
+
});
|
|
589
|
+
const resultTypes = op.types(inputTypes, n.id, n);
|
|
590
|
+
const dependsOnState = n.args.some(ref => stateful.has(ref));
|
|
591
|
+
if (dependsOnState && ['fit-pitch@1', 'fixed-count@1'].includes(n.op))
|
|
592
|
+
fail('TOPOLOGY_STATE', n.id, 'layout must depend on design inputs only');
|
|
593
|
+
op.ports.forEach((p, i) => types.set(n.outputs[p], resultTypes[i]));
|
|
594
|
+
if (dependsOnState)
|
|
595
|
+
Object.values(n.outputs).forEach(ref => stateful.add(ref));
|
|
596
|
+
active.pop();
|
|
597
|
+
visited.add(name);
|
|
598
|
+
order.push(n);
|
|
599
|
+
}
|
|
600
|
+
for (const name of nodes.keys())
|
|
601
|
+
visit(name);
|
|
602
|
+
return {
|
|
603
|
+
outputTypes: Object.fromEntries([...types].map(([k, v]) => [k, structuredClone(v)])),
|
|
604
|
+
order: order.map(n => n.id),
|
|
605
|
+
evaluate(input) {
|
|
606
|
+
fields(input, m.inputs.map(p => p.id), [], 'input values');
|
|
607
|
+
const values = new Map(), geometry = [];
|
|
608
|
+
for (const p of m.inputs) {
|
|
609
|
+
const raw = num(input[p.id], p.id);
|
|
610
|
+
if (raw < p.min || raw > p.max)
|
|
611
|
+
fail('INPUT_RANGE', p.id, `${raw} outside [${p.min},${p.max}]`);
|
|
612
|
+
values.set(p.id, checkedValue(raw * unit(p.unit, p.id)[1], types.get(p.id), p.id));
|
|
613
|
+
}
|
|
614
|
+
for (const p of m.constants)
|
|
615
|
+
values.set(p.id, num(p.value * unit(p.unit, p.id)[1], p.id));
|
|
616
|
+
for (const n of order) {
|
|
617
|
+
const op = ops[n.op], args = n.args.map(k => values.get(k)), out = op.run(args, n.id, n);
|
|
618
|
+
op.ports.forEach((p, i) => values.set(n.outputs[p], checkedValue(out[i], types.get(n.outputs[p]), n.id)));
|
|
619
|
+
if (op.geometry) {
|
|
620
|
+
const g = op.geometry(args, out);
|
|
621
|
+
if (Array.isArray(g))
|
|
622
|
+
geometry.push(...g);
|
|
623
|
+
else
|
|
624
|
+
geometry.push({ id: n.id, ...g });
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return { values: Object.fromEntries(values), geometry };
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
//# sourceMappingURL=v3-graph.js.map
|