pelican-engine 0.0.0-stage → 0.3.1

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 (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,348 @@
1
+ import { Box3, Ray, Triangle, Vector3 } from "three";
2
+ export class MediumError extends Error {
3
+ code;
4
+ key;
5
+ constructor(code, message, key) {
6
+ super(code + ": " + message + (key === undefined ? "" : " [" + key + "]"));
7
+ this.code = code;
8
+ this.key = key;
9
+ this.name = "MediumError";
10
+ }
11
+ }
12
+ const directions = [
13
+ [1, 0.37139, 0.69421],
14
+ [-0.52973, 1, 0.21761],
15
+ [0.19317, -0.61843, 1],
16
+ ].map((v) => new Vector3(...v).normalize());
17
+ const fail = (code, message, key) => {
18
+ throw new MediumError("TC_MEDIUM_" + code, message, key);
19
+ };
20
+ function vector(v, key) {
21
+ if (!v || v.length !== 3 || !v.every(Number.isFinite))
22
+ fail("INPUT", "Supply three finite world-space coordinates.", key);
23
+ return new Vector3(...v);
24
+ }
25
+ function tuple(v) {
26
+ return Object.freeze([v.x, v.y, v.z]);
27
+ }
28
+ function optical(m, key) {
29
+ const ior = m.ior ?? 1.5, distance = m.attenuationDistance ?? 0, color = m.attenuationColor ?? [1, 1, 1];
30
+ if (!Number.isFinite(ior) ||
31
+ ior <= 0 ||
32
+ !Number.isFinite(distance) ||
33
+ distance < 0 ||
34
+ color.length !== 3 ||
35
+ !color.every((c) => Number.isFinite(c) && c >= 0 && c <= 1))
36
+ fail("MATERIAL", "Use positive finite IOR, finite distance >= 0 and linear attenuation color in [0,1].", key);
37
+ if (distance > 0 && color.some((c) => c === 0))
38
+ fail("ZERO_ATTENUATION", "Zero attenuation at positive distance has infinite absorption; use an explicit opaque path or positive color.", key);
39
+ const sigma = color.map((c) => distance === 0 || c === 1 ? 0 : -Math.log(c) / distance);
40
+ if (!sigma.every((c) => Number.isFinite(c) && Number.isFinite(Math.fround(c))))
41
+ fail("ABSORPTION_RANGE", "Absorption must fit finite GPU float32; change attenuation color/distance.", key);
42
+ return { ior, sigma: Object.freeze(sigma) };
43
+ }
44
+ function build(indices, snapshot, id, key, ior, sigma) {
45
+ const raw = indices.map((i) => snapshot.triangles[i].vertices.map((v) => vector(v.position, key)));
46
+ const bounds = new Box3().setFromPoints(raw.flat()), extent = bounds.getSize(new Vector3());
47
+ const scale = Math.max(extent.x, extent.y, extent.z), magnitude = Math.max(...bounds.min.toArray().map(Math.abs), ...bounds.max.toArray().map(Math.abs));
48
+ const epsilon = Math.max(scale * 1e-7, magnitude * Number.EPSILON * 32);
49
+ if (!(epsilon > 0) || !Number.isFinite(epsilon) || !Number.isFinite(scale))
50
+ fail("DEGENERATE", "Provide a resolvable nonzero closed volume.", key);
51
+ // Numeric spatial buckets search adjacent cells, then verify world-space distance.
52
+ const cells = new Map(), points = [];
53
+ function weld(p) {
54
+ const c = p.clone().sub(bounds.min).divideScalar(epsilon).floor();
55
+ let found = Infinity;
56
+ for (let x = c.x - 1; x <= c.x + 1; x++)
57
+ for (let y = c.y - 1; y <= c.y + 1; y++)
58
+ for (let z = c.z - 1; z <= c.z + 1; z++)
59
+ for (const i of cells.get(x)?.get(y)?.get(z) ?? [])
60
+ if (points[i].distanceTo(p) <= epsilon)
61
+ found = Math.min(found, i);
62
+ if (found !== Infinity)
63
+ return found;
64
+ if (!cells.has(c.x))
65
+ cells.set(c.x, new Map());
66
+ const ys = cells.get(c.x);
67
+ if (!ys.has(c.y))
68
+ ys.set(c.y, new Map());
69
+ const zs = ys.get(c.y);
70
+ if (!zs.has(c.z))
71
+ zs.set(c.z, []);
72
+ const index = points.length;
73
+ zs.get(c.z).push(index);
74
+ points.push(p.clone());
75
+ return index;
76
+ }
77
+ const faces = raw.map((v) => {
78
+ const ids = v.map(weld), triangle = new Triangle(...ids.map((i) => points[i]));
79
+ const lengths = [
80
+ triangle.a.distanceTo(triangle.b),
81
+ triangle.b.distanceTo(triangle.c),
82
+ triangle.c.distanceTo(triangle.a),
83
+ ];
84
+ if (new Set(ids).size !== 3 ||
85
+ triangle.getArea() <= epsilon * Math.max(...lengths))
86
+ fail("DEGENERATE", "Remove zero-area or sub-tolerance triangles.", key);
87
+ return {
88
+ triangle,
89
+ ids,
90
+ bounds: new Box3().setFromPoints([triangle.a, triangle.b, triangle.c]),
91
+ };
92
+ });
93
+ const edges = new Map();
94
+ const incident = points.map(() => []), adjacent = faces.map(() => []);
95
+ faces.forEach((f, face) => {
96
+ for (let j = 0; j < 3; j++) {
97
+ const a = f.ids[j], b = f.ids[(j + 1) % 3], lo = Math.min(a, b), hi = Math.max(a, b);
98
+ incident[a].push(face);
99
+ if (!edges.has(lo))
100
+ edges.set(lo, new Map());
101
+ const row = edges.get(lo);
102
+ if (!row.has(hi))
103
+ row.set(hi, []);
104
+ row.get(hi).push({ face, forward: a === lo });
105
+ }
106
+ });
107
+ for (const row of edges.values())
108
+ for (const uses of row.values()) {
109
+ if (uses.length !== 2)
110
+ fail("OPEN_OR_NONMANIFOLD", "Each welded edge must have exactly two incident faces; close holes/remove duplicate faces.", key);
111
+ if (uses[0].forward === uses[1].forward)
112
+ fail("ORIENTATION", "Adjacent faces must traverse each shared edge in opposite directions.", key);
113
+ adjacent[uses[0].face].push(uses[1].face);
114
+ adjacent[uses[1].face].push(uses[0].face);
115
+ }
116
+ function connected(allowed, start) {
117
+ const seen = new Set([start]), queue = [start];
118
+ for (let i = 0; i < queue.length; i++)
119
+ for (const n of adjacent[queue[i]])
120
+ if (allowed.has(n) && !seen.has(n)) {
121
+ seen.add(n);
122
+ queue.push(n);
123
+ }
124
+ return seen.size === allowed.size;
125
+ }
126
+ for (const fs of incident)
127
+ if (!connected(new Set(fs), fs[0]))
128
+ fail("VERTEX_NONMANIFOLD", "Split pinched vertices into separate objects with valid closed shells.", key);
129
+ if (!connected(new Set(faces.map((_, i) => i)), 0))
130
+ fail("MULTIPLE_SHELLS", "Use one connected closed shell per object/instance; split disconnected shells and cavities into separate objects.", key);
131
+ // Translation-relative signed volume accepts either global winding, including mirrors.
132
+ let signed = 0, area = 0;
133
+ for (const { triangle: t } of faces) {
134
+ signed +=
135
+ t.a
136
+ .clone()
137
+ .sub(points[0])
138
+ .dot(t.b.clone().sub(points[0]).cross(t.c.clone().sub(points[0]))) / 6;
139
+ area += t.getArea();
140
+ }
141
+ if (!Number.isFinite(signed) ||
142
+ !Number.isFinite(area) ||
143
+ Math.abs(signed) <= epsilon * area)
144
+ fail("DEGENERATE", "Shell has zero or numerically unresolved enclosed volume.", key);
145
+ const volume = Object.freeze({
146
+ id,
147
+ key,
148
+ ior,
149
+ sigma,
150
+ bounds: Object.freeze({ min: tuple(bounds.min), max: tuple(bounds.max) }),
151
+ triangleIndices: Object.freeze(indices.slice()),
152
+ });
153
+ return { volume, faces, points, epsilon, bounds };
154
+ }
155
+ function classify(s, point) {
156
+ if (!s.bounds.clone().expandByScalar(s.epsilon).containsPoint(point))
157
+ return false;
158
+ const near = new Vector3();
159
+ let angle = 0;
160
+ for (const { triangle: t } of s.faces) {
161
+ if (t.closestPointToPoint(point, near).distanceTo(point) <= s.epsilon)
162
+ fail("BOUNDARY", "Origin is on/within tolerance of a medium surface; move it explicitly to one side.", s.volume.key);
163
+ const a = t.a.clone().sub(point), b = t.b.clone().sub(point), c = t.c.clone().sub(point);
164
+ const la = a.length(), lb = b.length(), lc = c.length();
165
+ angle +=
166
+ 2 *
167
+ Math.atan2(a.dot(b.clone().cross(c)), la * lb * lc + a.dot(b) * lc + b.dot(c) * la + c.dot(a) * lb);
168
+ }
169
+ const winding = Math.abs(angle / (4 * Math.PI)), inside = winding > 0.5;
170
+ if (!Number.isFinite(winding) || Math.abs(winding - (inside ? 1 : 0)) > 1e-5)
171
+ fail("CLASSIFICATION", "Noninteger winding: repair geometry or use a better-resolved scale.", s.volume.key);
172
+ // Edge/vertex hits invalidate a direction, never count both triangles of a face.
173
+ for (const direction of directions) {
174
+ const ray = new Ray(point, direction), hits = [];
175
+ let ambiguous = false;
176
+ for (const { triangle: t } of s.faces) {
177
+ const hit = ray.intersectTriangle(t.a, t.b, t.c, false, new Vector3());
178
+ if (!hit)
179
+ continue;
180
+ const bary = t.getBarycoord(hit, new Vector3());
181
+ if (!bary || Math.min(bary.x, bary.y, bary.z) < 1e-8) {
182
+ ambiguous = true;
183
+ break;
184
+ }
185
+ hits.push(hit.distanceTo(point));
186
+ }
187
+ if (ambiguous)
188
+ continue;
189
+ hits.sort((a, b) => a - b);
190
+ const crossings = hits.filter((t, i) => i === 0 || t - hits[i - 1] > s.epsilon).length;
191
+ if ((crossings % 2 === 1) !== inside)
192
+ fail("CLASSIFICATION", "Independent winding/ray classifications disagree; repair the shell.", s.volume.key);
193
+ }
194
+ return inside;
195
+ }
196
+ function touches(a, b, epsilon) {
197
+ if (!a.bounds.clone().expandByScalar(epsilon).intersectsBox(b.bounds))
198
+ return false;
199
+ const av = [a.triangle.a, a.triangle.b, a.triangle.c], bv = [b.triangle.a, b.triangle.b, b.triangle.c], near = new Vector3();
200
+ for (const p of av)
201
+ if (b.triangle.closestPointToPoint(p, near).distanceTo(p) <= epsilon)
202
+ return true;
203
+ for (const p of bv)
204
+ if (a.triangle.closestPointToPoint(p, near).distanceTo(p) <= epsilon)
205
+ return true;
206
+ for (const [v, target] of [
207
+ [av, b.triangle],
208
+ [bv, a.triangle],
209
+ ])
210
+ for (let i = 0; i < 3; i++) {
211
+ const direction = v[(i + 1) % 3].clone().sub(v[i]), length = direction.length(), ray = new Ray(v[i], direction.divideScalar(length));
212
+ const hit = ray.intersectTriangle(target.a, target.b, target.c, false, new Vector3());
213
+ if (hit && hit.distanceTo(v[i]) <= length + epsilon)
214
+ return true;
215
+ }
216
+ // Coplanar contact also needs edge/edge distance.
217
+ for (let i = 0; i < 3; i++)
218
+ for (let j = 0; j < 3; j++) {
219
+ const d = av[(i + 1) % 3].clone().sub(av[i]), length = d.length(), onRay = new Vector3();
220
+ new Ray(av[i], d.divideScalar(length)).distanceSqToSegment(bv[j], bv[(j + 1) % 3], onRay, near);
221
+ if (onRay.distanceTo(near) <= epsilon &&
222
+ onRay.distanceTo(av[i]) <= length + epsilon)
223
+ return true;
224
+ }
225
+ return false;
226
+ }
227
+ /** CPU catalog for properly nested, connected, closed transmissive shells. */
228
+ export function createMediumCatalog(snapshot) {
229
+ if (snapshot.version !== 1)
230
+ fail("INPUT", "Expected SurfaceScene version 1.");
231
+ const materials = new Map();
232
+ for (const m of snapshot.materials) {
233
+ if (!m.uuid || materials.has(m.uuid))
234
+ fail("MATERIAL_ID", "Material UUIDs must be unique and nonempty.", m.uuid);
235
+ const p = m, transmission = p.transmission ?? 0;
236
+ if (!Number.isFinite(transmission) || transmission < 0 || transmission > 1)
237
+ fail("MATERIAL", "Transmission must be finite and in [0,1].", m.uuid);
238
+ materials.set(m.uuid, p);
239
+ }
240
+ const groups = new Map();
241
+ snapshot.triangles.forEach((t, index) => {
242
+ if (typeof t.objectUuid !== "string" ||
243
+ !t.objectUuid ||
244
+ !Number.isSafeInteger(t.instanceId) ||
245
+ t.instanceId < 0)
246
+ fail("IDENTITY", "Require nonempty object UUID and nonnegative safe integer instance ID.");
247
+ const key = JSON.stringify([t.objectUuid, t.instanceId]), m = materials.get(t.materialUuid);
248
+ if (!m)
249
+ fail("MATERIAL_ID", "Triangle references an unknown material UUID.", key);
250
+ if (!groups.has(key))
251
+ groups.set(key, { indices: [], transmitting: 0 });
252
+ const group = groups.get(key);
253
+ group.indices.push(index);
254
+ if ((m.transmission ?? 0) > 0)
255
+ group.transmitting++;
256
+ });
257
+ const solids = [];
258
+ for (const [key, group] of groups) {
259
+ if (!group.transmitting)
260
+ continue;
261
+ if (group.transmitting !== group.indices.length)
262
+ fail("PARTIAL_TRANSMISSION", "Every triangle of a volume object/instance must transmit; opaque coatings are not supported.", key);
263
+ let properties;
264
+ for (const index of group.indices) {
265
+ const t = snapshot.triangles[index], p = optical(materials.get(t.materialUuid), key);
266
+ if (properties &&
267
+ (properties.ior !== p.ior ||
268
+ properties.sigma.some((s, i) => s !== p.sigma[i])))
269
+ fail("INCONSISTENT_MEDIUM", "IOR and absorption must agree across all faces; split true phase interfaces into nested objects.", key);
270
+ properties = p;
271
+ const n = t.normalMatrix;
272
+ if (n.length !== 9 || !n.every(Number.isFinite))
273
+ fail("MATRIX", "Expected a finite 3x3 inverse-transpose normal matrix.", key);
274
+ const det = n[0] * (n[4] * n[8] - n[5] * n[7]) -
275
+ n[3] * (n[1] * n[8] - n[2] * n[7]) +
276
+ n[6] * (n[1] * n[5] - n[2] * n[4]);
277
+ if (!Number.isFinite(det) || det === 0)
278
+ fail("MATRIX", "A medium transform must be nonsingular; negative determinant is supported.", key);
279
+ }
280
+ solids.push(build(group.indices, snapshot, solids.length + 1, key, properties.ior, properties.sigma));
281
+ }
282
+ const containers = solids.map(() => new Set());
283
+ for (let i = 0; i < solids.length; i++)
284
+ for (let j = i + 1; j < solids.length; j++) {
285
+ const a = solids[i], b = solids[j], epsilon = Math.max(a.epsilon, b.epsilon);
286
+ if (!a.bounds.clone().expandByScalar(epsilon).intersectsBox(b.bounds))
287
+ continue;
288
+ for (const fa of a.faces)
289
+ for (const fb of b.faces)
290
+ if (touches(fa, fb, epsilon))
291
+ fail("OVERLAP", "Medium boundaries intersect/touch; only disjoint or strictly nested shells are supported. Other key: " +
292
+ b.volume.key, a.volume.key);
293
+ if (classify(a, b.points[0]))
294
+ containers[j].add(i);
295
+ if (classify(b, a.points[0]))
296
+ containers[i].add(j);
297
+ }
298
+ const order = solids
299
+ .map((_, i) => i)
300
+ .sort((a, b) => containers[a].size - containers[b].size || a - b);
301
+ const volumes = Object.freeze(solids.map((s) => s.volume));
302
+ const tolerances = Object.freeze(solids.map((s) => Object.freeze({ id: s.volume.id, weld: s.epsilon })));
303
+ const nesting = Object.freeze(solids.map((s, i) => {
304
+ const chain = [...containers[i]].sort((a, b) => containers[b].size - containers[a].size);
305
+ for (let c = 1; c < chain.length; c++)
306
+ if (!containers[chain[c - 1]].has(chain[c]))
307
+ fail("OVERLAP", "Containers do not form a proper nesting chain.", s.volume.key);
308
+ return Object.freeze({
309
+ id: s.volume.id,
310
+ parentId: chain.length ? solids[chain[0]].volume.id : 0,
311
+ });
312
+ }));
313
+ let disposed = false;
314
+ function alive() {
315
+ if (disposed)
316
+ fail("DISPOSED", "Create a new catalog after dispose().");
317
+ }
318
+ return Object.freeze({
319
+ get volumes() {
320
+ alive();
321
+ return volumes;
322
+ },
323
+ initial(origin) {
324
+ alive();
325
+ const point = vector(origin, "origin"), result = [];
326
+ for (const index of order)
327
+ if (classify(solids[index], point))
328
+ result.push(solids[index].volume.id);
329
+ return Object.freeze(result);
330
+ },
331
+ inspect() {
332
+ return Object.freeze({
333
+ disposed,
334
+ volumes: disposed ? Object.freeze([]) : volumes,
335
+ tolerance: disposed ? Object.freeze([]) : tolerances,
336
+ nesting: disposed ? Object.freeze([]) : nesting,
337
+ });
338
+ },
339
+ dispose() {
340
+ if (!disposed) {
341
+ disposed = true;
342
+ solids.length = 0;
343
+ containers.length = 0;
344
+ order.length = 0;
345
+ }
346
+ },
347
+ });
348
+ }