@hatiolab/figure-model 0.1.30 → 0.1.32

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 (143) hide show
  1. package/README.md +2 -0
  2. package/dist/index.d.ts +12 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +11 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/release-contract.d.ts +2 -2
  7. package/dist/release-contract.d.ts.map +1 -1
  8. package/dist/release-contract.js +45 -3
  9. package/dist/release-contract.js.map +1 -1
  10. package/dist/types.d.ts +15 -4
  11. package/dist/types.d.ts.map +1 -1
  12. package/dist/types.js +15 -4
  13. package/dist/types.js.map +1 -1
  14. package/dist/v3-asset-types.d.ts +116 -0
  15. package/dist/v3-asset-types.d.ts.map +1 -0
  16. package/dist/v3-asset-types.js +2 -0
  17. package/dist/v3-asset-types.js.map +1 -0
  18. package/dist/v3-asset.d.ts +8 -0
  19. package/dist/v3-asset.d.ts.map +1 -0
  20. package/dist/v3-asset.js +135 -0
  21. package/dist/v3-asset.js.map +1 -0
  22. package/dist/v3-authoring-session.d.ts +119 -0
  23. package/dist/v3-authoring-session.d.ts.map +1 -0
  24. package/dist/v3-authoring-session.js +132 -0
  25. package/dist/v3-authoring-session.js.map +1 -0
  26. package/dist/v3-capabilities.d.ts +3 -0
  27. package/dist/v3-capabilities.d.ts.map +1 -0
  28. package/dist/v3-capabilities.js +221 -0
  29. package/dist/v3-capabilities.js.map +1 -0
  30. package/dist/v3-contract.d.ts +7 -0
  31. package/dist/v3-contract.d.ts.map +1 -0
  32. package/dist/v3-contract.js +12 -0
  33. package/dist/v3-contract.js.map +1 -0
  34. package/dist/v3-create.d.ts +61 -0
  35. package/dist/v3-create.d.ts.map +1 -0
  36. package/dist/v3-create.js +151 -0
  37. package/dist/v3-create.js.map +1 -0
  38. package/dist/v3-document.d.ts +5 -0
  39. package/dist/v3-document.d.ts.map +1 -0
  40. package/dist/v3-document.js +31 -0
  41. package/dist/v3-document.js.map +1 -0
  42. package/dist/v3-editor-workspace.d.ts +367 -0
  43. package/dist/v3-editor-workspace.d.ts.map +1 -0
  44. package/dist/v3-editor-workspace.js +179 -0
  45. package/dist/v3-editor-workspace.js.map +1 -0
  46. package/dist/v3-graph-types.d.ts +91 -0
  47. package/dist/v3-graph-types.d.ts.map +1 -0
  48. package/dist/v3-graph-types.js +2 -0
  49. package/dist/v3-graph-types.js.map +1 -0
  50. package/dist/v3-graph.d.ts +5 -0
  51. package/dist/v3-graph.d.ts.map +1 -0
  52. package/dist/v3-graph.js +631 -0
  53. package/dist/v3-graph.js.map +1 -0
  54. package/dist/v3-json.d.ts +13 -0
  55. package/dist/v3-json.d.ts.map +1 -0
  56. package/dist/v3-json.js +94 -0
  57. package/dist/v3-json.js.map +1 -0
  58. package/dist/v3-part-edit.d.ts +12 -0
  59. package/dist/v3-part-edit.d.ts.map +1 -0
  60. package/dist/v3-part-edit.js +61 -0
  61. package/dist/v3-part-edit.js.map +1 -0
  62. package/docs/assembly-constraints-v3.md +141 -0
  63. package/docs/prototypes/README.md +73 -0
  64. package/docs/prototypes/render-dryroom.py +60 -0
  65. package/docs/prototypes/render-full-dryroom.mjs +86 -0
  66. package/docs/prototypes/shapeassembly-probe-results.json +325 -0
  67. package/docs/prototypes/shapeassembly-probe.py +84 -0
  68. package/docs/prototypes/v3-asset-samples.mjs +15 -0
  69. package/docs/prototypes/v3-asset-store.mjs +52 -0
  70. package/docs/prototypes/v3-asset-store.test.mjs +59 -0
  71. package/docs/prototypes/v3-asset.mjs +1 -0
  72. package/docs/prototypes/v3-asset.schema.json +44 -0
  73. package/docs/prototypes/v3-asset.test.mjs +68 -0
  74. package/docs/prototypes/v3-authoring-session.mjs +1 -0
  75. package/docs/prototypes/v3-authoring-session.test.mjs +65 -0
  76. package/docs/prototypes/v3-capability-plan.mjs +1 -0
  77. package/docs/prototypes/v3-capability-plan.test.mjs +56 -0
  78. package/docs/prototypes/v3-capability-samples.mjs +23 -0
  79. package/docs/prototypes/v3-document.mjs +2 -0
  80. package/docs/prototypes/v3-document.schema.json +66 -0
  81. package/docs/prototypes/v3-document.test.mjs +55 -0
  82. package/docs/prototypes/v3-dryroom-mesh.mjs +60 -0
  83. package/docs/prototypes/v3-dryroom-mesh.test.mjs +30 -0
  84. package/docs/prototypes/v3-editor-browser.mjs +97 -0
  85. package/docs/prototypes/v3-editor-workspace.mjs +1 -0
  86. package/docs/prototypes/v3-editor-workspace.test.mjs +35 -0
  87. package/docs/prototypes/v3-feature-pattern.test.mjs +131 -0
  88. package/docs/prototypes/v3-full-conveyor.mjs +60 -0
  89. package/docs/prototypes/v3-full-conveyor.test.mjs +61 -0
  90. package/docs/prototypes/v3-full-dryroom.mjs +67 -0
  91. package/docs/prototypes/v3-full-dryroom.test.mjs +54 -0
  92. package/docs/prototypes/v3-full-forklift.mjs +40 -0
  93. package/docs/prototypes/v3-full-forklift.test.mjs +37 -0
  94. package/docs/prototypes/v3-full-robot.mjs +35 -0
  95. package/docs/prototypes/v3-full-robot.test.mjs +68 -0
  96. package/docs/prototypes/v3-kernel.mjs +147 -0
  97. package/docs/prototypes/v3-kernel.test.mjs +144 -0
  98. package/docs/prototypes/v3-layout.test.mjs +281 -0
  99. package/docs/prototypes/v3-linear-comparison.py +183 -0
  100. package/docs/prototypes/v3-more-samples.test.mjs +182 -0
  101. package/docs/prototypes/v3-payload-collision.mjs +54 -0
  102. package/docs/prototypes/v3-payload-collision.test.mjs +68 -0
  103. package/docs/prototypes/v3-payload-fit.mjs +58 -0
  104. package/docs/prototypes/v3-payload-fit.test.mjs +59 -0
  105. package/docs/prototypes/v3-repeated-binding.mjs +13 -0
  106. package/docs/prototypes/v3-repeated-binding.test.mjs +46 -0
  107. package/docs/prototypes/v3-runtime-browser.mjs +76 -0
  108. package/docs/prototypes/v3-samples.test.mjs +144 -0
  109. package/docs/prototypes/v3-semantics.test.mjs +55 -0
  110. package/docs/prototypes/v3-spatial-plan.test.mjs +102 -0
  111. package/docs/prototypes/v3-typed-plan.mjs +3 -0
  112. package/docs/prototypes/v3-typed-plan.test.mjs +70 -0
  113. package/docs/prototypes/v3-typed-repeat.test.mjs +96 -0
  114. package/docs/v3-asset-persistence.md +66 -0
  115. package/docs/v3-authoring-transactions.md +57 -0
  116. package/docs/v3-capability-bindings.md +109 -0
  117. package/docs/v3-common-kernel-experiment.md +116 -0
  118. package/docs/v3-core-semantics.md +303 -0
  119. package/docs/v3-cutover.md +27 -0
  120. package/docs/v3-design.md +388 -0
  121. package/docs/v3-document-contract.md +55 -0
  122. package/docs/v3-dryroom-profile-validation.md +87 -0
  123. package/docs/v3-editor-integration.md +59 -0
  124. package/docs/v3-full-conveyor-validation.md +69 -0
  125. package/docs/v3-full-dryroom-validation.md +55 -0
  126. package/docs/v3-full-forklift-validation.md +41 -0
  127. package/docs/v3-full-robot-validation.md +64 -0
  128. package/docs/v3-layout-system.md +318 -0
  129. package/docs/v3-minimal-model.md +255 -0
  130. package/docs/v3-operator-contracts.md +148 -0
  131. package/docs/v3-payload-collision-validation.md +42 -0
  132. package/docs/v3-payload-contact-validation.md +56 -0
  133. package/docs/v3-repeated-binding-validation.md +42 -0
  134. package/docs/v3-research-review.md +160 -0
  135. package/docs/v3-runtime-extraction.md +107 -0
  136. package/docs/v3-sample-validation-expanded.md +106 -0
  137. package/docs/v3-sample-validation.md +97 -0
  138. package/docs/v3-shapeassembly-reproduction.md +99 -0
  139. package/docs/v3-spatial-contracts.md +84 -0
  140. package/docs/v3-status.md +41 -0
  141. package/docs/v3-storage-integration.md +58 -0
  142. package/docs/v3-typed-repeat-contracts.md +79 -0
  143. package/package.json +1 -1
@@ -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