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,277 @@
1
+ import path from "node:path";
2
+ export function parseGLB(data, ctx) {
3
+ if (data.toString("latin1", 0, 4) !== "glTF") {
4
+ if (path.extname(ctx.file).toLowerCase() === ".glb")
5
+ ctx.invalid("GLB/header", "Missing or damaged GLB magic bytes.");
6
+ ctx.unsupported("model/format", "Only self-contained GLB 2.0 is implemented; JSON .gltf is not supported.");
7
+ }
8
+ ctx.check(data.length >= 20, "GLB/header", "Truncated GLB header.");
9
+ if (data.readUInt32LE(4) !== 2)
10
+ ctx.unsupported("GLB/version", `GLB version ${data.readUInt32LE(4)} is not implemented.`);
11
+ ctx.check(data.readUInt32LE(8) === data.length && data.length % 4 === 0, "GLB/length", "Declared GLB length must equal actual bytes and be four-byte aligned.");
12
+ let offset = 12, raw, bin = Buffer.alloc(0), hasBin = false;
13
+ while (offset < data.length) {
14
+ ctx.check(offset + 8 <= data.length, "GLB/chunk", "Truncated chunk header.");
15
+ const size = data.readUInt32LE(offset), type = data.readUInt32LE(offset + 4), end = offset + 8 + size;
16
+ ctx.check(size % 4 === 0 && end <= data.length, "GLB/chunk", "Chunk length is misaligned or exceeds GLB boundary.");
17
+ const chunk = data.subarray(offset + 8, end);
18
+ if (type === 0x4e4f534a) {
19
+ ctx.check(offset === 12 && raw === undefined, "GLB/JSON", "Exactly one JSON chunk must be first.");
20
+ try {
21
+ raw = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(chunk));
22
+ }
23
+ catch {
24
+ ctx.invalid("GLB/JSON", "Malformed UTF-8 or JSON chunk (padding must be spaces).");
25
+ }
26
+ }
27
+ else if (type === 0x004e4942) {
28
+ ctx.check(raw !== undefined && !hasBin, "GLB/BIN", "Duplicate BIN chunk or BIN before JSON.");
29
+ hasBin = true;
30
+ bin = chunk;
31
+ }
32
+ else
33
+ ctx.unsupported("GLB/chunk", `Unknown GLB chunk type 0x${type.toString(16)}.`);
34
+ offset = end;
35
+ }
36
+ ctx.check(raw && typeof raw === "object" && !Array.isArray(raw), "GLB/JSON", "JSON root must be an object.");
37
+ const json = raw;
38
+ ctx.check(json.asset && typeof json.asset === "object", "asset", "Missing glTF asset declaration.");
39
+ if (json.asset.version !== "2.0" ||
40
+ (json.asset.minVersion !== undefined && json.asset.minVersion !== "2.0"))
41
+ ctx.unsupported("asset/version", "Only glTF 2.0 is implemented.");
42
+ for (const key of [
43
+ "buffers",
44
+ "bufferViews",
45
+ "accessors",
46
+ "images",
47
+ "textures",
48
+ "samplers",
49
+ "materials",
50
+ "meshes",
51
+ "nodes",
52
+ "skins",
53
+ "animations",
54
+ "scenes",
55
+ "cameras",
56
+ ]) {
57
+ const list = json[key];
58
+ if (list !== undefined)
59
+ ctx.check(Array.isArray(list) &&
60
+ list.length > 0 &&
61
+ list.every((item) => item && typeof item === "object" && !Array.isArray(item)), key, "Expected a nonempty array of objects.");
62
+ }
63
+ const supported = new Set(["KHR_materials_specular", "KHR_materials_ior"]);
64
+ for (const key of ["extensionsUsed", "extensionsRequired"])
65
+ if (json[key] !== undefined) {
66
+ ctx.check(Array.isArray(json[key]) &&
67
+ json[key].length > 0 &&
68
+ json[key].every((s) => typeof s === "string" && s.length > 0) &&
69
+ new Set(json[key]).size === json[key].length, key, "Expected extension names.");
70
+ const unknown = json[key].filter((name) => !supported.has(name));
71
+ if (unknown.length)
72
+ ctx.unsupported(key, `glTF extensions are not implemented: ${unknown.join(", ")}.`);
73
+ }
74
+ const used = new Set(json.extensionsUsed ?? []);
75
+ for (const name of json.extensionsRequired ?? [])
76
+ ctx.check(used.has(name), "extensionsRequired", `${name} must also be declared in extensionsUsed.`);
77
+ const encountered = new Set();
78
+ // Never silently lose optional extension semantics in the metadata.
79
+ const checkExtensions = (value, location) => {
80
+ if (typeof value === "number")
81
+ ctx.check(Number.isFinite(value), location, "JSON numbers must be finite.");
82
+ if (!value || typeof value !== "object")
83
+ return;
84
+ if (!Array.isArray(value) && "name" in value)
85
+ ctx.check(typeof value.name === "string", location + "/name", "Object name must be a string.");
86
+ for (const [key, child] of Object.entries(value)) {
87
+ if (key === "extras")
88
+ continue;
89
+ if (key === "extensions") {
90
+ ctx.check(child && typeof child === "object" && !Array.isArray(child), location + "/extensions", "Expected extension object.");
91
+ for (const [name, payload] of Object.entries(child)) {
92
+ const at = location + "/extensions/" + name;
93
+ if (!supported.has(name) || !/^glTF\/materials\/(0|[1-9]\d*)$/.test(location))
94
+ ctx.unsupported(at, `Extension payload ${name} is not supported at this location.`);
95
+ ctx.check(used.has(name), at, `${name} must be declared in extensionsUsed.`);
96
+ encountered.add(name);
97
+ ctx.check(payload && typeof payload === "object" && !Array.isArray(payload), at, "Expected extension object.");
98
+ for (const [field, fieldValue] of Object.entries(payload)) {
99
+ if (name === "KHR_materials_specular" && (field === "specularTexture" || field === "specularColorTexture")) {
100
+ checkExtensions(fieldValue, at + "/" + field);
101
+ continue;
102
+ }
103
+ if (field === "extensions" || field === "extras") {
104
+ checkExtensions({ [field]: fieldValue }, at);
105
+ continue;
106
+ }
107
+ const allowed = name === "KHR_materials_specular"
108
+ ? ["specularFactor", "specularColorFactor"] : ["ior"];
109
+ ctx.check(allowed.includes(field), at + "/" + field, "Unknown extension property.");
110
+ const validNumber = (v, min, max) => typeof v === "number" && Number.isFinite(v) && v >= min && v <= max;
111
+ ctx.check(field === "specularColorFactor"
112
+ ? Array.isArray(fieldValue) && fieldValue.length === 3 && fieldValue.every(v => validNumber(v, 0, 1))
113
+ : validNumber(fieldValue, field === "ior" ? 1 : 0, field === "ior" ? Infinity : 1), at + "/" + field, "Invalid extension factor or IOR.");
114
+ }
115
+ }
116
+ }
117
+ else
118
+ checkExtensions(child, location + "/" + key);
119
+ }
120
+ };
121
+ checkExtensions(json, "glTF");
122
+ for (const name of used)
123
+ ctx.check(encountered.has(name), "extensionsUsed", `${name} is declared but not used by a material.`);
124
+ const buffers = json.buffers ?? [];
125
+ for (let i = 0; i < buffers.length; i++)
126
+ if (buffers[i].uri !== undefined)
127
+ ctx.unsupported(`buffers/${i}/uri`, `External or URI buffer resource ${String(buffers[i].uri)} is not supported; embed it in the GLB BIN chunk.`);
128
+ ctx.check(buffers.length <= 1, "buffers", "GLB may have only one embedded buffer.");
129
+ if (buffers.length) {
130
+ const size = ctx.integer(buffers[0].byteLength, "buffers/0/byteLength", 1);
131
+ ctx.check(hasBin && size <= bin.length && bin.length - size <= 3, "buffers/0", "Missing BIN resource or buffer length differs from BIN length beyond padding.");
132
+ ctx.check(bin.subarray(size).every((b) => b === 0), "GLB/BIN", "BIN padding must be zero.");
133
+ bin = bin.subarray(0, size);
134
+ }
135
+ else
136
+ ctx.check(!hasBin, "GLB/BIN", "BIN chunk has no buffer declaration.");
137
+ for (const [i, view] of (json.bufferViews ?? []).entries()) {
138
+ ctx.check(view.buffer === 0 && buffers.length === 1, `bufferViews/${i}/buffer`, "Buffer reference does not exist.");
139
+ const start = ctx.integer(view.byteOffset ?? 0, `bufferViews/${i}/byteOffset`), size = ctx.integer(view.byteLength, `bufferViews/${i}/byteLength`, 1);
140
+ ctx.check(start + size <= bin.length, `bufferViews/${i}`, "Buffer view exceeds embedded buffer boundary.");
141
+ if (view.byteStride !== undefined) {
142
+ ctx.integer(view.byteStride, `bufferViews/${i}/byteStride`, 4, 252);
143
+ ctx.check(view.byteStride % 4 === 0, `bufferViews/${i}/byteStride`, "Vertex stride must be a multiple of four.");
144
+ }
145
+ if (view.target !== undefined)
146
+ ctx.check([34962, 34963].includes(view.target), `bufferViews/${i}/target`, "Invalid buffer view target.");
147
+ }
148
+ const sizes = {
149
+ 5120: 1,
150
+ 5121: 1,
151
+ 5122: 2,
152
+ 5123: 2,
153
+ 5125: 4,
154
+ 5126: 4,
155
+ };
156
+ const counts = {
157
+ SCALAR: 1,
158
+ VEC2: 2,
159
+ VEC3: 3,
160
+ VEC4: 4,
161
+ MAT2: 4,
162
+ MAT3: 9,
163
+ MAT4: 16,
164
+ };
165
+ let inspectedValues = 0;
166
+ const accessors = (json.accessors ?? []).map((a, i) => {
167
+ const at = `accessors/${i}`;
168
+ ctx.integer(a.count, at + "/count", 1);
169
+ ctx.check(sizes[a.componentType] && counts[a.type], at, "Invalid accessor componentType or type.");
170
+ const size = sizes[a.componentType], components = counts[a.type];
171
+ if (a.type.startsWith("MAT") && size < 4)
172
+ ctx.unsupported(at, "Matrix accessors with sub-32-bit components are not implemented.");
173
+ if (a.normalized !== undefined)
174
+ ctx.check(typeof a.normalized === "boolean" &&
175
+ (!a.normalized || [5120, 5121, 5122, 5123].includes(a.componentType)), at + "/normalized", "Invalid normalized accessor.");
176
+ const byteOffset = ctx.integer(a.byteOffset ?? 0, at + "/byteOffset");
177
+ ctx.check(byteOffset % size === 0, at, "Accessor byte offset is misaligned.");
178
+ let source, stride = size * components, start = 0;
179
+ if (a.bufferView !== undefined) {
180
+ const index = ctx.integer(a.bufferView, at + "/bufferView", 0, (json.bufferViews?.length ?? 0) - 1);
181
+ const view = json.bufferViews[index];
182
+ stride = view.byteStride ?? stride;
183
+ ctx.check(stride >= size * components &&
184
+ stride % size === 0 &&
185
+ byteOffset + (a.count - 1) * stride + size * components <=
186
+ view.byteLength, at, "Accessor range or stride exceeds buffer view boundary.");
187
+ start = (view.byteOffset ?? 0) + byteOffset;
188
+ ctx.check(start % size === 0, at, "Accessor absolute byte offset is misaligned.");
189
+ source = bin;
190
+ }
191
+ else
192
+ ctx.check(byteOffset === 0, at, "Accessor without buffer view cannot have byte offset.");
193
+ inspectedValues += a.count * components;
194
+ if (inspectedValues > 16_000_000)
195
+ ctx.unsupported(at, "Accessors exceed the 16 million scalar inspection limit.");
196
+ const replacements = new Map();
197
+ if (a.sparse !== undefined) {
198
+ const sparse = a.sparse, sparseAt = at + "/sparse";
199
+ ctx.check(sparse && typeof sparse === "object" && !Array.isArray(sparse), sparseAt, "Expected a sparse object.");
200
+ if (components > 4)
201
+ ctx.unsupported(sparseAt, "Sparse matrices are not supported by the current model loader.");
202
+ if (a.bufferView !== undefined && json.bufferViews[a.bufferView].byteStride !== undefined)
203
+ ctx.unsupported(sparseAt, "Sparse accessors with interleaved base data are not supported by the current model loader.");
204
+ const count = ctx.integer(sparse.count, sparseAt + "/count", 1, a.count);
205
+ ctx.check(sparse.indices && typeof sparse.indices === "object" && !Array.isArray(sparse.indices), sparseAt + "/indices", "Expected sparse indices.");
206
+ ctx.check(sparse.values && typeof sparse.values === "object" && !Array.isArray(sparse.values), sparseAt + "/values", "Expected sparse values.");
207
+ const indexSize = sizes[sparse.indices.componentType];
208
+ ctx.check([5121, 5123, 5125].includes(sparse.indices.componentType), sparseAt + "/indices/componentType", "Sparse indices must be unsigned byte, short, or int.");
209
+ const sparseStart = (part, label, alignment, length) => {
210
+ const resource = sparseAt + "/" + label;
211
+ const index = ctx.integer(part.bufferView, resource + "/bufferView", 0, (json.bufferViews?.length ?? 0) - 1);
212
+ const view = json.bufferViews[index];
213
+ const offset = ctx.integer(part.byteOffset ?? 0, resource + "/byteOffset");
214
+ ctx.check(view.byteStride === undefined && view.target === undefined, resource + "/bufferView", "Sparse buffer views cannot have byteStride or target.");
215
+ ctx.check(offset % alignment === 0 && ((view.byteOffset ?? 0) + offset) % alignment === 0, resource + "/byteOffset", "Sparse data is misaligned.");
216
+ ctx.check(offset + length <= view.byteLength, resource, "Sparse data exceeds buffer view boundary.");
217
+ return (view.byteOffset ?? 0) + offset;
218
+ };
219
+ const indexStart = sparseStart(sparse.indices, "indices", indexSize, count * indexSize);
220
+ const valueStart = sparseStart(sparse.values, "values", size, count * components * size);
221
+ let previous = -1;
222
+ for (let n = 0; n < count; n++) {
223
+ const p = indexStart + n * indexSize;
224
+ const index = indexSize === 1 ? bin.readUInt8(p) : indexSize === 2 ? bin.readUInt16LE(p) : bin.readUInt32LE(p);
225
+ ctx.check(index > previous && index < a.count, sparseAt + "/indices", "Sparse indices must be strictly increasing and within accessor count.");
226
+ replacements.set(index, valueStart + n * components * size);
227
+ previous = index;
228
+ }
229
+ }
230
+ const values = [], min = Array(components).fill(Infinity), max = Array(components).fill(-Infinity);
231
+ for (let element = 0; element < a.count; element++)
232
+ for (let c = 0; c < components; c++) {
233
+ const replacement = replacements.get(element);
234
+ const p = replacement === undefined ? start + element * stride + c * size : replacement + c * size;
235
+ let v = 0;
236
+ if (source || replacement !== undefined)
237
+ switch (a.componentType) {
238
+ case 5120:
239
+ v = bin.readInt8(p);
240
+ break;
241
+ case 5121:
242
+ v = bin.readUInt8(p);
243
+ break;
244
+ case 5122:
245
+ v = bin.readInt16LE(p);
246
+ break;
247
+ case 5123:
248
+ v = bin.readUInt16LE(p);
249
+ break;
250
+ case 5125:
251
+ v = bin.readUInt32LE(p);
252
+ break;
253
+ case 5126:
254
+ v = bin.readFloatLE(p);
255
+ break;
256
+ }
257
+ ctx.check(Number.isFinite(v), at, "Accessor contains a non-finite value.");
258
+ min[c] = Math.min(min[c], v);
259
+ max[c] = Math.max(max[c], v);
260
+ values.push(v);
261
+ }
262
+ for (const [key, actual] of [
263
+ ["min", min],
264
+ ["max", max],
265
+ ])
266
+ if (a[key] !== undefined) {
267
+ const declared = a[key];
268
+ ctx.check(Array.isArray(declared) &&
269
+ declared.length === components &&
270
+ declared.every((v, c) => Number.isFinite(v) &&
271
+ Math.abs(v - actual[c]) <=
272
+ Math.max(1, Math.abs(actual[c])) * 1e-6), at + "/" + key, "Declared accessor bounds do not match decoded values.");
273
+ }
274
+ return { definition: a, values, components, min, max };
275
+ });
276
+ return { json, bin, accessors };
277
+ }
@@ -0,0 +1,53 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { AssetInspectionError, InspectionContext } from "./errors.js";
5
+ import { inspectModel } from "./model.js";
6
+ import { inspectTexture } from "./texture.js";
7
+ import { inspectWAV } from "./audio.js";
8
+ export { AssetInspectionError } from "./errors.js";
9
+ /** Read-only, offline inspection. Reject invalid assets before returning JSON metadata. */
10
+ export async function inspectAsset(file, type) {
11
+ const ctx = new InspectionContext(file);
12
+ let data;
13
+ try {
14
+ data = await fs.readFile(file);
15
+ }
16
+ catch (error) {
17
+ const code = error.code;
18
+ if (code === "ENOENT" || code === "ENOTDIR")
19
+ throw new AssetInspectionError("TC_ASSET_MISSING", file, "file", "Asset file does not exist.", "Correct the asset path or restore the source file.");
20
+ return ctx.invalid("file", `Cannot read asset (${code ?? "I/O error"}).`);
21
+ }
22
+ try {
23
+ let metadata;
24
+ switch (type) {
25
+ case "model":
26
+ metadata = await inspectModel(data, ctx);
27
+ break;
28
+ case "texture":
29
+ metadata = inspectTexture(data, ctx);
30
+ break;
31
+ case "audio":
32
+ metadata = inspectWAV(data, ctx);
33
+ break;
34
+ case "binary":
35
+ metadata = { format: "binary" };
36
+ break;
37
+ default:
38
+ return ctx.unsupported("type", `Unknown asset type ${String(type)}.`);
39
+ }
40
+ return {
41
+ ...metadata,
42
+ bytes: data.length,
43
+ sha256: createHash("sha256").update(data).digest("hex"),
44
+ };
45
+ }
46
+ catch (error) {
47
+ if (error instanceof AssetInspectionError)
48
+ throw error;
49
+ // A recognizable supported format with a damaged schema must never leak a raw TypeError.
50
+ const extension = path.extname(file).toLowerCase();
51
+ return ctx.invalid(type, `Malformed ${extension || type} asset: ${error instanceof Error ? error.message : String(error)}`);
52
+ }
53
+ }
@@ -0,0 +1,199 @@
1
+ // Baseline sequential Huffman JPEG: validate every coefficient in the entropy stream.
2
+ // Pixel reconstruction/IDCT is unnecessary for dimensions and integrity inspection.
3
+ export function inspectJPEG(data, ctx) {
4
+ let offset = 2, width = 0, height = 0, restartInterval = 0;
5
+ const quantization = new Set();
6
+ const tables = new Map();
7
+ const components = [];
8
+ while (offset < data.length) {
9
+ ctx.check(data[offset++] === 0xff, "JPEG/marker", "Expected marker prefix.");
10
+ while (data[offset] === 0xff)
11
+ offset++;
12
+ const marker = data[offset++];
13
+ ctx.check(marker !== undefined &&
14
+ marker !== 0 &&
15
+ marker !== 0xd8 &&
16
+ marker !== 0xd9 &&
17
+ !(marker >= 0xd0 && marker <= 0xd7), "JPEG/marker", "Unexpected marker before image scan.");
18
+ ctx.check(offset + 2 <= data.length, "JPEG/segment", "Truncated segment length.");
19
+ const length = data.readUInt16BE(offset), end = offset + length;
20
+ ctx.check(length >= 2 && end <= data.length, "JPEG/segment", "Segment length exceeds file.");
21
+ const segment = data.subarray(offset + 2, end);
22
+ offset = end;
23
+ if (marker === 0xdb) {
24
+ let p = 0;
25
+ while (p < segment.length) {
26
+ const spec = segment[p++], precision = spec >>> 4, id = spec & 15;
27
+ ctx.check(precision <= 1 &&
28
+ id <= 3 &&
29
+ p + 64 * (precision + 1) <= segment.length, "JPEG/DQT", "Invalid quantization table.");
30
+ if (precision)
31
+ ctx.unsupported("JPEG/DQT", "16-bit quantization tables are not baseline JPEG.");
32
+ for (let i = 0; i < 64; i++)
33
+ ctx.check(segment[p++] > 0, "JPEG/DQT", "Zero quantization coefficient.");
34
+ quantization.add(id);
35
+ }
36
+ ctx.check(segment.length > 0, "JPEG/DQT", "Empty quantization segment.");
37
+ }
38
+ else if (marker === 0xc4) {
39
+ let p = 0;
40
+ while (p < segment.length) {
41
+ ctx.check(p + 17 <= segment.length, "JPEG/DHT", "Truncated Huffman table.");
42
+ const spec = segment[p++], kind = spec >>> 4, id = spec & 15;
43
+ ctx.check(kind <= 1 && id <= 3, "JPEG/DHT", "Invalid Huffman table selector.");
44
+ const counts = segment.subarray(p, p + 16);
45
+ p += 16;
46
+ const total = counts.reduce((a, b) => a + b, 0);
47
+ ctx.check(total > 0 && total <= 256 && p + total <= segment.length, "JPEG/DHT", "Invalid Huffman symbol count.");
48
+ const table = new Map();
49
+ let code = 0;
50
+ for (let bits = 1; bits <= 16; bits++) {
51
+ ctx.check(code + counts[bits - 1] < 2 ** bits, "JPEG/DHT", "Oversubscribed Huffman tree or forbidden all-ones code.");
52
+ for (let i = 0; i < counts[bits - 1]; i++) {
53
+ const value = segment[p++];
54
+ ctx.check(kind === 0
55
+ ? value <= 11
56
+ : value === 0 ||
57
+ value === 0xf0 ||
58
+ ((value & 15) > 0 && (value & 15) <= 10), "JPEG/DHT", "Invalid baseline Huffman symbol.");
59
+ table.set((1 << bits) + code++, value);
60
+ }
61
+ code *= 2;
62
+ }
63
+ tables.set(`${kind}/${id}`, table);
64
+ }
65
+ ctx.check(segment.length > 0, "JPEG/DHT", "Empty Huffman segment.");
66
+ }
67
+ else if (marker === 0xc0) {
68
+ ctx.check(!components.length && segment.length >= 6 && segment[0] === 8, "JPEG/SOF0", "Invalid baseline frame or duplicate frame header.");
69
+ height = segment.readUInt16BE(1);
70
+ width = segment.readUInt16BE(3);
71
+ const count = segment[5];
72
+ ctx.check(width > 0 &&
73
+ height > 0 &&
74
+ count > 0 &&
75
+ segment.length === 6 + count * 3, "JPEG/SOF0", "Invalid dimensions or frame component count.");
76
+ if (![1, 3].includes(count))
77
+ ctx.unsupported("JPEG/components", "Only grayscale and three-component baseline JPEG are implemented.");
78
+ for (let i = 0; i < count; i++) {
79
+ const p = 6 + i * 3, id = segment[p], h = segment[p + 1] >>> 4, v = segment[p + 1] & 15, q = segment[p + 2];
80
+ ctx.check(!components.some((c) => c.id === id) &&
81
+ h > 0 &&
82
+ h <= 4 &&
83
+ v > 0 &&
84
+ v <= 4 &&
85
+ q <= 3, "JPEG/SOF0", "Invalid frame component or sampling factor.");
86
+ components.push({ id, h, v, q });
87
+ }
88
+ ctx.check(components.reduce((n, c) => n + c.h * c.v, 0) <= 10, "JPEG/SOF0", "Too many blocks per MCU.");
89
+ if (count === 1 && (components[0].h !== 1 || components[0].v !== 1))
90
+ ctx.unsupported("JPEG/sampling", "Grayscale sampling factors other than 1x1 are not implemented.");
91
+ }
92
+ else if (marker === 0xdd) {
93
+ ctx.check(segment.length === 2, "JPEG/DRI", "Invalid restart interval segment.");
94
+ restartInterval = segment.readUInt16BE(0);
95
+ }
96
+ else if (marker === 0xda) {
97
+ ctx.check(components.length && segment.length >= 4, "JPEG/SOS", "Missing frame or truncated scan header.");
98
+ const count = segment[0];
99
+ ctx.check(count > 0 && segment.length === 1 + count * 2 + 3, "JPEG/SOS", "Invalid scan component count.");
100
+ if (count !== components.length)
101
+ ctx.unsupported("JPEG/SOS", "Multiple/non-interleaved scans are not implemented.");
102
+ ctx.check(segment[segment.length - 3] === 0 &&
103
+ segment[segment.length - 2] === 63 &&
104
+ segment[segment.length - 1] === 0, "JPEG/SOS", "Invalid baseline spectral selection.");
105
+ const scan = [];
106
+ for (let i = 0; i < count; i++) {
107
+ const id = segment[1 + i * 2], spec = segment[2 + i * 2], c = components.find((c) => c.id === id);
108
+ ctx.check(c && !scan.includes(c), "JPEG/SOS", "Unknown or duplicate scan component.");
109
+ c.dc = spec >>> 4;
110
+ c.ac = spec & 15;
111
+ ctx.check(quantization.has(c.q) &&
112
+ tables.has(`0/${c.dc}`) &&
113
+ tables.has(`1/${c.ac}`), "JPEG/SOS", "Missing quantization or Huffman table.");
114
+ scan.push(c);
115
+ }
116
+ const hmax = Math.max(...components.map((c) => c.h)), vmax = Math.max(...components.map((c) => c.v));
117
+ const mcus = Math.ceil(width / (8 * hmax)) * Math.ceil(height / (8 * vmax));
118
+ if (mcus * components.reduce((n, c) => n + c.h * c.v, 0) > 8_000_000)
119
+ ctx.unsupported("JPEG/scan", "Image exceeds the inspection block limit.");
120
+ let bitsLeft = 0, current = 0, restart = 0;
121
+ const bit = () => {
122
+ if (!bitsLeft) {
123
+ ctx.check(offset < data.length, "JPEG/entropy", "Truncated entropy stream.");
124
+ current = data[offset++];
125
+ if (current === 0xff)
126
+ ctx.check(data[offset++] === 0, "JPEG/entropy", "Unexpected marker or truncated entropy stream.");
127
+ bitsLeft = 8;
128
+ }
129
+ return (current >>> --bitsLeft) & 1;
130
+ };
131
+ const symbol = (table) => {
132
+ let code = 0;
133
+ for (let n = 1; n <= 16; n++) {
134
+ code = code * 2 + bit();
135
+ const value = table.get((1 << n) + code);
136
+ if (value !== undefined)
137
+ return value;
138
+ }
139
+ return ctx.invalid("JPEG/entropy", "Entropy code is absent from Huffman table.");
140
+ };
141
+ const align = () => {
142
+ ctx.check((current & ((1 << bitsLeft) - 1)) === (1 << bitsLeft) - 1, "JPEG/entropy", "Invalid entropy padding bits.");
143
+ bitsLeft = 0;
144
+ };
145
+ for (let mcu = 0; mcu < mcus; mcu++) {
146
+ if (mcu && restartInterval && mcu % restartInterval === 0) {
147
+ align();
148
+ ctx.check(data[offset++] === 0xff &&
149
+ data[offset++] === 0xd0 + (restart++ % 8), "JPEG/restart", "Missing or out-of-order restart marker.");
150
+ }
151
+ for (const c of scan)
152
+ for (let block = 0; block < c.h * c.v; block++) {
153
+ const dcBits = symbol(tables.get(`0/${c.dc}`));
154
+ for (let i = 0; i < dcBits; i++)
155
+ bit();
156
+ let coefficient = 1;
157
+ while (coefficient < 64) {
158
+ const ac = symbol(tables.get(`1/${c.ac}`));
159
+ if (ac === 0)
160
+ break;
161
+ if (ac === 0xf0)
162
+ coefficient += 16;
163
+ else {
164
+ coefficient += ac >>> 4;
165
+ ctx.check(coefficient < 64, "JPEG/entropy", "AC run exceeds block boundary.");
166
+ for (let i = 0; i < (ac & 15); i++)
167
+ bit();
168
+ coefficient++;
169
+ }
170
+ ctx.check(coefficient <= 64, "JPEG/entropy", "AC run exceeds block boundary.");
171
+ }
172
+ }
173
+ }
174
+ align();
175
+ ctx.check(data[offset++] === 0xff, "JPEG/EOI", "Missing end marker or excess entropy data.");
176
+ while (data[offset] === 0xff)
177
+ offset++;
178
+ ctx.check(data[offset++] === 0xd9 && offset === data.length, "JPEG/EOI", "Missing end marker, extra scan or trailing data.");
179
+ return {
180
+ format: "jpeg",
181
+ width,
182
+ height,
183
+ bitDepth: 8,
184
+ channels: components.length,
185
+ encoding: "baseline-huffman",
186
+ };
187
+ }
188
+ else if ((marker >= 0xc1 &&
189
+ marker <= 0xcf &&
190
+ ![0xc4, 0xc8, 0xcc].includes(marker)) ||
191
+ marker === 0xcc) {
192
+ ctx.unsupported("JPEG/encoding", `JPEG frame/coding marker 0x${marker.toString(16)} is not baseline Huffman (SOF0).`);
193
+ }
194
+ else if (!(marker >= 0xe0 && marker <= 0xef) && marker !== 0xfe) {
195
+ ctx.unsupported("JPEG/marker", `JPEG marker 0x${marker.toString(16)} is not implemented.`);
196
+ }
197
+ }
198
+ return ctx.invalid("JPEG/SOS", "Missing image scan or truncated JPEG.");
199
+ }