@plasius/gpu-model-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +203 -0
- package/README.md +185 -0
- package/dist/index.cjs +2118 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +506 -0
- package/dist/index.d.ts +506 -0
- package/dist/index.js +2069 -0
- package/dist/index.js.map +1 -0
- package/package.json +109 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2069 @@
|
|
|
1
|
+
// src/model-document.ts
|
|
2
|
+
var GPU_MODEL_DOCUMENT_SCHEMA_VERSION = "plasius.gpu-model-document/1";
|
|
3
|
+
var CANONICAL_GPU_MODEL_COORDINATE_SYSTEM = Object.freeze({
|
|
4
|
+
unit: "metre",
|
|
5
|
+
upAxis: "y",
|
|
6
|
+
forwardAxis: "-z",
|
|
7
|
+
handedness: "right",
|
|
8
|
+
winding: "counter-clockwise",
|
|
9
|
+
origin: "floor-centred"
|
|
10
|
+
});
|
|
11
|
+
var GPU_MODEL_DOCUMENT_LIMITS = Object.freeze({
|
|
12
|
+
roots: 4096,
|
|
13
|
+
nodes: 65536,
|
|
14
|
+
resources: 16384,
|
|
15
|
+
accessors: 262144,
|
|
16
|
+
accessorElements: 16e6,
|
|
17
|
+
meshes: 65536,
|
|
18
|
+
primitives: 262144,
|
|
19
|
+
materials: 65536,
|
|
20
|
+
textures: 65536,
|
|
21
|
+
skeletons: 4096,
|
|
22
|
+
joints: 65536,
|
|
23
|
+
skins: 4096,
|
|
24
|
+
blendShapes: 65536,
|
|
25
|
+
blendShapeDeltas: 262144,
|
|
26
|
+
blendShapeAccessorReferences: 786432,
|
|
27
|
+
animations: 4096,
|
|
28
|
+
analyticGeometry: 4096,
|
|
29
|
+
diagnostics: 4096,
|
|
30
|
+
sceneEdges: 65535,
|
|
31
|
+
primitiveAttributes: 262144,
|
|
32
|
+
skinJoints: 65536,
|
|
33
|
+
weightVertices: 1e6,
|
|
34
|
+
weightInfluences: 8e6,
|
|
35
|
+
animationSamplers: 65536,
|
|
36
|
+
animationChannels: 262144,
|
|
37
|
+
animationValues: 8e6,
|
|
38
|
+
geometryIndexElements: 16e6,
|
|
39
|
+
geometryWorldAccessorInstances: 262144,
|
|
40
|
+
geometryWorldVertices: 16e6,
|
|
41
|
+
identifierLength: 128,
|
|
42
|
+
nameLength: 512,
|
|
43
|
+
diagnosticMessageLength: 2048,
|
|
44
|
+
metadataDepth: 8,
|
|
45
|
+
metadataValues: 4096,
|
|
46
|
+
metadataObjectKeys: 128,
|
|
47
|
+
metadataArrayLength: 512,
|
|
48
|
+
metadataKeyLength: 128,
|
|
49
|
+
metadataStringLength: 2048
|
|
50
|
+
});
|
|
51
|
+
var GPU_MODEL_RESOURCE_VERIFICATION_LIMITS = Object.freeze({
|
|
52
|
+
maxResourceBytes: 100 * 1024 * 1024,
|
|
53
|
+
maxAggregateBytes: 164 * 1024 * 1024,
|
|
54
|
+
maxImageBytes: 64 * 1024 * 1024,
|
|
55
|
+
maxAggregateImageBytes: 64 * 1024 * 1024,
|
|
56
|
+
maxImageDimension: 4096,
|
|
57
|
+
maxImagePixels: 4096 * 4096,
|
|
58
|
+
chunkBytes: 1024 * 1024,
|
|
59
|
+
timeoutMs: 3e4
|
|
60
|
+
});
|
|
61
|
+
function escapeDiagnosticText(value, maximum) {
|
|
62
|
+
let output = "";
|
|
63
|
+
for (const character of value) {
|
|
64
|
+
const point = character.codePointAt(0);
|
|
65
|
+
const unsafe = point < 32 || point >= 127 && point <= 159 || point === 8232 || point === 8233 || point >= 8234 && point <= 8238 || point >= 8294 && point <= 8297 || point >= 55296 && point <= 57343;
|
|
66
|
+
output += character === "\\" ? "\\\\" : unsafe ? `\\u${point.toString(16).padStart(point <= 65535 ? 4 : 6, "0")}` : character;
|
|
67
|
+
if (output.length >= maximum) break;
|
|
68
|
+
}
|
|
69
|
+
return output.slice(0, maximum);
|
|
70
|
+
}
|
|
71
|
+
var GpuModelDocumentError = class extends TypeError {
|
|
72
|
+
code;
|
|
73
|
+
path;
|
|
74
|
+
constructor(code, path, detail) {
|
|
75
|
+
const safePath = escapeDiagnosticText(path, 256);
|
|
76
|
+
const safeDetail = escapeDiagnosticText(detail, 512);
|
|
77
|
+
super(`Invalid GPU model document at ${safePath}: ${safeDetail}`);
|
|
78
|
+
this.name = "GpuModelDocumentError";
|
|
79
|
+
this.code = code;
|
|
80
|
+
this.path = safePath;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
var PROHIBITED_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
84
|
+
var IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
85
|
+
var METADATA_KEY = /^[A-Za-z][A-Za-z0-9_.:-]{0,127}$/u;
|
|
86
|
+
var TOKEN = /^[A-Za-z0-9][A-Za-z0-9.+_-]{0,127}$/u;
|
|
87
|
+
var ATTRIBUTE_SEMANTIC = /^(?:POSITION|NORMAL|TANGENT|TEXCOORD_[0-7]|COLOR_[0-7]|JOINTS_[0-7]|WEIGHTS_[0-7]|_[A-Z][A-Z0-9_]{0,62})$/u;
|
|
88
|
+
var SHA_256 = /^[a-f0-9]{64}$/u;
|
|
89
|
+
var MIME_TYPE = /^[a-z0-9][a-z0-9!#$&^_.+-]{0,63}\/[a-z0-9][a-z0-9!#$&^_.+-]{0,63}$/u;
|
|
90
|
+
var RELATIVE_PATH = /^(?!\/)(?!.*(?:^|\/)\.\.(?:\/|$))(?!.*\\)(?![a-z][a-z0-9+.-]*:).{1,2048}$/iu;
|
|
91
|
+
var FILE_NAME = /^(?!\.{1,2}$)(?!.*[\\/]).{1,512}$/u;
|
|
92
|
+
var GPU_FLOAT_MAX = 34028234663852886e22;
|
|
93
|
+
var EPSILON = 1e-5;
|
|
94
|
+
var BOUNDS_ABSOLUTE_EPSILON = 1e-5;
|
|
95
|
+
var COMPONENT_BYTES = Object.freeze({
|
|
96
|
+
i8: 1,
|
|
97
|
+
u8: 1,
|
|
98
|
+
i16: 2,
|
|
99
|
+
u16: 2,
|
|
100
|
+
u32: 4,
|
|
101
|
+
f32: 4
|
|
102
|
+
});
|
|
103
|
+
var ELEMENT_COMPONENTS = Object.freeze({
|
|
104
|
+
scalar: 1,
|
|
105
|
+
vec2: 2,
|
|
106
|
+
vec3: 3,
|
|
107
|
+
vec4: 4,
|
|
108
|
+
mat2: 4,
|
|
109
|
+
mat3: 9,
|
|
110
|
+
mat4: 16
|
|
111
|
+
});
|
|
112
|
+
var COMPONENT_RANGES = Object.freeze({
|
|
113
|
+
i8: [-128, 127, true],
|
|
114
|
+
u8: [0, 255, true],
|
|
115
|
+
i16: [-32768, 32767, true],
|
|
116
|
+
u16: [0, 65535, true],
|
|
117
|
+
u32: [0, 4294967295, true],
|
|
118
|
+
f32: [-GPU_FLOAT_MAX, GPU_FLOAT_MAX, false]
|
|
119
|
+
});
|
|
120
|
+
var IMAGE_MIME_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/ktx2", "image/png", "image/webp"]);
|
|
121
|
+
var BUFFER_MIME_TYPES = /* @__PURE__ */ new Set(["application/octet-stream", "application/vnd.plasius.gpu-buffer"]);
|
|
122
|
+
var TEXTURE_USAGES = Object.freeze([
|
|
123
|
+
"base-color",
|
|
124
|
+
"metallic-roughness",
|
|
125
|
+
"normal",
|
|
126
|
+
"occlusion",
|
|
127
|
+
"emissive",
|
|
128
|
+
"transmission",
|
|
129
|
+
"clearcoat",
|
|
130
|
+
"clearcoat-roughness",
|
|
131
|
+
"clearcoat-normal",
|
|
132
|
+
"sheen-color",
|
|
133
|
+
"sheen-roughness"
|
|
134
|
+
]);
|
|
135
|
+
var MATERIAL_TEXTURE_SLOTS = Object.freeze({
|
|
136
|
+
baseColor: "base-color",
|
|
137
|
+
metallicRoughness: "metallic-roughness",
|
|
138
|
+
normal: "normal",
|
|
139
|
+
occlusion: "occlusion",
|
|
140
|
+
emissive: "emissive",
|
|
141
|
+
transmission: "transmission",
|
|
142
|
+
clearcoat: "clearcoat",
|
|
143
|
+
clearcoatRoughness: "clearcoat-roughness",
|
|
144
|
+
clearcoatNormal: "clearcoat-normal",
|
|
145
|
+
sheenColor: "sheen-color",
|
|
146
|
+
sheenRoughness: "sheen-roughness"
|
|
147
|
+
});
|
|
148
|
+
var verifiedResources = /* @__PURE__ */ new WeakSet();
|
|
149
|
+
var verifiedResourceBytes = /* @__PURE__ */ new WeakMap();
|
|
150
|
+
var verifiedResourceEvidence = /* @__PURE__ */ new WeakMap();
|
|
151
|
+
var validatedDocuments = /* @__PURE__ */ new WeakSet();
|
|
152
|
+
function fail(code, path, detail) {
|
|
153
|
+
throw new GpuModelDocumentError(code, path, detail);
|
|
154
|
+
}
|
|
155
|
+
function readRecord(value, path, allowedKeys) {
|
|
156
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
157
|
+
return fail("invalid-type", path, "expected an object");
|
|
158
|
+
}
|
|
159
|
+
const prototype = Object.getPrototypeOf(value);
|
|
160
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
161
|
+
return fail("invalid-type", path, "expected a plain object");
|
|
162
|
+
}
|
|
163
|
+
const allowed = new Set(allowedKeys);
|
|
164
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
165
|
+
if (typeof key !== "string") return fail("unknown-key", path, "symbol keys are not permitted");
|
|
166
|
+
if (PROHIBITED_KEYS.has(key)) return fail("unknown-key", `${path}.${key}`, "prohibited object key");
|
|
167
|
+
if (!allowed.has(key)) return fail("unknown-key", `${path}.${key}`, "unknown key");
|
|
168
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
169
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
170
|
+
return fail("invalid-type", `${path}.${key}`, "properties must be enumerable data properties");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function requireKey(record, key, path) {
|
|
176
|
+
if (!Object.hasOwn(record, key)) return fail("invalid-value", `${path}.${key}`, "required key is missing");
|
|
177
|
+
return record[key];
|
|
178
|
+
}
|
|
179
|
+
function readArray(value, path, maximum, minimum = 0) {
|
|
180
|
+
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
181
|
+
return fail("invalid-type", path, "expected an array");
|
|
182
|
+
}
|
|
183
|
+
if (value.length < minimum || value.length > maximum) {
|
|
184
|
+
return fail("limit-exceeded", path, `array length must be between ${minimum} and ${maximum}`);
|
|
185
|
+
}
|
|
186
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
187
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
188
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) {
|
|
189
|
+
return fail("invalid-type", `${path}[${index}]`, "array entries must be enumerable data properties");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
193
|
+
if (key === "length") continue;
|
|
194
|
+
if (typeof key === "string" && /^(?:0|[1-9][0-9]*)$/u.test(key) && Number(key) < value.length) continue;
|
|
195
|
+
return fail("unknown-key", path, "array properties and symbol keys are not permitted");
|
|
196
|
+
}
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
function hasInvalidText(value) {
|
|
200
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
201
|
+
const code = value.charCodeAt(index);
|
|
202
|
+
if (code <= 31 || code >= 127 && code <= 159 || code === 8232 || code === 8233) return true;
|
|
203
|
+
if (code >= 55296 && code <= 56319) {
|
|
204
|
+
const trailing = value.charCodeAt(index + 1);
|
|
205
|
+
if (!Number.isInteger(trailing) || trailing < 56320 || trailing > 57343) return true;
|
|
206
|
+
index += 1;
|
|
207
|
+
} else if (code >= 56320 && code <= 57343) return true;
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
function readString(value, path, maximum, pattern) {
|
|
212
|
+
if (typeof value !== "string") return fail("invalid-type", path, "expected a string");
|
|
213
|
+
if (value.length === 0 || value.length > maximum) {
|
|
214
|
+
return fail("limit-exceeded", path, `string length must be between 1 and ${maximum}`);
|
|
215
|
+
}
|
|
216
|
+
if (hasInvalidText(value)) return fail("invalid-value", path, "string must contain well-formed text without control characters");
|
|
217
|
+
if (pattern && !pattern.test(value)) return fail("invalid-value", path, "string has an invalid format");
|
|
218
|
+
return value;
|
|
219
|
+
}
|
|
220
|
+
function readId(value, path) {
|
|
221
|
+
return readString(value, path, GPU_MODEL_DOCUMENT_LIMITS.identifierLength, IDENTIFIER);
|
|
222
|
+
}
|
|
223
|
+
function readSha256(value, path) {
|
|
224
|
+
if (typeof value !== "string" || !SHA_256.test(value)) {
|
|
225
|
+
return fail("invalid-value", path, "expected a lowercase hexadecimal SHA-256 digest");
|
|
226
|
+
}
|
|
227
|
+
return value;
|
|
228
|
+
}
|
|
229
|
+
function readBoolean(value, path) {
|
|
230
|
+
if (typeof value !== "boolean") return fail("invalid-type", path, "expected a boolean");
|
|
231
|
+
return value;
|
|
232
|
+
}
|
|
233
|
+
function readFinite(value, path, minimum, maximum) {
|
|
234
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fail("invalid-value", path, "expected a finite number");
|
|
235
|
+
const lower = minimum ?? -GPU_FLOAT_MAX;
|
|
236
|
+
const upper = maximum ?? GPU_FLOAT_MAX;
|
|
237
|
+
if (value < lower || value > upper) return fail("invalid-value", path, `number must be between ${String(lower)} and ${String(upper)}`);
|
|
238
|
+
return Object.is(value, -0) ? 0 : value;
|
|
239
|
+
}
|
|
240
|
+
function readSafeInteger(value, path, minimum = 0, maximum = Number.MAX_SAFE_INTEGER) {
|
|
241
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
242
|
+
return fail("invalid-value", path, `expected a safe integer between ${minimum} and ${maximum}`);
|
|
243
|
+
}
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
function readEnum(value, path, allowed) {
|
|
247
|
+
if (typeof value !== "string" || !allowed.includes(value)) {
|
|
248
|
+
return fail("invalid-value", path, `expected one of: ${allowed.join(", ")}`);
|
|
249
|
+
}
|
|
250
|
+
return value;
|
|
251
|
+
}
|
|
252
|
+
function readNumberTuple(value, path, length, minimum, maximum) {
|
|
253
|
+
const input = readArray(value, path, length, length);
|
|
254
|
+
return Object.freeze(input.map((entry, index) => readFinite(entry, `${path}[${index}]`, minimum, maximum)));
|
|
255
|
+
}
|
|
256
|
+
function readOptional(record, key, parser, path) {
|
|
257
|
+
if (!Object.hasOwn(record, key)) return void 0;
|
|
258
|
+
return parser(record[key], `${path}.${key}`);
|
|
259
|
+
}
|
|
260
|
+
function assignOptional(target, key, value) {
|
|
261
|
+
if (value !== void 0) Object.assign(target, { [key]: value });
|
|
262
|
+
}
|
|
263
|
+
function consumeBudget(budget, key, amount, maximum, path, label) {
|
|
264
|
+
budget[key] += amount;
|
|
265
|
+
if (!Number.isSafeInteger(budget[key]) || budget[key] > maximum) {
|
|
266
|
+
return fail("limit-exceeded", path, `document ${label} count exceeds the limit`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
function readMetadataValue(value, path, depth, budget) {
|
|
270
|
+
consumeBudget(budget, "metadataValues", 1, GPU_MODEL_DOCUMENT_LIMITS.metadataValues, path, "metadata value");
|
|
271
|
+
if (value === null || typeof value === "boolean") return value;
|
|
272
|
+
if (typeof value === "number") return readFinite(value, path);
|
|
273
|
+
if (typeof value === "string") return readString(value, path, GPU_MODEL_DOCUMENT_LIMITS.metadataStringLength);
|
|
274
|
+
if (depth >= GPU_MODEL_DOCUMENT_LIMITS.metadataDepth) return fail("limit-exceeded", path, "metadata nesting is too deep");
|
|
275
|
+
if (Array.isArray(value)) {
|
|
276
|
+
const input = readArray(value, path, GPU_MODEL_DOCUMENT_LIMITS.metadataArrayLength);
|
|
277
|
+
return Object.freeze(input.map((entry, index) => readMetadataValue(entry, `${path}[${index}]`, depth + 1, budget)));
|
|
278
|
+
}
|
|
279
|
+
if (typeof value !== "object" || value === null) return fail("invalid-type", path, "metadata value is not JSON-compatible");
|
|
280
|
+
const record = value;
|
|
281
|
+
const prototype = Object.getPrototypeOf(record);
|
|
282
|
+
if (prototype !== Object.prototype && prototype !== null) return fail("invalid-type", path, "metadata objects must be plain objects");
|
|
283
|
+
const keys = Reflect.ownKeys(record);
|
|
284
|
+
if (keys.length > GPU_MODEL_DOCUMENT_LIMITS.metadataObjectKeys) return fail("limit-exceeded", path, "metadata object has too many keys");
|
|
285
|
+
const output = {};
|
|
286
|
+
for (const key of keys) {
|
|
287
|
+
if (typeof key !== "string") return fail("unknown-key", path, "metadata symbol keys are not permitted");
|
|
288
|
+
if (PROHIBITED_KEYS.has(key)) return fail("unknown-key", `${path}.${key}`, "prohibited metadata key");
|
|
289
|
+
readString(key, `${path}.[key]`, GPU_MODEL_DOCUMENT_LIMITS.metadataKeyLength, METADATA_KEY);
|
|
290
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
291
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return fail("invalid-type", `${path}.${key}`, "metadata properties must be enumerable data properties");
|
|
292
|
+
output[key] = readMetadataValue(descriptor.value, `${path}.${key}`, depth + 1, budget);
|
|
293
|
+
}
|
|
294
|
+
return Object.freeze(output);
|
|
295
|
+
}
|
|
296
|
+
function readMetadata(value, path, budget) {
|
|
297
|
+
const parsed = readMetadataValue(value, path, 0, budget);
|
|
298
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return fail("invalid-type", path, "metadata must be an object");
|
|
299
|
+
return parsed;
|
|
300
|
+
}
|
|
301
|
+
function parseCoordinateSystem(value) {
|
|
302
|
+
const path = "$.coordinateSystem";
|
|
303
|
+
const record = readRecord(value, path, ["unit", "upAxis", "forwardAxis", "handedness", "winding", "origin"]);
|
|
304
|
+
for (const [key, expected] of Object.entries(CANONICAL_GPU_MODEL_COORDINATE_SYSTEM)) {
|
|
305
|
+
if (requireKey(record, key, path) !== expected) return fail("invalid-value", `${path}.${key}`, "coordinate system is not canonical");
|
|
306
|
+
}
|
|
307
|
+
return CANONICAL_GPU_MODEL_COORDINATE_SYSTEM;
|
|
308
|
+
}
|
|
309
|
+
function parseMatrix(value, path, requireAffine = true) {
|
|
310
|
+
const matrix = readNumberTuple(value, path, 16);
|
|
311
|
+
if (requireAffine && (matrix[3] !== 0 || matrix[7] !== 0 || matrix[11] !== 0 || matrix[15] !== 1)) {
|
|
312
|
+
return fail("invalid-value", path, "matrix must be affine, with a [0, 0, 0, 1] projective row");
|
|
313
|
+
}
|
|
314
|
+
return matrix;
|
|
315
|
+
}
|
|
316
|
+
function parseBounds(value, path) {
|
|
317
|
+
const record = readRecord(value, path, ["min", "max"]);
|
|
318
|
+
const min = readNumberTuple(requireKey(record, "min", path), `${path}.min`, 3);
|
|
319
|
+
const max = readNumberTuple(requireKey(record, "max", path), `${path}.max`, 3);
|
|
320
|
+
for (let index = 0; index < 3; index += 1) {
|
|
321
|
+
if (min[index] > max[index]) return fail("invalid-value", path, "bounds min cannot exceed max");
|
|
322
|
+
}
|
|
323
|
+
return { min, max };
|
|
324
|
+
}
|
|
325
|
+
function parseTransformTrs(value, path) {
|
|
326
|
+
const record = readRecord(value, path, ["translation", "rotation", "scale"]);
|
|
327
|
+
const translation = readNumberTuple(requireKey(record, "translation", path), `${path}.translation`, 3);
|
|
328
|
+
const rotation = readNumberTuple(requireKey(record, "rotation", path), `${path}.rotation`, 4);
|
|
329
|
+
const scale = readNumberTuple(requireKey(record, "scale", path), `${path}.scale`, 3);
|
|
330
|
+
const magnitude = Math.hypot(...rotation);
|
|
331
|
+
if (!almostEqual(magnitude, 1, 1e-4)) return fail("invalid-value", `${path}.rotation`, "rotation quaternion must be normalized");
|
|
332
|
+
return { translation, rotation, scale };
|
|
333
|
+
}
|
|
334
|
+
var blobSizeGetter = Object.getOwnPropertyDescriptor(Blob.prototype, "size").get;
|
|
335
|
+
var blobTypeGetter = Object.getOwnPropertyDescriptor(Blob.prototype, "type").get;
|
|
336
|
+
var blobSlice = Blob.prototype.slice;
|
|
337
|
+
var blobArrayBuffer = Blob.prototype.arrayBuffer;
|
|
338
|
+
function intrinsicBlobSize(blob) {
|
|
339
|
+
return Reflect.apply(blobSizeGetter, blob, []);
|
|
340
|
+
}
|
|
341
|
+
function intrinsicBlobType(blob) {
|
|
342
|
+
return Reflect.apply(blobTypeGetter, blob, []);
|
|
343
|
+
}
|
|
344
|
+
function intrinsicBlobSlice(blob, start, end, contentType) {
|
|
345
|
+
return Reflect.apply(blobSlice, blob, contentType === void 0 ? [start, end] : [start, end, contentType]);
|
|
346
|
+
}
|
|
347
|
+
function intrinsicBlobArrayBuffer(blob) {
|
|
348
|
+
return Reflect.apply(blobArrayBuffer, blob, []);
|
|
349
|
+
}
|
|
350
|
+
function parseResourceHeader(value, path) {
|
|
351
|
+
const record = readRecord(value, path, ["id", "kind", "contentHash", "byteLength", "mimeType", "payload"]);
|
|
352
|
+
const kind = readEnum(requireKey(record, "kind", path), `${path}.kind`, ["buffer", "image"]);
|
|
353
|
+
const mimeType = readString(requireKey(record, "mimeType", path), `${path}.mimeType`, 128, MIME_TYPE).toLowerCase();
|
|
354
|
+
if (kind === "image" ? !IMAGE_MIME_TYPES.has(mimeType) : !BUFFER_MIME_TYPES.has(mimeType)) {
|
|
355
|
+
return fail("invalid-value", `${path}.mimeType`, `unsupported ${kind} MIME type`);
|
|
356
|
+
}
|
|
357
|
+
const payload = requireKey(record, "payload", path);
|
|
358
|
+
if (!(payload instanceof Blob)) return fail("invalid-type", `${path}.payload`, "expected an immutable Blob payload");
|
|
359
|
+
if (Object.getPrototypeOf(payload) !== Blob.prototype || Reflect.ownKeys(payload).some((key) => typeof key === "string")) {
|
|
360
|
+
return fail("invalid-type", `${path}.payload`, "expected a native Blob without caller-defined properties");
|
|
361
|
+
}
|
|
362
|
+
const byteLength = readSafeInteger(requireKey(record, "byteLength", path), `${path}.byteLength`, 1);
|
|
363
|
+
if (intrinsicBlobSize(payload) !== byteLength) return fail("payload-length-mismatch", `${path}.payload`, "payload byte length does not match byteLength");
|
|
364
|
+
const payloadType = intrinsicBlobType(payload);
|
|
365
|
+
if (payloadType && payloadType.toLowerCase() !== mimeType) return fail("invalid-value", `${path}.payload`, "payload MIME type does not match mimeType");
|
|
366
|
+
return {
|
|
367
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
368
|
+
kind,
|
|
369
|
+
contentHash: readSha256(requireKey(record, "contentHash", path), `${path}.contentHash`),
|
|
370
|
+
byteLength,
|
|
371
|
+
mimeType,
|
|
372
|
+
payload
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
function resolveVerificationLimits(options) {
|
|
376
|
+
const overrides = options?.limits;
|
|
377
|
+
if (overrides === void 0) return GPU_MODEL_RESOURCE_VERIFICATION_LIMITS;
|
|
378
|
+
const record = readRecord(overrides, "$.verification.limits", Object.keys(GPU_MODEL_RESOURCE_VERIFICATION_LIMITS));
|
|
379
|
+
const output = { ...GPU_MODEL_RESOURCE_VERIFICATION_LIMITS };
|
|
380
|
+
for (const [key, ceiling] of Object.entries(GPU_MODEL_RESOURCE_VERIFICATION_LIMITS)) {
|
|
381
|
+
if (!Object.hasOwn(record, key)) continue;
|
|
382
|
+
const value = readSafeInteger(record[key], `$.verification.limits.${key}`, 1, ceiling);
|
|
383
|
+
Object.assign(output, { [key]: value });
|
|
384
|
+
}
|
|
385
|
+
if (output.maxImageBytes > output.maxResourceBytes) output.maxImageBytes = output.maxResourceBytes;
|
|
386
|
+
if (output.maxAggregateImageBytes > output.maxAggregateBytes) output.maxAggregateImageBytes = output.maxAggregateBytes;
|
|
387
|
+
return Object.freeze(output);
|
|
388
|
+
}
|
|
389
|
+
function preflightResourceBudget(resources, limits) {
|
|
390
|
+
let aggregate = 0;
|
|
391
|
+
let aggregateImages = 0;
|
|
392
|
+
for (let index = 0; index < resources.length; index += 1) {
|
|
393
|
+
const resource = resources[index];
|
|
394
|
+
const path = `$.resources[${index}]`;
|
|
395
|
+
if (resource.byteLength > limits.maxResourceBytes) return fail("resource-budget-exceeded", `${path}.byteLength`, "resource byte budget exceeded");
|
|
396
|
+
if (resource.kind === "image" && resource.byteLength > limits.maxImageBytes) {
|
|
397
|
+
return fail("resource-budget-exceeded", `${path}.byteLength`, "image resource byte budget exceeded");
|
|
398
|
+
}
|
|
399
|
+
aggregate += resource.byteLength;
|
|
400
|
+
if (!Number.isSafeInteger(aggregate) || aggregate > limits.maxAggregateBytes) {
|
|
401
|
+
return fail("resource-budget-exceeded", "$.resources", "aggregate resource byte budget exceeded");
|
|
402
|
+
}
|
|
403
|
+
if (resource.kind === "image") {
|
|
404
|
+
aggregateImages += resource.byteLength;
|
|
405
|
+
if (!Number.isSafeInteger(aggregateImages) || aggregateImages > limits.maxAggregateImageBytes) {
|
|
406
|
+
return fail("resource-budget-exceeded", "$.resources", "aggregate image byte budget exceeded");
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function assertVerifiedResourceEvidence(resource, limits, path) {
|
|
412
|
+
const evidence = verifiedResourceEvidence.get(resource);
|
|
413
|
+
if (!evidence) return fail("resource-unverified", path, "verified resource inspection evidence is unavailable");
|
|
414
|
+
if (resource.kind !== "image") return;
|
|
415
|
+
const width = evidence.width;
|
|
416
|
+
const height = evidence.height;
|
|
417
|
+
if (width === void 0 || height === void 0) {
|
|
418
|
+
return fail("resource-verification-failed", path, "verified image inspection evidence is incomplete");
|
|
419
|
+
}
|
|
420
|
+
if (width > limits.maxImageDimension || height > limits.maxImageDimension || width * height > limits.maxImagePixels) {
|
|
421
|
+
return fail("resource-budget-exceeded", path, "image dimension budget exceeded");
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
function cachedResourceWithinLimits(resource, limits, path) {
|
|
425
|
+
preflightResourceBudget([resource], limits);
|
|
426
|
+
assertVerifiedResourceEvidence(resource, limits, path);
|
|
427
|
+
return resource;
|
|
428
|
+
}
|
|
429
|
+
function verificationAbortError(signal, path) {
|
|
430
|
+
return signal.reason instanceof GpuModelDocumentError ? signal.reason : new GpuModelDocumentError("resource-verification-failed", path, "resource verification was aborted");
|
|
431
|
+
}
|
|
432
|
+
function throwIfVerificationAborted(signal, path) {
|
|
433
|
+
if (signal.aborted) throw verificationAbortError(signal, path);
|
|
434
|
+
}
|
|
435
|
+
function createBlobChunks(blob, chunkBytes, observed, signal, path, snapshot) {
|
|
436
|
+
return {
|
|
437
|
+
async *[Symbol.asyncIterator]() {
|
|
438
|
+
const size = intrinsicBlobSize(blob);
|
|
439
|
+
for (let offset = 0; offset < size; offset += chunkBytes) {
|
|
440
|
+
throwIfVerificationAborted(signal, path);
|
|
441
|
+
const end = Math.min(size, offset + chunkBytes);
|
|
442
|
+
const chunk = new Uint8Array(await intrinsicBlobArrayBuffer(intrinsicBlobSlice(blob, offset, end)));
|
|
443
|
+
throwIfVerificationAborted(signal, path);
|
|
444
|
+
observed.bytes += chunk.byteLength;
|
|
445
|
+
if (snapshot) snapshot.set(chunk, offset);
|
|
446
|
+
yield chunk;
|
|
447
|
+
}
|
|
448
|
+
throwIfVerificationAborted(signal, path);
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
function createVerificationDeadline(timeoutMs, callerSignal, path) {
|
|
453
|
+
const controller = new AbortController();
|
|
454
|
+
const abort = (detail) => {
|
|
455
|
+
if (!controller.signal.aborted) {
|
|
456
|
+
controller.abort(new GpuModelDocumentError("resource-verification-failed", path, detail));
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const callerAbort = () => abort("resource verification was aborted");
|
|
460
|
+
if (callerSignal?.aborted) callerAbort();
|
|
461
|
+
else callerSignal?.addEventListener("abort", callerAbort, { once: true });
|
|
462
|
+
const timeout = setTimeout(() => abort("resource verification deadline exceeded"), timeoutMs);
|
|
463
|
+
timeout.unref?.();
|
|
464
|
+
return {
|
|
465
|
+
signal: controller.signal,
|
|
466
|
+
async race(operation) {
|
|
467
|
+
throwIfVerificationAborted(controller.signal, path);
|
|
468
|
+
let abortListener;
|
|
469
|
+
const aborted = new Promise((_resolve, reject) => {
|
|
470
|
+
abortListener = () => reject(verificationAbortError(controller.signal, path));
|
|
471
|
+
controller.signal.addEventListener("abort", abortListener, { once: true });
|
|
472
|
+
if (controller.signal.aborted) abortListener();
|
|
473
|
+
});
|
|
474
|
+
try {
|
|
475
|
+
const running = Promise.resolve().then(() => {
|
|
476
|
+
throwIfVerificationAborted(controller.signal, path);
|
|
477
|
+
return operation();
|
|
478
|
+
});
|
|
479
|
+
return await Promise.race([running, aborted]);
|
|
480
|
+
} finally {
|
|
481
|
+
if (abortListener) controller.signal.removeEventListener("abort", abortListener);
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
throwIfAborted() {
|
|
485
|
+
throwIfVerificationAborted(controller.signal, path);
|
|
486
|
+
},
|
|
487
|
+
dispose() {
|
|
488
|
+
clearTimeout(timeout);
|
|
489
|
+
callerSignal?.removeEventListener("abort", callerAbort);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function parseInspection(value, path) {
|
|
494
|
+
const record = readRecord(value, path, ["valid", "detectedMimeType", "width", "height"]);
|
|
495
|
+
const output = {
|
|
496
|
+
valid: readBoolean(requireKey(record, "valid", path), `${path}.valid`),
|
|
497
|
+
detectedMimeType: readString(requireKey(record, "detectedMimeType", path), `${path}.detectedMimeType`, 128, MIME_TYPE).toLowerCase()
|
|
498
|
+
};
|
|
499
|
+
assignOptional(output, "width", readOptional(record, "width", (entry, entryPath) => readSafeInteger(entry, entryPath, 1), path));
|
|
500
|
+
assignOptional(output, "height", readOptional(record, "height", (entry, entryPath) => readSafeInteger(entry, entryPath, 1), path));
|
|
501
|
+
return output;
|
|
502
|
+
}
|
|
503
|
+
function bytesStartWith(bytes, signature) {
|
|
504
|
+
return bytes.byteLength >= signature.length && signature.every((value, index) => bytes[index] === value);
|
|
505
|
+
}
|
|
506
|
+
function validateImageMagic(resource, bytes, inspection, path) {
|
|
507
|
+
if (inspection.width === void 0 || inspection.height === void 0) {
|
|
508
|
+
return fail("resource-verification-failed", path, "image inspection must report dimensions");
|
|
509
|
+
}
|
|
510
|
+
let signatureValid = false;
|
|
511
|
+
if (resource.mimeType === "image/png") {
|
|
512
|
+
signatureValid = bytes.byteLength >= 24 && bytesStartWith(bytes, [137, 80, 78, 71, 13, 10, 26, 10]);
|
|
513
|
+
if (signatureValid) {
|
|
514
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
515
|
+
if (view.getUint32(16) !== inspection.width || view.getUint32(20) !== inspection.height) {
|
|
516
|
+
return fail("resource-verification-failed", path, "image dimensions disagree with the PNG header");
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
} else if (resource.mimeType === "image/jpeg") {
|
|
520
|
+
signatureValid = bytes.byteLength >= 4 && bytesStartWith(bytes, [255, 216, 255]) && bytes.at(-2) === 255 && bytes.at(-1) === 217;
|
|
521
|
+
} else if (resource.mimeType === "image/webp") {
|
|
522
|
+
signatureValid = bytes.byteLength >= 16 && bytesStartWith(bytes, [82, 73, 70, 70]) && String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP";
|
|
523
|
+
} else if (resource.mimeType === "image/ktx2") {
|
|
524
|
+
signatureValid = bytesStartWith(bytes, [171, 75, 84, 88, 32, 50, 48, 187, 13, 10, 26, 10]);
|
|
525
|
+
}
|
|
526
|
+
if (!signatureValid) return fail("resource-verification-failed", path, "image signature is invalid for the declared MIME type");
|
|
527
|
+
}
|
|
528
|
+
async function verifyParsedResource(resource, port, limits, options, path = "$.resource") {
|
|
529
|
+
if (options?.signal?.aborted) return fail("resource-verification-failed", path, "resource verification was aborted");
|
|
530
|
+
if (verifiedResources.has(resource) && verifiedResourceBytes.has(resource)) {
|
|
531
|
+
return cachedResourceWithinLimits(resource, limits, path);
|
|
532
|
+
}
|
|
533
|
+
preflightResourceBudget([resource], limits);
|
|
534
|
+
if (typeof port !== "object" || port === null || typeof port.inspectResource !== "function" || typeof port.digestSha256 !== "function") {
|
|
535
|
+
return fail("invalid-type", "$.verificationPort", "expected digest and inspection functions");
|
|
536
|
+
}
|
|
537
|
+
const deadline = createVerificationDeadline(limits.timeoutMs, options?.signal, path);
|
|
538
|
+
try {
|
|
539
|
+
const context = Object.freeze({
|
|
540
|
+
resourceId: resource.id,
|
|
541
|
+
kind: resource.kind,
|
|
542
|
+
declaredMimeType: resource.mimeType,
|
|
543
|
+
byteLength: resource.byteLength,
|
|
544
|
+
signal: deadline.signal
|
|
545
|
+
});
|
|
546
|
+
const inspectionObserved = { bytes: 0 };
|
|
547
|
+
let inspectionValue;
|
|
548
|
+
try {
|
|
549
|
+
inspectionValue = await deadline.race(() => port.inspectResource(
|
|
550
|
+
createBlobChunks(resource.payload, limits.chunkBytes, inspectionObserved, deadline.signal, path),
|
|
551
|
+
context
|
|
552
|
+
));
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (error instanceof GpuModelDocumentError) throw error;
|
|
555
|
+
deadline.throwIfAborted();
|
|
556
|
+
return fail("resource-verification-failed", path, "resource inspection failed");
|
|
557
|
+
}
|
|
558
|
+
deadline.throwIfAborted();
|
|
559
|
+
if (inspectionObserved.bytes !== resource.byteLength) return fail("resource-verification-failed", path, "resource inspection did not consume the complete payload");
|
|
560
|
+
const inspection = parseInspection(inspectionValue, `${path}.inspection`);
|
|
561
|
+
if (!inspection.valid) return fail("resource-verification-failed", path, "resource inspection rejected the payload");
|
|
562
|
+
if (inspection.detectedMimeType !== resource.mimeType) return fail("resource-verification-failed", path, "detected MIME type does not match the declaration");
|
|
563
|
+
if (resource.kind === "image") {
|
|
564
|
+
const width = inspection.width;
|
|
565
|
+
const height = inspection.height;
|
|
566
|
+
if (width === void 0 || height === void 0) return fail("resource-verification-failed", path, "image inspection did not report dimensions");
|
|
567
|
+
if (width > limits.maxImageDimension || height > limits.maxImageDimension || width * height > limits.maxImagePixels) {
|
|
568
|
+
return fail("resource-budget-exceeded", path, "image dimension budget exceeded");
|
|
569
|
+
}
|
|
570
|
+
} else if (inspection.width !== void 0 || inspection.height !== void 0) {
|
|
571
|
+
return fail("resource-verification-failed", path, "buffer inspection must not report image dimensions");
|
|
572
|
+
}
|
|
573
|
+
const snapshot = new Uint8Array(resource.byteLength);
|
|
574
|
+
const digestObserved = { bytes: 0 };
|
|
575
|
+
let digest;
|
|
576
|
+
try {
|
|
577
|
+
digest = await deadline.race(() => port.digestSha256(
|
|
578
|
+
createBlobChunks(resource.payload, limits.chunkBytes, digestObserved, deadline.signal, path, snapshot),
|
|
579
|
+
context
|
|
580
|
+
));
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (error instanceof GpuModelDocumentError) throw error;
|
|
583
|
+
deadline.throwIfAborted();
|
|
584
|
+
return fail("resource-verification-failed", path, "resource digest verification failed");
|
|
585
|
+
}
|
|
586
|
+
deadline.throwIfAborted();
|
|
587
|
+
if (digestObserved.bytes !== resource.byteLength) return fail("resource-verification-failed", path, "resource digest did not consume the complete payload");
|
|
588
|
+
if (typeof digest !== "string" || !SHA_256.test(digest) || digest !== resource.contentHash) {
|
|
589
|
+
return fail("resource-verification-failed", path, "resource digest does not match contentHash");
|
|
590
|
+
}
|
|
591
|
+
if (resource.kind === "image") validateImageMagic(resource, snapshot, inspection, path);
|
|
592
|
+
const verified = Object.freeze({
|
|
593
|
+
...resource,
|
|
594
|
+
payload: new Blob([snapshot], { type: resource.mimeType })
|
|
595
|
+
});
|
|
596
|
+
verifiedResources.add(verified);
|
|
597
|
+
verifiedResourceBytes.set(verified, snapshot);
|
|
598
|
+
verifiedResourceEvidence.set(verified, Object.freeze({ ...inspection }));
|
|
599
|
+
return verified;
|
|
600
|
+
} finally {
|
|
601
|
+
deadline.dispose();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
async function verifyGpuModelResource(value, port, options) {
|
|
605
|
+
try {
|
|
606
|
+
const limits = resolveVerificationLimits(options);
|
|
607
|
+
const resource = typeof value === "object" && value !== null && verifiedResources.has(value) && verifiedResourceBytes.has(value) ? value : parseResourceHeader(value, "$.resource");
|
|
608
|
+
return await verifyParsedResource(resource, port, limits, options);
|
|
609
|
+
} catch (error) {
|
|
610
|
+
if (error instanceof GpuModelDocumentError) throw error;
|
|
611
|
+
throw new GpuModelDocumentError("invalid-type", "$.resource", "resource could not be safely inspected");
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function parseNode(value, path, budget) {
|
|
615
|
+
const record = readRecord(value, path, ["id", "name", "children", "meshId", "skinId", "localMatrix", "metadata"]);
|
|
616
|
+
const children = Object.freeze(readArray(requireKey(record, "children", path), `${path}.children`, GPU_MODEL_DOCUMENT_LIMITS.nodes).map((entry, index) => readId(entry, `${path}.children[${index}]`)));
|
|
617
|
+
consumeBudget(budget, "sceneEdges", children.length, GPU_MODEL_DOCUMENT_LIMITS.sceneEdges, path, "scene edge");
|
|
618
|
+
ensureUnique(children, `${path}.children`, "duplicate child node");
|
|
619
|
+
const output = {
|
|
620
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
621
|
+
children,
|
|
622
|
+
localMatrix: parseMatrix(requireKey(record, "localMatrix", path), `${path}.localMatrix`)
|
|
623
|
+
};
|
|
624
|
+
assignOptional(output, "name", readOptional(record, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
625
|
+
assignOptional(output, "meshId", readOptional(record, "meshId", readId, path));
|
|
626
|
+
assignOptional(output, "skinId", readOptional(record, "skinId", readId, path));
|
|
627
|
+
assignOptional(output, "metadata", readOptional(record, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
628
|
+
return output;
|
|
629
|
+
}
|
|
630
|
+
function parseAccessor(value, path, budget) {
|
|
631
|
+
const record = readRecord(value, path, ["id", "resourceId", "byteOffset", "byteStride", "count", "componentType", "elementType", "normalized", "min", "max"]);
|
|
632
|
+
const componentType = readEnum(requireKey(record, "componentType", path), `${path}.componentType`, ["i8", "u8", "i16", "u16", "u32", "f32"]);
|
|
633
|
+
const elementType = readEnum(requireKey(record, "elementType", path), `${path}.elementType`, ["scalar", "vec2", "vec3", "vec4", "mat2", "mat3", "mat4"]);
|
|
634
|
+
const normalized = readOptional(record, "normalized", readBoolean, path);
|
|
635
|
+
if (normalized === true && componentType === "f32") return fail("invalid-value", `${path}.normalized`, "floating-point accessors cannot be normalized");
|
|
636
|
+
if (elementType.startsWith("mat") && componentType !== "f32") return fail("invalid-value", `${path}.componentType`, "matrix accessors must use f32 components");
|
|
637
|
+
const expectedComponents = ELEMENT_COMPONENTS[elementType];
|
|
638
|
+
const min = readOptional(record, "min", (entry, entryPath) => readNumberTuple(entry, entryPath, expectedComponents), path);
|
|
639
|
+
const max = readOptional(record, "max", (entry, entryPath) => readNumberTuple(entry, entryPath, expectedComponents), path);
|
|
640
|
+
if (min === void 0 !== (max === void 0)) return fail("invalid-value", path, "accessor min and max must be supplied together");
|
|
641
|
+
if (min && max) {
|
|
642
|
+
const [componentMinimum, componentMaximum, integer] = COMPONENT_RANGES[componentType];
|
|
643
|
+
for (let index = 0; index < expectedComponents; index += 1) {
|
|
644
|
+
const minimum = min[index];
|
|
645
|
+
const maximum = max[index];
|
|
646
|
+
if (minimum > maximum) return fail("invalid-value", path, "accessor min cannot exceed max");
|
|
647
|
+
if (minimum < componentMinimum || maximum > componentMaximum || integer && (!Number.isInteger(minimum) || !Number.isInteger(maximum))) {
|
|
648
|
+
return fail("invalid-value", path, "accessor bounds are outside the component representation");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const count = readSafeInteger(requireKey(record, "count", path), `${path}.count`, 1, GPU_MODEL_DOCUMENT_LIMITS.weightVertices);
|
|
653
|
+
consumeBudget(budget, "accessorElements", count * expectedComponents, GPU_MODEL_DOCUMENT_LIMITS.accessorElements, path, "accessor element component");
|
|
654
|
+
const output = {
|
|
655
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
656
|
+
resourceId: readId(requireKey(record, "resourceId", path), `${path}.resourceId`),
|
|
657
|
+
byteOffset: readSafeInteger(requireKey(record, "byteOffset", path), `${path}.byteOffset`),
|
|
658
|
+
count,
|
|
659
|
+
componentType,
|
|
660
|
+
elementType
|
|
661
|
+
};
|
|
662
|
+
assignOptional(output, "byteStride", readOptional(record, "byteStride", (entry, entryPath) => readSafeInteger(entry, entryPath, 1, 2048), path));
|
|
663
|
+
assignOptional(output, "normalized", normalized);
|
|
664
|
+
assignOptional(output, "min", min);
|
|
665
|
+
assignOptional(output, "max", max);
|
|
666
|
+
return output;
|
|
667
|
+
}
|
|
668
|
+
function parsePrimitive(value, path, budget) {
|
|
669
|
+
const record = readRecord(value, path, ["id", "topology", "attributes", "indicesAccessorId", "materialId", "metadata"]);
|
|
670
|
+
const attributesInput = readArray(requireKey(record, "attributes", path), `${path}.attributes`, 64, 1);
|
|
671
|
+
consumeBudget(budget, "primitiveAttributes", attributesInput.length, GPU_MODEL_DOCUMENT_LIMITS.primitiveAttributes, path, "primitive attribute");
|
|
672
|
+
const attributes = Object.freeze(attributesInput.map((entry, index) => {
|
|
673
|
+
const entryPath = `${path}.attributes[${index}]`;
|
|
674
|
+
const attribute = readRecord(entry, entryPath, ["semantic", "accessorId"]);
|
|
675
|
+
return {
|
|
676
|
+
semantic: readString(requireKey(attribute, "semantic", entryPath), `${entryPath}.semantic`, 64, ATTRIBUTE_SEMANTIC),
|
|
677
|
+
accessorId: readId(requireKey(attribute, "accessorId", entryPath), `${entryPath}.accessorId`)
|
|
678
|
+
};
|
|
679
|
+
}));
|
|
680
|
+
ensureUnique(attributes.map(({ semantic }) => semantic), `${path}.attributes`, "duplicate attribute semantic");
|
|
681
|
+
if (!attributes.some(({ semantic }) => semantic === "POSITION")) return fail("invalid-value", `${path}.attributes`, "primitive requires POSITION");
|
|
682
|
+
const output = {
|
|
683
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
684
|
+
topology: readEnum(requireKey(record, "topology", path), `${path}.topology`, ["points", "lines", "line-strip", "triangles", "triangle-strip"]),
|
|
685
|
+
attributes
|
|
686
|
+
};
|
|
687
|
+
assignOptional(output, "indicesAccessorId", readOptional(record, "indicesAccessorId", readId, path));
|
|
688
|
+
assignOptional(output, "materialId", readOptional(record, "materialId", readId, path));
|
|
689
|
+
assignOptional(output, "metadata", readOptional(record, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
690
|
+
return output;
|
|
691
|
+
}
|
|
692
|
+
function parseMesh(value, path, budget) {
|
|
693
|
+
const record = readRecord(value, path, ["id", "name", "primitives", "metadata"]);
|
|
694
|
+
const input = readArray(requireKey(record, "primitives", path), `${path}.primitives`, GPU_MODEL_DOCUMENT_LIMITS.primitives, 1);
|
|
695
|
+
consumeBudget(budget, "primitives", input.length, GPU_MODEL_DOCUMENT_LIMITS.primitives, path, "primitive");
|
|
696
|
+
const output = {
|
|
697
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
698
|
+
primitives: Object.freeze(input.map((entry, index) => parsePrimitive(entry, `${path}.primitives[${index}]`, budget)))
|
|
699
|
+
};
|
|
700
|
+
assignOptional(output, "name", readOptional(record, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
701
|
+
assignOptional(output, "metadata", readOptional(record, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
702
|
+
return output;
|
|
703
|
+
}
|
|
704
|
+
function parseTextureTransform(value, path) {
|
|
705
|
+
const record = readRecord(value, path, ["offset", "rotationRadians", "scale"]);
|
|
706
|
+
if (Reflect.ownKeys(record).length === 0) return fail("invalid-value", path, "texture transform cannot be empty");
|
|
707
|
+
const output = {};
|
|
708
|
+
assignOptional(output, "offset", readOptional(record, "offset", (entry, entryPath) => readNumberTuple(entry, entryPath, 2), path));
|
|
709
|
+
assignOptional(output, "rotationRadians", readOptional(record, "rotationRadians", readFinite, path));
|
|
710
|
+
assignOptional(output, "scale", readOptional(record, "scale", (entry, entryPath) => readNumberTuple(entry, entryPath, 2), path));
|
|
711
|
+
return output;
|
|
712
|
+
}
|
|
713
|
+
function parseTextureBinding(value, path) {
|
|
714
|
+
const record = readRecord(value, path, ["textureId", "samplerId", "texCoordSet", "transform", "colorSpace", "intendedUsage"]);
|
|
715
|
+
const output = {
|
|
716
|
+
textureId: readId(requireKey(record, "textureId", path), `${path}.textureId`),
|
|
717
|
+
colorSpace: readEnum(requireKey(record, "colorSpace", path), `${path}.colorSpace`, ["srgb", "linear", "normal-map", "data"]),
|
|
718
|
+
intendedUsage: readEnum(requireKey(record, "intendedUsage", path), `${path}.intendedUsage`, TEXTURE_USAGES)
|
|
719
|
+
};
|
|
720
|
+
assignOptional(output, "samplerId", readOptional(record, "samplerId", readId, path));
|
|
721
|
+
assignOptional(output, "texCoordSet", readOptional(record, "texCoordSet", (entry, entryPath) => readSafeInteger(entry, entryPath, 0, 7), path));
|
|
722
|
+
assignOptional(output, "transform", readOptional(record, "transform", parseTextureTransform, path));
|
|
723
|
+
return output;
|
|
724
|
+
}
|
|
725
|
+
function parseMaterialTextures(value, path) {
|
|
726
|
+
const slots = Object.keys(MATERIAL_TEXTURE_SLOTS);
|
|
727
|
+
const record = readRecord(value, path, slots);
|
|
728
|
+
const output = {};
|
|
729
|
+
for (const slot of slots) assignOptional(output, slot, readOptional(record, slot, parseTextureBinding, path));
|
|
730
|
+
return output;
|
|
731
|
+
}
|
|
732
|
+
function parseMaterial(value, path, budget) {
|
|
733
|
+
const keys = [
|
|
734
|
+
"id",
|
|
735
|
+
"name",
|
|
736
|
+
"workflow",
|
|
737
|
+
"alphaMode",
|
|
738
|
+
"alphaCutoff",
|
|
739
|
+
"doubleSided",
|
|
740
|
+
"baseColorFactor",
|
|
741
|
+
"metallicFactor",
|
|
742
|
+
"roughnessFactor",
|
|
743
|
+
"emissiveFactor",
|
|
744
|
+
"transmissionFactor",
|
|
745
|
+
"clearcoatFactor",
|
|
746
|
+
"clearcoatRoughnessFactor",
|
|
747
|
+
"sheenColorFactor",
|
|
748
|
+
"sheenRoughnessFactor",
|
|
749
|
+
"normalScale",
|
|
750
|
+
"occlusionStrength",
|
|
751
|
+
"textures",
|
|
752
|
+
"extensions",
|
|
753
|
+
"sourceMetadata"
|
|
754
|
+
];
|
|
755
|
+
const record = readRecord(value, path, keys);
|
|
756
|
+
const alphaMode = readEnum(requireKey(record, "alphaMode", path), `${path}.alphaMode`, ["opaque", "mask", "blend"]);
|
|
757
|
+
const alphaCutoff = readOptional(record, "alphaCutoff", (entry, entryPath) => readFinite(entry, entryPath, 0, 1), path);
|
|
758
|
+
if (alphaMode === "mask" && alphaCutoff === void 0) return fail("invalid-value", `${path}.alphaCutoff`, "mask materials require alphaCutoff");
|
|
759
|
+
if (alphaMode !== "mask" && alphaCutoff !== void 0) return fail("invalid-value", `${path}.alphaCutoff`, "alphaCutoff is only valid for mask materials");
|
|
760
|
+
const output = {
|
|
761
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
762
|
+
workflow: readEnum(requireKey(record, "workflow", path), `${path}.workflow`, ["metallic-roughness", "specular-glossiness", "unlit", "custom"]),
|
|
763
|
+
alphaMode,
|
|
764
|
+
baseColorFactor: readNumberTuple(requireKey(record, "baseColorFactor", path), `${path}.baseColorFactor`, 4, 0, 1),
|
|
765
|
+
textures: parseMaterialTextures(requireKey(record, "textures", path), `${path}.textures`),
|
|
766
|
+
extensions: readMetadata(requireKey(record, "extensions", path), `${path}.extensions`, budget),
|
|
767
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
768
|
+
};
|
|
769
|
+
assignOptional(output, "name", readOptional(record, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
770
|
+
assignOptional(output, "alphaCutoff", alphaCutoff);
|
|
771
|
+
assignOptional(output, "doubleSided", readOptional(record, "doubleSided", readBoolean, path));
|
|
772
|
+
for (const key of ["metallicFactor", "roughnessFactor", "transmissionFactor", "clearcoatFactor", "clearcoatRoughnessFactor", "sheenRoughnessFactor", "occlusionStrength"]) {
|
|
773
|
+
assignOptional(output, key, readOptional(record, key, (entry, entryPath) => readFinite(entry, entryPath, 0, 1), path));
|
|
774
|
+
}
|
|
775
|
+
assignOptional(output, "normalScale", readOptional(record, "normalScale", readFinite, path));
|
|
776
|
+
assignOptional(output, "emissiveFactor", readOptional(record, "emissiveFactor", (entry, entryPath) => readNumberTuple(entry, entryPath, 3, 0), path));
|
|
777
|
+
assignOptional(output, "sheenColorFactor", readOptional(record, "sheenColorFactor", (entry, entryPath) => readNumberTuple(entry, entryPath, 3, 0, 1), path));
|
|
778
|
+
return output;
|
|
779
|
+
}
|
|
780
|
+
function parseTextureSampler(value, path) {
|
|
781
|
+
const record = readRecord(value, path, ["id", "wrapS", "wrapT", "minFilter", "magFilter", "mipFilter"]);
|
|
782
|
+
const output = { id: readId(requireKey(record, "id", path), `${path}.id`) };
|
|
783
|
+
assignOptional(output, "wrapS", readOptional(record, "wrapS", (entry, entryPath) => readEnum(entry, entryPath, ["repeat", "mirrored-repeat", "clamp-to-edge"]), path));
|
|
784
|
+
assignOptional(output, "wrapT", readOptional(record, "wrapT", (entry, entryPath) => readEnum(entry, entryPath, ["repeat", "mirrored-repeat", "clamp-to-edge"]), path));
|
|
785
|
+
assignOptional(output, "minFilter", readOptional(record, "minFilter", (entry, entryPath) => readString(entry, entryPath, 128, TOKEN), path));
|
|
786
|
+
assignOptional(output, "magFilter", readOptional(record, "magFilter", (entry, entryPath) => readString(entry, entryPath, 128, TOKEN), path));
|
|
787
|
+
assignOptional(output, "mipFilter", readOptional(record, "mipFilter", (entry, entryPath) => readEnum(entry, entryPath, ["nearest", "linear"]), path));
|
|
788
|
+
return output;
|
|
789
|
+
}
|
|
790
|
+
function parseTexture(value, path, budget) {
|
|
791
|
+
const record = readRecord(value, path, ["id", "fileName", "relativePath", "mimeType", "contentHash", "imageResourceId", "byteLength", "intendedUsage", "colorSpace", "sampler", "exportEmbeddingPolicy", "sourceMetadata"]);
|
|
792
|
+
const usages = Object.freeze(readArray(requireKey(record, "intendedUsage", path), `${path}.intendedUsage`, TEXTURE_USAGES.length, 1).map((entry, index) => readEnum(entry, `${path}.intendedUsage[${index}]`, TEXTURE_USAGES)));
|
|
793
|
+
ensureUnique(usages, `${path}.intendedUsage`, "duplicate intended texture usage");
|
|
794
|
+
const output = {
|
|
795
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
796
|
+
contentHash: readSha256(requireKey(record, "contentHash", path), `${path}.contentHash`),
|
|
797
|
+
imageResourceId: readId(requireKey(record, "imageResourceId", path), `${path}.imageResourceId`),
|
|
798
|
+
intendedUsage: usages,
|
|
799
|
+
colorSpace: readEnum(requireKey(record, "colorSpace", path), `${path}.colorSpace`, ["srgb", "linear", "normal-map", "data"]),
|
|
800
|
+
exportEmbeddingPolicy: readEnum(requireKey(record, "exportEmbeddingPolicy", path), `${path}.exportEmbeddingPolicy`, ["inherit-source", "prefer-embedded", "prefer-external", "forbid-embedding"]),
|
|
801
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
802
|
+
};
|
|
803
|
+
assignOptional(output, "fileName", readOptional(record, "fileName", (entry, entryPath) => readString(entry, entryPath, 512, FILE_NAME), path));
|
|
804
|
+
assignOptional(output, "relativePath", readOptional(record, "relativePath", (entry, entryPath) => readString(entry, entryPath, 2048, RELATIVE_PATH), path));
|
|
805
|
+
assignOptional(output, "mimeType", readOptional(record, "mimeType", (entry, entryPath) => readString(entry, entryPath, 128, MIME_TYPE).toLowerCase(), path));
|
|
806
|
+
assignOptional(output, "byteLength", readOptional(record, "byteLength", (entry, entryPath) => readSafeInteger(entry, entryPath, 1), path));
|
|
807
|
+
assignOptional(output, "sampler", readOptional(record, "sampler", parseTextureSampler, path));
|
|
808
|
+
return output;
|
|
809
|
+
}
|
|
810
|
+
function parseSkeleton(value, path, budget) {
|
|
811
|
+
const record = readRecord(value, path, ["id", "name", "jointIds", "rootJointIds", "sourceMetadata"]);
|
|
812
|
+
const jointIds = Object.freeze(readArray(requireKey(record, "jointIds", path), `${path}.jointIds`, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, 1).map((entry, index) => readId(entry, `${path}.jointIds[${index}]`)));
|
|
813
|
+
const rootJointIds = Object.freeze(readArray(requireKey(record, "rootJointIds", path), `${path}.rootJointIds`, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, 1).map((entry, index) => readId(entry, `${path}.rootJointIds[${index}]`)));
|
|
814
|
+
consumeBudget(budget, "skinJoints", jointIds.length + rootJointIds.length, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, path, "rig joint");
|
|
815
|
+
ensureUnique(jointIds, `${path}.jointIds`, "duplicate skeleton joint");
|
|
816
|
+
ensureUnique(rootJointIds, `${path}.rootJointIds`, "duplicate root joint");
|
|
817
|
+
const output = {
|
|
818
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
819
|
+
jointIds,
|
|
820
|
+
rootJointIds,
|
|
821
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
822
|
+
};
|
|
823
|
+
assignOptional(output, "name", readOptional(record, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
824
|
+
return output;
|
|
825
|
+
}
|
|
826
|
+
function parseJoint(value, path, budget) {
|
|
827
|
+
const record = readRecord(value, path, ["id", "name", "nodeId", "parentJointId", "restLocalTransform", "inverseBindMatrix", "sourceMetadata"]);
|
|
828
|
+
const output = {
|
|
829
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
830
|
+
nodeId: readId(requireKey(record, "nodeId", path), `${path}.nodeId`),
|
|
831
|
+
restLocalTransform: parseTransformTrs(requireKey(record, "restLocalTransform", path), `${path}.restLocalTransform`),
|
|
832
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
833
|
+
};
|
|
834
|
+
assignOptional(output, "name", readOptional(record, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
835
|
+
assignOptional(output, "parentJointId", readOptional(record, "parentJointId", readId, path));
|
|
836
|
+
assignOptional(output, "inverseBindMatrix", readOptional(record, "inverseBindMatrix", parseMatrix, path));
|
|
837
|
+
return output;
|
|
838
|
+
}
|
|
839
|
+
function parseWeightRows(value, path, rows, columns, integer, budget) {
|
|
840
|
+
const input = readArray(value, path, rows, rows);
|
|
841
|
+
consumeBudget(budget, "weightVertices", input.length, GPU_MODEL_DOCUMENT_LIMITS.weightVertices, path, "weight vertex");
|
|
842
|
+
consumeBudget(budget, "weightInfluences", input.length * columns, GPU_MODEL_DOCUMENT_LIMITS.weightInfluences, path, "weight influence");
|
|
843
|
+
return Object.freeze(input.map((row, rowIndex) => {
|
|
844
|
+
const parsed = readNumberTuple(row, `${path}[${rowIndex}]`, columns, 0, integer ? Number.MAX_SAFE_INTEGER : 1);
|
|
845
|
+
if (integer && parsed.some((entry) => !Number.isSafeInteger(entry))) return fail("invalid-value", `${path}[${rowIndex}]`, "joint indices must be safe integers");
|
|
846
|
+
return parsed;
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
function parseVertexWeightSet(value, path, budget) {
|
|
850
|
+
const record = readRecord(value, path, ["primitiveId", "influencesPerVertex", "joints", "weights"]);
|
|
851
|
+
const influences = readSafeInteger(requireKey(record, "influencesPerVertex", path), `${path}.influencesPerVertex`, 1, 32);
|
|
852
|
+
if (influences % 4 !== 0) return fail("invalid-value", `${path}.influencesPerVertex`, "influencesPerVertex must be a multiple of four");
|
|
853
|
+
const jointRows = readArray(requireKey(record, "joints", path), `${path}.joints`, GPU_MODEL_DOCUMENT_LIMITS.weightVertices, 1);
|
|
854
|
+
const weightRows = readArray(requireKey(record, "weights", path), `${path}.weights`, GPU_MODEL_DOCUMENT_LIMITS.weightVertices, 1);
|
|
855
|
+
if (jointRows.length !== weightRows.length) return fail("invalid-value", path, "joint and weight row counts must match");
|
|
856
|
+
return {
|
|
857
|
+
primitiveId: readId(requireKey(record, "primitiveId", path), `${path}.primitiveId`),
|
|
858
|
+
influencesPerVertex: influences,
|
|
859
|
+
joints: parseWeightRows(jointRows, `${path}.joints`, jointRows.length, influences, true, budget),
|
|
860
|
+
weights: parseWeightRows(weightRows, `${path}.weights`, weightRows.length, influences, false, budget)
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
function parseSkin(value, path, budget) {
|
|
864
|
+
const record = readRecord(value, path, ["id", "skeletonId", "jointIds", "inverseBindMatrices", "weightSets", "sourceMetadata"]);
|
|
865
|
+
const jointIds = Object.freeze(readArray(requireKey(record, "jointIds", path), `${path}.jointIds`, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, 1).map((entry, index) => readId(entry, `${path}.jointIds[${index}]`)));
|
|
866
|
+
consumeBudget(budget, "skinJoints", jointIds.length, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, path, "skin joint");
|
|
867
|
+
ensureUnique(jointIds, `${path}.jointIds`, "duplicate skin joint");
|
|
868
|
+
const inverseInput = readArray(requireKey(record, "inverseBindMatrices", path), `${path}.inverseBindMatrices`, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, jointIds.length);
|
|
869
|
+
if (inverseInput.length !== jointIds.length) return fail("invalid-value", `${path}.inverseBindMatrices`, "one inverse bind matrix is required per skin joint");
|
|
870
|
+
const weightInput = readArray(requireKey(record, "weightSets", path), `${path}.weightSets`, GPU_MODEL_DOCUMENT_LIMITS.primitives, 1);
|
|
871
|
+
const weightSets = Object.freeze(weightInput.map((entry, index) => parseVertexWeightSet(entry, `${path}.weightSets[${index}]`, budget)));
|
|
872
|
+
ensureUnique(weightSets.map(({ primitiveId }) => primitiveId), `${path}.weightSets`, "duplicate primitive weight set");
|
|
873
|
+
return {
|
|
874
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
875
|
+
skeletonId: readId(requireKey(record, "skeletonId", path), `${path}.skeletonId`),
|
|
876
|
+
jointIds,
|
|
877
|
+
inverseBindMatrices: Object.freeze(inverseInput.map((entry, index) => parseMatrix(entry, `${path}.inverseBindMatrices[${index}]`))),
|
|
878
|
+
weightSets,
|
|
879
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
function parseBlendShapeDelta(value, path) {
|
|
883
|
+
const record = readRecord(value, path, ["primitiveId", "positionAccessorId", "normalAccessorId", "tangentAccessorId"]);
|
|
884
|
+
const output = { primitiveId: readId(requireKey(record, "primitiveId", path), `${path}.primitiveId`) };
|
|
885
|
+
for (const key of ["positionAccessorId", "normalAccessorId", "tangentAccessorId"]) {
|
|
886
|
+
assignOptional(output, key, readOptional(record, key, readId, path));
|
|
887
|
+
}
|
|
888
|
+
if (!output.positionAccessorId && !output.normalAccessorId && !output.tangentAccessorId) {
|
|
889
|
+
return fail("invalid-value", path, "blend-shape delta requires at least one accessor");
|
|
890
|
+
}
|
|
891
|
+
return output;
|
|
892
|
+
}
|
|
893
|
+
function parseBlendShape(value, path, budget) {
|
|
894
|
+
const record = readRecord(value, path, ["id", "meshId", "name", "defaultWeight", "deltas", "sourceMetadata"]);
|
|
895
|
+
const deltaInput = readArray(requireKey(record, "deltas", path), `${path}.deltas`, GPU_MODEL_DOCUMENT_LIMITS.primitives, 1);
|
|
896
|
+
consumeBudget(
|
|
897
|
+
budget,
|
|
898
|
+
"blendShapeDeltas",
|
|
899
|
+
deltaInput.length,
|
|
900
|
+
GPU_MODEL_DOCUMENT_LIMITS.blendShapeDeltas,
|
|
901
|
+
`${path}.deltas`,
|
|
902
|
+
"blend-shape delta"
|
|
903
|
+
);
|
|
904
|
+
const deltas = Object.freeze(deltaInput.map((entry, index) => parseBlendShapeDelta(entry, `${path}.deltas[${index}]`)));
|
|
905
|
+
ensureUnique(deltas.map(({ primitiveId }) => primitiveId), `${path}.deltas`, "duplicate blend-shape primitive delta");
|
|
906
|
+
const output = {
|
|
907
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
908
|
+
meshId: readId(requireKey(record, "meshId", path), `${path}.meshId`),
|
|
909
|
+
deltas,
|
|
910
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
911
|
+
};
|
|
912
|
+
assignOptional(output, "name", readOptional(record, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
913
|
+
assignOptional(output, "defaultWeight", readOptional(record, "defaultWeight", (entry, entryPath) => readFinite(entry, entryPath, 0, 1), path));
|
|
914
|
+
return output;
|
|
915
|
+
}
|
|
916
|
+
function parseAnimationSampler(value, path, budget) {
|
|
917
|
+
const record = readRecord(value, path, ["id", "inputAccessorId", "outputAccessorId", "inputTimesSeconds", "outputValues", "interpolation", "sourceMetadata"]);
|
|
918
|
+
const timesInput = readArray(requireKey(record, "inputTimesSeconds", path), `${path}.inputTimesSeconds`, GPU_MODEL_DOCUMENT_LIMITS.animationValues, 1);
|
|
919
|
+
const valuesInput = readArray(requireKey(record, "outputValues", path), `${path}.outputValues`, GPU_MODEL_DOCUMENT_LIMITS.animationValues, 1);
|
|
920
|
+
consumeBudget(budget, "animationValues", timesInput.length + valuesInput.length, GPU_MODEL_DOCUMENT_LIMITS.animationValues, path, "animation value");
|
|
921
|
+
const times = Object.freeze(timesInput.map((entry, index) => readFinite(entry, `${path}.inputTimesSeconds[${index}]`, 0)));
|
|
922
|
+
for (let index = 1; index < times.length; index += 1) {
|
|
923
|
+
if (times[index] <= times[index - 1]) return fail("invalid-value", `${path}.inputTimesSeconds`, "animation input times must be strictly increasing");
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
927
|
+
inputAccessorId: readId(requireKey(record, "inputAccessorId", path), `${path}.inputAccessorId`),
|
|
928
|
+
outputAccessorId: readId(requireKey(record, "outputAccessorId", path), `${path}.outputAccessorId`),
|
|
929
|
+
inputTimesSeconds: times,
|
|
930
|
+
outputValues: Object.freeze(valuesInput.map((entry, index) => readFinite(entry, `${path}.outputValues[${index}]`))),
|
|
931
|
+
interpolation: readEnum(requireKey(record, "interpolation", path), `${path}.interpolation`, ["step", "linear", "cubic-spline"]),
|
|
932
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
function parseAnimationChannel(value, path, budget) {
|
|
936
|
+
const record = readRecord(value, path, ["id", "samplerId", "targetType", "targetId", "sourceMetadata"]);
|
|
937
|
+
return {
|
|
938
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
939
|
+
samplerId: readId(requireKey(record, "samplerId", path), `${path}.samplerId`),
|
|
940
|
+
targetType: readEnum(requireKey(record, "targetType", path), `${path}.targetType`, ["node-translation", "node-rotation", "node-scale", "weights"]),
|
|
941
|
+
targetId: readId(requireKey(record, "targetId", path), `${path}.targetId`),
|
|
942
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
function parseAnimation(value, path, budget) {
|
|
946
|
+
const record = readRecord(value, path, ["id", "name", "durationSeconds", "samplers", "channels", "sourceMetadata"]);
|
|
947
|
+
const samplerInput = readArray(requireKey(record, "samplers", path), `${path}.samplers`, GPU_MODEL_DOCUMENT_LIMITS.animationSamplers, 1);
|
|
948
|
+
const channelInput = readArray(requireKey(record, "channels", path), `${path}.channels`, GPU_MODEL_DOCUMENT_LIMITS.animationChannels, 1);
|
|
949
|
+
consumeBudget(budget, "animationSamplers", samplerInput.length, GPU_MODEL_DOCUMENT_LIMITS.animationSamplers, path, "animation sampler");
|
|
950
|
+
consumeBudget(budget, "animationChannels", channelInput.length, GPU_MODEL_DOCUMENT_LIMITS.animationChannels, path, "animation channel");
|
|
951
|
+
const output = {
|
|
952
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
953
|
+
durationSeconds: readFinite(requireKey(record, "durationSeconds", path), `${path}.durationSeconds`, 0),
|
|
954
|
+
samplers: Object.freeze(samplerInput.map((entry, index) => parseAnimationSampler(entry, `${path}.samplers[${index}]`, budget))),
|
|
955
|
+
channels: Object.freeze(channelInput.map((entry, index) => parseAnimationChannel(entry, `${path}.channels[${index}]`, budget))),
|
|
956
|
+
sourceMetadata: readMetadata(requireKey(record, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
957
|
+
};
|
|
958
|
+
assignOptional(output, "name", readOptional(record, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
959
|
+
return output;
|
|
960
|
+
}
|
|
961
|
+
function parseAnalyticGeometry(value, path, budget) {
|
|
962
|
+
const record = readRecord(value, path, ["id", "kind", "sourceResourceId", "sourceUnit", "tessellatedMeshId", "metadata"]);
|
|
963
|
+
if (requireKey(record, "kind", path) !== "brep-placeholder") return fail("invalid-value", `${path}.kind`, "unsupported analytic geometry kind");
|
|
964
|
+
const output = {
|
|
965
|
+
id: readId(requireKey(record, "id", path), `${path}.id`),
|
|
966
|
+
kind: "brep-placeholder",
|
|
967
|
+
sourceResourceId: readId(requireKey(record, "sourceResourceId", path), `${path}.sourceResourceId`),
|
|
968
|
+
sourceUnit: readEnum(requireKey(record, "sourceUnit", path), `${path}.sourceUnit`, ["metre", "millimetre", "centimetre", "inch", "foot", "unitless"])
|
|
969
|
+
};
|
|
970
|
+
assignOptional(output, "tessellatedMeshId", readOptional(record, "tessellatedMeshId", readId, path));
|
|
971
|
+
assignOptional(output, "metadata", readOptional(record, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
972
|
+
return output;
|
|
973
|
+
}
|
|
974
|
+
function parseProvenance(value, budget) {
|
|
975
|
+
const path = "$.provenance";
|
|
976
|
+
const record = readRecord(value, path, ["sourceFormat", "sourceContentHash", "converterId", "converterVersion", "provider", "sourceIdentifier", "metadata"]);
|
|
977
|
+
const output = {
|
|
978
|
+
sourceFormat: readString(requireKey(record, "sourceFormat", path), `${path}.sourceFormat`, 128, TOKEN),
|
|
979
|
+
sourceContentHash: readSha256(requireKey(record, "sourceContentHash", path), `${path}.sourceContentHash`),
|
|
980
|
+
converterId: readString(requireKey(record, "converterId", path), `${path}.converterId`, 128, TOKEN),
|
|
981
|
+
converterVersion: readString(requireKey(record, "converterVersion", path), `${path}.converterVersion`, 128, TOKEN)
|
|
982
|
+
};
|
|
983
|
+
assignOptional(output, "provider", readOptional(record, "provider", (entry, entryPath) => readString(entry, entryPath, 128, TOKEN), path));
|
|
984
|
+
assignOptional(output, "sourceIdentifier", readOptional(record, "sourceIdentifier", (entry, entryPath) => readString(entry, entryPath, 2048), path));
|
|
985
|
+
assignOptional(output, "metadata", readOptional(record, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
986
|
+
return output;
|
|
987
|
+
}
|
|
988
|
+
function parseDiagnostic(value, path, budget) {
|
|
989
|
+
const record = readRecord(value, path, ["severity", "code", "message", "path", "metadata"]);
|
|
990
|
+
const output = {
|
|
991
|
+
severity: readEnum(requireKey(record, "severity", path), `${path}.severity`, ["info", "warning", "error"]),
|
|
992
|
+
code: readString(requireKey(record, "code", path), `${path}.code`, 128, TOKEN),
|
|
993
|
+
message: readString(requireKey(record, "message", path), `${path}.message`, GPU_MODEL_DOCUMENT_LIMITS.diagnosticMessageLength)
|
|
994
|
+
};
|
|
995
|
+
assignOptional(output, "path", readOptional(record, "path", (entry, entryPath) => readString(entry, entryPath, 512), path));
|
|
996
|
+
assignOptional(output, "metadata", readOptional(record, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
997
|
+
return output;
|
|
998
|
+
}
|
|
999
|
+
function ensureUnique(values, path, detail = "duplicate identity") {
|
|
1000
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1001
|
+
for (const value of values) {
|
|
1002
|
+
if (seen.has(value)) return fail("duplicate-id", path, `${detail}: ${value}`);
|
|
1003
|
+
seen.add(value);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
function indexById(values, path) {
|
|
1007
|
+
ensureUnique(values.map(({ id }) => id), path);
|
|
1008
|
+
return new Map(values.map((value) => [value.id, value]));
|
|
1009
|
+
}
|
|
1010
|
+
function requireReference(index, id, path, kind) {
|
|
1011
|
+
const value = index.get(id);
|
|
1012
|
+
if (!value) return fail("missing-reference", path, `missing ${kind}: ${id}`);
|
|
1013
|
+
return value;
|
|
1014
|
+
}
|
|
1015
|
+
function almostEqual(left, right, epsilon = EPSILON) {
|
|
1016
|
+
return Math.abs(left - right) <= epsilon * Math.max(1, Math.abs(left), Math.abs(right));
|
|
1017
|
+
}
|
|
1018
|
+
function matrixAlmostEqual(left, right) {
|
|
1019
|
+
return left.every((value, index) => almostEqual(value, right[index]));
|
|
1020
|
+
}
|
|
1021
|
+
function multiplyMatrices(left, right) {
|
|
1022
|
+
const output = new Array(16).fill(0);
|
|
1023
|
+
for (let column = 0; column < 4; column += 1) {
|
|
1024
|
+
for (let row = 0; row < 4; row += 1) {
|
|
1025
|
+
let value = 0;
|
|
1026
|
+
for (let inner = 0; inner < 4; inner += 1) value += left[inner * 4 + row] * right[column * 4 + inner];
|
|
1027
|
+
output[column * 4 + row] = value;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return Object.freeze(output);
|
|
1031
|
+
}
|
|
1032
|
+
function transformPosition(matrix, position) {
|
|
1033
|
+
const [x, y, z] = position;
|
|
1034
|
+
return [
|
|
1035
|
+
matrix[0] * x + matrix[4] * y + matrix[8] * z + matrix[12],
|
|
1036
|
+
matrix[1] * x + matrix[5] * y + matrix[9] * z + matrix[13],
|
|
1037
|
+
matrix[2] * x + matrix[6] * y + matrix[10] * z + matrix[14]
|
|
1038
|
+
];
|
|
1039
|
+
}
|
|
1040
|
+
function validateScene(roots, nodes, nodeIndex) {
|
|
1041
|
+
ensureUnique(roots, "$.roots", "duplicate root node");
|
|
1042
|
+
const parentCounts = new Map(nodes.map(({ id }) => [id, 0]));
|
|
1043
|
+
for (let nodeIndexValue = 0; nodeIndexValue < nodes.length; nodeIndexValue += 1) {
|
|
1044
|
+
const node = nodes[nodeIndexValue];
|
|
1045
|
+
for (let childIndex = 0; childIndex < node.children.length; childIndex += 1) {
|
|
1046
|
+
const childId = node.children[childIndex];
|
|
1047
|
+
requireReference(nodeIndex, childId, `$.nodes[${nodeIndexValue}].children[${childIndex}]`, "node");
|
|
1048
|
+
const parentCount = parentCounts.get(childId) + 1;
|
|
1049
|
+
if (parentCount > 1) return fail("invalid-reference", `$.nodes[${nodeIndexValue}].children[${childIndex}]`, "node cannot have more than one parent");
|
|
1050
|
+
parentCounts.set(childId, parentCount);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
const rootSet = new Set(roots);
|
|
1054
|
+
for (let index = 0; index < roots.length; index += 1) {
|
|
1055
|
+
const root = roots[index];
|
|
1056
|
+
requireReference(nodeIndex, root, `$.roots[${index}]`, "node");
|
|
1057
|
+
if (parentCounts.get(root) !== 0) return fail("invalid-reference", `$.roots[${index}]`, "root node cannot have a parent");
|
|
1058
|
+
}
|
|
1059
|
+
for (const [nodeId, count] of parentCounts) {
|
|
1060
|
+
if (count === 0 && !rootSet.has(nodeId)) return fail("invalid-reference", "$.roots", `parentless node is missing from roots: ${nodeId}`);
|
|
1061
|
+
}
|
|
1062
|
+
const worldByNode = /* @__PURE__ */ new Map();
|
|
1063
|
+
const queue = roots.map((root) => ({ id: root, world: requireReference(nodeIndex, root, "$.roots", "node").localMatrix }));
|
|
1064
|
+
for (let cursor = 0; cursor < queue.length; cursor += 1) {
|
|
1065
|
+
const { id, world } = queue[cursor];
|
|
1066
|
+
if (worldByNode.has(id)) return fail("graph-cycle", "$.nodes", "scene graph contains a cycle");
|
|
1067
|
+
worldByNode.set(id, world);
|
|
1068
|
+
const node = requireReference(nodeIndex, id, "$.nodes", "node");
|
|
1069
|
+
for (const childId of node.children) {
|
|
1070
|
+
const child = requireReference(nodeIndex, childId, "$.nodes", "node");
|
|
1071
|
+
queue.push({ id: childId, world: multiplyMatrices(world, child.localMatrix) });
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (worldByNode.size !== nodes.length) return fail("graph-cycle", "$.nodes", "scene graph contains a cycle or unreachable node");
|
|
1075
|
+
const ancestryEntryByNode = /* @__PURE__ */ new Map();
|
|
1076
|
+
const ancestryExitByNode = /* @__PURE__ */ new Map();
|
|
1077
|
+
const traversal = roots.slice().reverse().map((id) => ({ id, exiting: false }));
|
|
1078
|
+
let timestamp = 0;
|
|
1079
|
+
while (traversal.length > 0) {
|
|
1080
|
+
const current = traversal.pop();
|
|
1081
|
+
if (current.exiting) {
|
|
1082
|
+
ancestryExitByNode.set(current.id, timestamp);
|
|
1083
|
+
timestamp += 1;
|
|
1084
|
+
continue;
|
|
1085
|
+
}
|
|
1086
|
+
ancestryEntryByNode.set(current.id, timestamp);
|
|
1087
|
+
timestamp += 1;
|
|
1088
|
+
traversal.push({ id: current.id, exiting: true });
|
|
1089
|
+
const node = requireReference(nodeIndex, current.id, "$.nodes", "node");
|
|
1090
|
+
for (let index = node.children.length - 1; index >= 0; index -= 1) {
|
|
1091
|
+
traversal.push({ id: node.children[index], exiting: false });
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (ancestryExitByNode.size !== nodes.length) return fail("graph-cycle", "$.nodes", "scene ancestry traversal is incomplete");
|
|
1095
|
+
return { worldByNode, ancestryEntryByNode, ancestryExitByNode };
|
|
1096
|
+
}
|
|
1097
|
+
function isAncestor(scene, possibleAncestor, nodeId) {
|
|
1098
|
+
const ancestorEntry = scene.ancestryEntryByNode.get(possibleAncestor);
|
|
1099
|
+
const ancestorExit = scene.ancestryExitByNode.get(possibleAncestor);
|
|
1100
|
+
const nodeEntry = scene.ancestryEntryByNode.get(nodeId);
|
|
1101
|
+
const nodeExit = scene.ancestryExitByNode.get(nodeId);
|
|
1102
|
+
return ancestorEntry !== void 0 && ancestorExit !== void 0 && nodeEntry !== void 0 && nodeExit !== void 0 && ancestorEntry < nodeEntry && nodeExit < ancestorExit;
|
|
1103
|
+
}
|
|
1104
|
+
function validateAttributeAccessor(semantic, accessor, path) {
|
|
1105
|
+
const unsigned = accessor.componentType === "u8" || accessor.componentType === "u16";
|
|
1106
|
+
if (semantic === "POSITION") {
|
|
1107
|
+
if (accessor.componentType !== "f32" || accessor.elementType !== "vec3" || accessor.normalized === true) {
|
|
1108
|
+
return fail("invalid-reference", path, "POSITION accessor must be a non-normalized f32 vec3");
|
|
1109
|
+
}
|
|
1110
|
+
if (!accessor.min || !accessor.max) return fail("invalid-reference", path, "POSITION accessor requires min and max bounds");
|
|
1111
|
+
} else if (semantic === "NORMAL" && (accessor.componentType !== "f32" || accessor.elementType !== "vec3" || accessor.normalized === true)) {
|
|
1112
|
+
return fail("invalid-reference", path, "NORMAL accessor must be a non-normalized f32 vec3");
|
|
1113
|
+
} else if (semantic === "TANGENT" && (accessor.componentType !== "f32" || accessor.elementType !== "vec4" || accessor.normalized === true)) {
|
|
1114
|
+
return fail("invalid-reference", path, "TANGENT accessor must be a non-normalized f32 vec4");
|
|
1115
|
+
} else if (semantic.startsWith("TEXCOORD_") && (accessor.elementType !== "vec2" || accessor.componentType !== "f32" && !(unsigned && accessor.normalized === true))) {
|
|
1116
|
+
return fail("invalid-reference", path, "texture-coordinate accessor must be f32 or normalized unsigned vec2");
|
|
1117
|
+
} else if (semantic.startsWith("COLOR_") && (!(accessor.elementType === "vec3" || accessor.elementType === "vec4") || accessor.componentType !== "f32" && !(unsigned && accessor.normalized === true))) {
|
|
1118
|
+
return fail("invalid-reference", path, "colour accessor must be f32 or normalized unsigned vec3/vec4");
|
|
1119
|
+
} else if (semantic.startsWith("JOINTS_") && (accessor.elementType !== "vec4" || !unsigned || accessor.normalized === true)) {
|
|
1120
|
+
return fail("invalid-reference", path, "joint accessor must be a non-normalized unsigned vec4");
|
|
1121
|
+
} else if (semantic.startsWith("WEIGHTS_") && (accessor.elementType !== "vec4" || accessor.componentType !== "f32" && !(unsigned && accessor.normalized === true))) {
|
|
1122
|
+
return fail("invalid-reference", path, "weight accessor must be f32 or normalized unsigned vec4");
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
function createAccessorReader(accessor, resource, path) {
|
|
1126
|
+
const bytes = verifiedResourceBytes.get(resource);
|
|
1127
|
+
if (!bytes) return fail("resource-unverified", path, "accessor requires a privately verified resource");
|
|
1128
|
+
const componentBytes = COMPONENT_BYTES[accessor.componentType];
|
|
1129
|
+
const components = ELEMENT_COMPONENTS[accessor.elementType];
|
|
1130
|
+
const elementBytes = componentBytes * components;
|
|
1131
|
+
const stride = accessor.byteStride ?? elementBytes;
|
|
1132
|
+
if (stride < elementBytes) return fail("invalid-value", `${path}.byteStride`, "byteStride cannot be smaller than one element");
|
|
1133
|
+
if (accessor.byteOffset % componentBytes !== 0 || stride % componentBytes !== 0) {
|
|
1134
|
+
return fail("invalid-value", path, "accessor offset and stride must align to the component size");
|
|
1135
|
+
}
|
|
1136
|
+
const extent = accessor.byteOffset + stride * (accessor.count - 1) + elementBytes;
|
|
1137
|
+
if (!Number.isSafeInteger(extent) || extent > bytes.byteLength) return fail("invalid-reference", path, "accessor range exceeds its verified buffer resource");
|
|
1138
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
1139
|
+
const rawComponent = (offset) => {
|
|
1140
|
+
switch (accessor.componentType) {
|
|
1141
|
+
case "i8":
|
|
1142
|
+
return view.getInt8(offset);
|
|
1143
|
+
case "u8":
|
|
1144
|
+
return view.getUint8(offset);
|
|
1145
|
+
case "i16":
|
|
1146
|
+
return view.getInt16(offset, true);
|
|
1147
|
+
case "u16":
|
|
1148
|
+
return view.getUint16(offset, true);
|
|
1149
|
+
case "u32":
|
|
1150
|
+
return view.getUint32(offset, true);
|
|
1151
|
+
case "f32":
|
|
1152
|
+
return view.getFloat32(offset, true);
|
|
1153
|
+
}
|
|
1154
|
+
};
|
|
1155
|
+
const normalize = (value) => {
|
|
1156
|
+
if (accessor.normalized !== true) return value;
|
|
1157
|
+
switch (accessor.componentType) {
|
|
1158
|
+
case "i8":
|
|
1159
|
+
return Math.max(value / 127, -1);
|
|
1160
|
+
case "u8":
|
|
1161
|
+
return value / 255;
|
|
1162
|
+
case "i16":
|
|
1163
|
+
return Math.max(value / 32767, -1);
|
|
1164
|
+
case "u16":
|
|
1165
|
+
return value / 65535;
|
|
1166
|
+
case "u32":
|
|
1167
|
+
return value / 4294967295;
|
|
1168
|
+
case "f32":
|
|
1169
|
+
return value;
|
|
1170
|
+
}
|
|
1171
|
+
};
|
|
1172
|
+
const readRaw = (index) => {
|
|
1173
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= accessor.count) return fail("invalid-reference", path, "accessor index is out of range");
|
|
1174
|
+
const offset = accessor.byteOffset + stride * index;
|
|
1175
|
+
return Object.freeze(Array.from({ length: components }, (_, component) => rawComponent(offset + component * componentBytes)));
|
|
1176
|
+
};
|
|
1177
|
+
return {
|
|
1178
|
+
accessor,
|
|
1179
|
+
components,
|
|
1180
|
+
readRaw,
|
|
1181
|
+
read(index) {
|
|
1182
|
+
return Object.freeze(readRaw(index).map(normalize));
|
|
1183
|
+
}
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
function validateAccessorPayload(reader, path) {
|
|
1187
|
+
const actualMin = new Array(reader.components).fill(Number.POSITIVE_INFINITY);
|
|
1188
|
+
const actualMax = new Array(reader.components).fill(Number.NEGATIVE_INFINITY);
|
|
1189
|
+
for (let item = 0; item < reader.accessor.count; item += 1) {
|
|
1190
|
+
const values = reader.readRaw(item);
|
|
1191
|
+
for (let component = 0; component < reader.components; component += 1) {
|
|
1192
|
+
const value = values[component];
|
|
1193
|
+
if (!Number.isFinite(value)) return fail("invalid-value", path, "accessor payload contains a non-finite value");
|
|
1194
|
+
actualMin[component] = Math.min(actualMin[component], value);
|
|
1195
|
+
actualMax[component] = Math.max(actualMax[component], value);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
if (reader.accessor.min && reader.accessor.max) {
|
|
1199
|
+
for (let component = 0; component < reader.components; component += 1) {
|
|
1200
|
+
const minimumMatches = reader.accessor.componentType === "f32" ? almostEqual(actualMin[component], reader.accessor.min[component]) : actualMin[component] === reader.accessor.min[component];
|
|
1201
|
+
const maximumMatches = reader.accessor.componentType === "f32" ? almostEqual(actualMax[component], reader.accessor.max[component]) : actualMax[component] === reader.accessor.max[component];
|
|
1202
|
+
if (!minimumMatches || !maximumMatches) {
|
|
1203
|
+
return fail("invalid-reference", path, "declared accessor min/max do not match verified payload bytes");
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
return Object.freeze({
|
|
1208
|
+
min: Object.freeze(actualMin),
|
|
1209
|
+
max: Object.freeze(actualMax)
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
function actualAccessorBounds(reader, evidence, path) {
|
|
1213
|
+
if (reader.components !== 3) return fail("invalid-reference", path, "position reader must have three components");
|
|
1214
|
+
return {
|
|
1215
|
+
min: evidence.min,
|
|
1216
|
+
max: evidence.max
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1219
|
+
function assertBoundsEqual(actual, declared, path) {
|
|
1220
|
+
for (let component = 0; component < 3; component += 1) {
|
|
1221
|
+
if (Math.abs(actual.min[component] - declared.min[component]) > BOUNDS_ABSOLUTE_EPSILON || Math.abs(actual.max[component] - declared.max[component]) > BOUNDS_ABSOLUTE_EPSILON) {
|
|
1222
|
+
return fail("invalid-reference", path, "declared bounds do not match byte-verified POSITION data");
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function validateReferences(document, budget) {
|
|
1227
|
+
const nodes = indexById(document.nodes, "$.nodes");
|
|
1228
|
+
const resources = indexById(document.resources, "$.resources");
|
|
1229
|
+
const accessors = indexById(document.accessors, "$.accessors");
|
|
1230
|
+
const meshes = indexById(document.meshes, "$.meshes");
|
|
1231
|
+
const materials = indexById(document.materials, "$.materials");
|
|
1232
|
+
const textures = indexById(document.textures, "$.textures");
|
|
1233
|
+
const skeletons = indexById(document.skeletons, "$.skeletons");
|
|
1234
|
+
const joints = indexById(document.joints, "$.joints");
|
|
1235
|
+
const skins = indexById(document.skins, "$.skins");
|
|
1236
|
+
const blendShapes = indexById(document.blendShapes, "$.blendShapes");
|
|
1237
|
+
indexById(document.animations, "$.animations");
|
|
1238
|
+
indexById(document.analyticGeometry, "$.analyticGeometry");
|
|
1239
|
+
const scene = validateScene(document.roots, document.nodes, nodes);
|
|
1240
|
+
const readers = /* @__PURE__ */ new Map();
|
|
1241
|
+
const payloadEvidence = /* @__PURE__ */ new Map();
|
|
1242
|
+
for (let index = 0; index < document.accessors.length; index += 1) {
|
|
1243
|
+
const accessor = document.accessors[index];
|
|
1244
|
+
const resource = requireReference(resources, accessor.resourceId, `$.accessors[${index}].resourceId`, "resource");
|
|
1245
|
+
if (resource.kind !== "buffer") return fail("invalid-reference", `$.accessors[${index}].resourceId`, "accessor resource must be a buffer");
|
|
1246
|
+
const reader = createAccessorReader(accessor, resource, `$.accessors[${index}]`);
|
|
1247
|
+
payloadEvidence.set(accessor.id, validateAccessorPayload(reader, `$.accessors[${index}]`));
|
|
1248
|
+
readers.set(accessor.id, reader);
|
|
1249
|
+
}
|
|
1250
|
+
const primitiveById = /* @__PURE__ */ new Map();
|
|
1251
|
+
const weightedPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1252
|
+
const maximumIndexByAccessor = /* @__PURE__ */ new Map();
|
|
1253
|
+
for (let meshIndex = 0; meshIndex < document.meshes.length; meshIndex += 1) {
|
|
1254
|
+
const mesh = document.meshes[meshIndex];
|
|
1255
|
+
for (let primitiveIndex = 0; primitiveIndex < mesh.primitives.length; primitiveIndex += 1) {
|
|
1256
|
+
const primitive = mesh.primitives[primitiveIndex];
|
|
1257
|
+
const path = `$.meshes[${meshIndex}].primitives[${primitiveIndex}]`;
|
|
1258
|
+
if (primitiveById.has(primitive.id)) return fail("duplicate-id", path, `duplicate primitive ID: ${primitive.id}`);
|
|
1259
|
+
const positionAttribute = primitive.attributes.find(({ semantic }) => semantic === "POSITION");
|
|
1260
|
+
const position = requireReference(accessors, positionAttribute.accessorId, `${path}.attributes`, "accessor");
|
|
1261
|
+
for (let attributeIndex = 0; attributeIndex < primitive.attributes.length; attributeIndex += 1) {
|
|
1262
|
+
const attribute = primitive.attributes[attributeIndex];
|
|
1263
|
+
const accessor = requireReference(accessors, attribute.accessorId, `${path}.attributes[${attributeIndex}].accessorId`, "accessor");
|
|
1264
|
+
validateAttributeAccessor(attribute.semantic, accessor, `${path}.attributes[${attributeIndex}]`);
|
|
1265
|
+
if (accessor.count !== position.count) return fail("invalid-reference", `${path}.attributes[${attributeIndex}]`, "vertex attribute count must match POSITION");
|
|
1266
|
+
}
|
|
1267
|
+
const jointSets = primitive.attributes.filter(({ semantic }) => semantic.startsWith("JOINTS_")).map(({ semantic }) => semantic.slice("JOINTS_".length)).sort();
|
|
1268
|
+
const weightSets = primitive.attributes.filter(({ semantic }) => semantic.startsWith("WEIGHTS_")).map(({ semantic }) => semantic.slice("WEIGHTS_".length)).sort();
|
|
1269
|
+
if (jointSets.length > 0 || weightSets.length > 0) {
|
|
1270
|
+
if (jointSets.length !== weightSets.length || jointSets.some((suffix, index) => suffix !== weightSets[index] || Number(suffix) !== index)) {
|
|
1271
|
+
return fail("invalid-reference", `${path}.attributes`, "JOINTS_n and WEIGHTS_n attributes must be paired and contiguous from zero");
|
|
1272
|
+
}
|
|
1273
|
+
weightedPrimitiveIds.add(primitive.id);
|
|
1274
|
+
}
|
|
1275
|
+
const positionReader = requireReference(readers, position.id, `${path}.attributes`, "accessor reader");
|
|
1276
|
+
const actual = actualAccessorBounds(
|
|
1277
|
+
positionReader,
|
|
1278
|
+
requireReference(payloadEvidence, position.id, `${path}.attributes`, "accessor payload evidence"),
|
|
1279
|
+
`${path}.attributes.POSITION`
|
|
1280
|
+
);
|
|
1281
|
+
assertBoundsEqual(actual, { min: position.min, max: position.max }, `${path}.attributes.POSITION`);
|
|
1282
|
+
if (primitive.indicesAccessorId) {
|
|
1283
|
+
const indices = requireReference(accessors, primitive.indicesAccessorId, `${path}.indicesAccessorId`, "accessor");
|
|
1284
|
+
if (indices.elementType !== "scalar" || !(indices.componentType === "u8" || indices.componentType === "u16" || indices.componentType === "u32") || indices.normalized === true) {
|
|
1285
|
+
return fail("invalid-reference", `${path}.indicesAccessorId`, "index accessor must be an unsigned non-normalized scalar");
|
|
1286
|
+
}
|
|
1287
|
+
const indexReader = requireReference(readers, indices.id, `${path}.indicesAccessorId`, "accessor reader");
|
|
1288
|
+
let maximumIndex = maximumIndexByAccessor.get(indices.id);
|
|
1289
|
+
if (maximumIndex === void 0) {
|
|
1290
|
+
consumeBudget(
|
|
1291
|
+
budget,
|
|
1292
|
+
"geometryIndexElements",
|
|
1293
|
+
indices.count,
|
|
1294
|
+
GPU_MODEL_DOCUMENT_LIMITS.geometryIndexElements,
|
|
1295
|
+
`${path}.indicesAccessorId`,
|
|
1296
|
+
"geometry index element"
|
|
1297
|
+
);
|
|
1298
|
+
maximumIndex = 0;
|
|
1299
|
+
for (let item = 0; item < indices.count; item += 1) {
|
|
1300
|
+
maximumIndex = Math.max(maximumIndex, indexReader.read(item)[0]);
|
|
1301
|
+
}
|
|
1302
|
+
maximumIndexByAccessor.set(indices.id, maximumIndex);
|
|
1303
|
+
}
|
|
1304
|
+
if (maximumIndex >= position.count) return fail("invalid-reference", `${path}.indicesAccessorId`, "index payload references a vertex outside POSITION");
|
|
1305
|
+
}
|
|
1306
|
+
if (primitive.materialId) {
|
|
1307
|
+
const material = requireReference(materials, primitive.materialId, `${path}.materialId`, "material");
|
|
1308
|
+
const semantics = new Set(primitive.attributes.map(({ semantic }) => semantic));
|
|
1309
|
+
for (const [slot, binding] of Object.entries(material.textures)) {
|
|
1310
|
+
if (!binding) continue;
|
|
1311
|
+
const requiredSemantic = `TEXCOORD_${String(binding.texCoordSet ?? 0)}`;
|
|
1312
|
+
if (!semantics.has(requiredSemantic)) {
|
|
1313
|
+
return fail(
|
|
1314
|
+
"invalid-reference",
|
|
1315
|
+
`${path}.materialId`,
|
|
1316
|
+
`material texture binding ${slot} requires primitive attribute ${requiredSemantic}`
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
primitiveById.set(primitive.id, { primitive, mesh, path, position });
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
const meshEvidenceById = /* @__PURE__ */ new Map();
|
|
1325
|
+
for (const mesh of document.meshes) {
|
|
1326
|
+
const primitiveIds = /* @__PURE__ */ new Set();
|
|
1327
|
+
const positionIds = /* @__PURE__ */ new Set();
|
|
1328
|
+
for (const primitive of mesh.primitives) {
|
|
1329
|
+
primitiveIds.add(primitive.id);
|
|
1330
|
+
positionIds.add(requireReference(primitiveById, primitive.id, `$.meshes.${mesh.id}`, "primitive").position.id);
|
|
1331
|
+
}
|
|
1332
|
+
const positionReaders = Object.freeze([...positionIds].map((positionId) => requireReference(readers, positionId, `$.meshes.${mesh.id}`, "POSITION reader")));
|
|
1333
|
+
meshEvidenceById.set(mesh.id, Object.freeze({ primitiveIds, positionReaders }));
|
|
1334
|
+
}
|
|
1335
|
+
const worldGeometry = [];
|
|
1336
|
+
for (let nodeIndex = 0; nodeIndex < document.nodes.length; nodeIndex += 1) {
|
|
1337
|
+
const node = document.nodes[nodeIndex];
|
|
1338
|
+
if (node.skinId && !node.meshId) return fail("invalid-reference", `$.nodes[${nodeIndex}].skinId`, "a skinned node must also reference a mesh");
|
|
1339
|
+
if (node.skinId) requireReference(skins, node.skinId, `$.nodes[${nodeIndex}].skinId`, "skin");
|
|
1340
|
+
if (!node.meshId) continue;
|
|
1341
|
+
requireReference(meshes, node.meshId, `$.nodes[${nodeIndex}].meshId`, "mesh");
|
|
1342
|
+
const meshEvidence = requireReference(meshEvidenceById, node.meshId, `$.nodes[${nodeIndex}].meshId`, "mesh validation evidence");
|
|
1343
|
+
const world = requireReference(scene.worldByNode, node.id, `$.nodes[${nodeIndex}]`, "world transform");
|
|
1344
|
+
const positionReaders = Object.freeze(meshEvidence.positionReaders.map((reader) => {
|
|
1345
|
+
consumeBudget(
|
|
1346
|
+
budget,
|
|
1347
|
+
"geometryWorldAccessorInstances",
|
|
1348
|
+
1,
|
|
1349
|
+
GPU_MODEL_DOCUMENT_LIMITS.geometryWorldAccessorInstances,
|
|
1350
|
+
`$.nodes[${nodeIndex}].meshId`,
|
|
1351
|
+
"world-geometry accessor instance"
|
|
1352
|
+
);
|
|
1353
|
+
consumeBudget(
|
|
1354
|
+
budget,
|
|
1355
|
+
"geometryWorldVertices",
|
|
1356
|
+
reader.accessor.count,
|
|
1357
|
+
GPU_MODEL_DOCUMENT_LIMITS.geometryWorldVertices,
|
|
1358
|
+
`$.nodes[${nodeIndex}].meshId`,
|
|
1359
|
+
"world-geometry vertex"
|
|
1360
|
+
);
|
|
1361
|
+
return reader;
|
|
1362
|
+
}));
|
|
1363
|
+
worldGeometry.push({ nodeIndex, world, positionReaders });
|
|
1364
|
+
}
|
|
1365
|
+
if (worldGeometry.length === 0) return fail("invalid-reference", "$.bounds", "document bounds require scene-referenced byte-verified POSITION geometry");
|
|
1366
|
+
const worldMin = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY];
|
|
1367
|
+
const worldMax = [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY];
|
|
1368
|
+
for (const { nodeIndex, world, positionReaders } of worldGeometry) {
|
|
1369
|
+
for (const reader of positionReaders) {
|
|
1370
|
+
for (let vertex = 0; vertex < reader.accessor.count; vertex += 1) {
|
|
1371
|
+
const transformed = transformPosition(world, reader.read(vertex));
|
|
1372
|
+
for (let component = 0; component < 3; component += 1) {
|
|
1373
|
+
const value = transformed[component];
|
|
1374
|
+
if (!Number.isFinite(value)) return fail("invalid-value", `$.nodes[${nodeIndex}].localMatrix`, "world transform produced non-finite bounds");
|
|
1375
|
+
worldMin[component] = Math.min(worldMin[component], value);
|
|
1376
|
+
worldMax[component] = Math.max(worldMax[component], value);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
assertBoundsEqual(
|
|
1382
|
+
{ min: worldMin, max: worldMax },
|
|
1383
|
+
document.bounds,
|
|
1384
|
+
"$.bounds"
|
|
1385
|
+
);
|
|
1386
|
+
for (let index = 0; index < document.textures.length; index += 1) {
|
|
1387
|
+
const texture = document.textures[index];
|
|
1388
|
+
const resource = requireReference(resources, texture.imageResourceId, `$.textures[${index}].imageResourceId`, "resource");
|
|
1389
|
+
if (resource.kind !== "image") return fail("invalid-reference", `$.textures[${index}].imageResourceId`, "texture resource must be an image");
|
|
1390
|
+
if (texture.contentHash !== resource.contentHash) return fail("invalid-reference", `$.textures[${index}].contentHash`, "texture hash must match its verified image resource");
|
|
1391
|
+
if (texture.mimeType !== void 0 && texture.mimeType !== resource.mimeType) return fail("invalid-reference", `$.textures[${index}].mimeType`, "texture MIME type must match its image resource");
|
|
1392
|
+
if (texture.byteLength !== void 0 && texture.byteLength !== resource.byteLength) return fail("invalid-reference", `$.textures[${index}].byteLength`, "texture byteLength must match its image resource");
|
|
1393
|
+
}
|
|
1394
|
+
for (let materialIndex = 0; materialIndex < document.materials.length; materialIndex += 1) {
|
|
1395
|
+
const material = document.materials[materialIndex];
|
|
1396
|
+
for (const [slot, expectedUsage] of Object.entries(MATERIAL_TEXTURE_SLOTS)) {
|
|
1397
|
+
const binding = material.textures[slot];
|
|
1398
|
+
if (!binding) continue;
|
|
1399
|
+
const texture = requireReference(textures, binding.textureId, `$.materials[${materialIndex}].textures.${slot}.textureId`, "texture");
|
|
1400
|
+
if (binding.intendedUsage !== expectedUsage) return fail("invalid-reference", `$.materials[${materialIndex}].textures.${slot}.intendedUsage`, "texture usage does not match its material slot");
|
|
1401
|
+
if (!texture.intendedUsage.includes(binding.intendedUsage)) return fail("invalid-reference", `$.materials[${materialIndex}].textures.${slot}`, "texture does not declare the binding usage");
|
|
1402
|
+
if (texture.colorSpace !== binding.colorSpace) return fail("invalid-reference", `$.materials[${materialIndex}].textures.${slot}.colorSpace`, "binding and texture color spaces must match");
|
|
1403
|
+
if (binding.samplerId !== void 0 && binding.samplerId !== texture.sampler?.id) return fail("invalid-reference", `$.materials[${materialIndex}].textures.${slot}.samplerId`, "binding sampler does not match the texture sampler");
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
const skeletonForJoint = /* @__PURE__ */ new Map();
|
|
1407
|
+
const jointIdsBySkeleton = /* @__PURE__ */ new Map();
|
|
1408
|
+
for (let skeletonIndex = 0; skeletonIndex < document.skeletons.length; skeletonIndex += 1) {
|
|
1409
|
+
const skeleton = document.skeletons[skeletonIndex];
|
|
1410
|
+
const skeletonJointIds = new Set(skeleton.jointIds);
|
|
1411
|
+
const skeletonJointNodes = /* @__PURE__ */ new Set();
|
|
1412
|
+
const expectedRoots = /* @__PURE__ */ new Set();
|
|
1413
|
+
jointIdsBySkeleton.set(skeleton.id, skeletonJointIds);
|
|
1414
|
+
for (let jointIndex = 0; jointIndex < skeleton.jointIds.length; jointIndex += 1) {
|
|
1415
|
+
const jointId = skeleton.jointIds[jointIndex];
|
|
1416
|
+
const joint = requireReference(joints, jointId, `$.skeletons[${skeletonIndex}].jointIds[${jointIndex}]`, "joint");
|
|
1417
|
+
if (skeletonForJoint.has(jointId)) return fail("invalid-reference", `$.skeletons[${skeletonIndex}].jointIds[${jointIndex}]`, "joint cannot belong to more than one skeleton");
|
|
1418
|
+
skeletonForJoint.set(jointId, skeleton.id);
|
|
1419
|
+
requireReference(nodes, joint.nodeId, `$.joints.${joint.id}.nodeId`, "node");
|
|
1420
|
+
if (skeletonJointNodes.has(joint.nodeId)) return fail("duplicate-id", `$.skeletons[${skeletonIndex}].jointIds`, `duplicate joint node: ${joint.nodeId}`);
|
|
1421
|
+
skeletonJointNodes.add(joint.nodeId);
|
|
1422
|
+
if (joint.parentJointId === void 0) expectedRoots.add(joint.id);
|
|
1423
|
+
}
|
|
1424
|
+
const declaredRoots = new Set(skeleton.rootJointIds);
|
|
1425
|
+
if (expectedRoots.size !== declaredRoots.size || [...expectedRoots].some((id) => !declaredRoots.has(id))) {
|
|
1426
|
+
return fail("invalid-reference", `$.skeletons[${skeletonIndex}].rootJointIds`, "rootJointIds must exactly identify parentless skeleton joints");
|
|
1427
|
+
}
|
|
1428
|
+
for (let rootIndex = 0; rootIndex < skeleton.rootJointIds.length; rootIndex += 1) {
|
|
1429
|
+
const rootJoint = requireReference(joints, skeleton.rootJointIds[rootIndex], `$.skeletons[${skeletonIndex}].rootJointIds[${rootIndex}]`, "joint");
|
|
1430
|
+
if (!skeletonJointIds.has(rootJoint.id)) return fail("invalid-reference", `$.skeletons[${skeletonIndex}].rootJointIds[${rootIndex}]`, "root joint is not a member of the skeleton");
|
|
1431
|
+
requireReference(scene.worldByNode, rootJoint.nodeId, `$.joints.${rootJoint.id}.nodeId`, "scene node");
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (skeletonForJoint.size !== document.joints.length) return fail("invalid-reference", "$.joints", "every joint must belong to exactly one skeleton");
|
|
1435
|
+
for (let jointIndex = 0; jointIndex < document.joints.length; jointIndex += 1) {
|
|
1436
|
+
const joint = document.joints[jointIndex];
|
|
1437
|
+
const node = requireReference(nodes, joint.nodeId, `$.joints[${jointIndex}].nodeId`, "node");
|
|
1438
|
+
if (joint.parentJointId) {
|
|
1439
|
+
const parent = requireReference(joints, joint.parentJointId, `$.joints[${jointIndex}].parentJointId`, "joint");
|
|
1440
|
+
if (skeletonForJoint.get(parent.id) !== skeletonForJoint.get(joint.id)) return fail("invalid-reference", `$.joints[${jointIndex}].parentJointId`, "parent joint must belong to the same skeleton");
|
|
1441
|
+
if (!isAncestor(scene, parent.nodeId, node.id)) return fail("invalid-reference", `$.joints[${jointIndex}].parentJointId`, "parent joint node must be an ancestor of the child joint node");
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
const skinUse = /* @__PURE__ */ new Map();
|
|
1445
|
+
for (const node of document.nodes) {
|
|
1446
|
+
if (!node.skinId || !node.meshId) continue;
|
|
1447
|
+
const set = skinUse.get(node.skinId) ?? /* @__PURE__ */ new Set();
|
|
1448
|
+
set.add(node.meshId);
|
|
1449
|
+
skinUse.set(node.skinId, set);
|
|
1450
|
+
}
|
|
1451
|
+
const skinValidatedPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1452
|
+
for (let skinIndex = 0; skinIndex < document.skins.length; skinIndex += 1) {
|
|
1453
|
+
const skin = document.skins[skinIndex];
|
|
1454
|
+
const skeleton = requireReference(skeletons, skin.skeletonId, `$.skins[${skinIndex}].skeletonId`, "skeleton");
|
|
1455
|
+
const skeletonJointIds = requireReference(jointIdsBySkeleton, skeleton.id, `$.skins[${skinIndex}].skeletonId`, "skeleton joint set");
|
|
1456
|
+
for (let jointIndex = 0; jointIndex < skin.jointIds.length; jointIndex += 1) {
|
|
1457
|
+
const joint = requireReference(joints, skin.jointIds[jointIndex], `$.skins[${skinIndex}].jointIds[${jointIndex}]`, "joint");
|
|
1458
|
+
if (!skeletonJointIds.has(joint.id)) return fail("invalid-reference", `$.skins[${skinIndex}].jointIds[${jointIndex}]`, "skin joint is not a member of its skeleton");
|
|
1459
|
+
if (joint.inverseBindMatrix && !matrixAlmostEqual(joint.inverseBindMatrix, skin.inverseBindMatrices[jointIndex])) {
|
|
1460
|
+
return fail("invalid-reference", `$.skins[${skinIndex}].inverseBindMatrices[${jointIndex}]`, "skin matrix does not match authored joint inverse bind matrix");
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
const usedMeshes = skinUse.get(skin.id);
|
|
1464
|
+
if (!usedMeshes || usedMeshes.size === 0) return fail("invalid-reference", `$.skins[${skinIndex}]`, "skin must be linked from a node that also references a mesh");
|
|
1465
|
+
const expectedPrimitiveIds = /* @__PURE__ */ new Set();
|
|
1466
|
+
for (const meshId of usedMeshes) {
|
|
1467
|
+
requireReference(meshes, meshId, `$.skins[${skinIndex}]`, "mesh");
|
|
1468
|
+
const meshEvidence = requireReference(meshEvidenceById, meshId, `$.skins[${skinIndex}]`, "mesh validation evidence");
|
|
1469
|
+
for (const primitiveId of meshEvidence.primitiveIds) expectedPrimitiveIds.add(primitiveId);
|
|
1470
|
+
}
|
|
1471
|
+
const weightSetByPrimitive = new Map(skin.weightSets.map((weightSet) => [weightSet.primitiveId, weightSet]));
|
|
1472
|
+
if (weightSetByPrimitive.size !== expectedPrimitiveIds.size || [...expectedPrimitiveIds].some((id) => !weightSetByPrimitive.has(id))) {
|
|
1473
|
+
return fail("invalid-reference", `$.skins[${skinIndex}].weightSets`, "skin weight sets must exactly cover linked mesh primitives");
|
|
1474
|
+
}
|
|
1475
|
+
for (const primitiveId of expectedPrimitiveIds) {
|
|
1476
|
+
skinValidatedPrimitiveIds.add(primitiveId);
|
|
1477
|
+
const primitiveEntry = requireReference(primitiveById, primitiveId, `$.skins[${skinIndex}].weightSets`, "primitive");
|
|
1478
|
+
const weightSet = requireReference(weightSetByPrimitive, primitiveId, `$.skins[${skinIndex}].weightSets`, "weight set");
|
|
1479
|
+
const jointsBySet = /* @__PURE__ */ new Map();
|
|
1480
|
+
const weightsBySet = /* @__PURE__ */ new Map();
|
|
1481
|
+
for (const attribute of primitiveEntry.primitive.attributes) {
|
|
1482
|
+
const jointMatch = /^JOINTS_([0-7])$/u.exec(attribute.semantic);
|
|
1483
|
+
const weightMatch = /^WEIGHTS_([0-7])$/u.exec(attribute.semantic);
|
|
1484
|
+
if (jointMatch) jointsBySet.set(Number(jointMatch[1]), requireReference(accessors, attribute.accessorId, primitiveEntry.path, "joint accessor"));
|
|
1485
|
+
if (weightMatch) weightsBySet.set(Number(weightMatch[1]), requireReference(accessors, attribute.accessorId, primitiveEntry.path, "weight accessor"));
|
|
1486
|
+
}
|
|
1487
|
+
if (jointsBySet.size === 0 || jointsBySet.size !== weightsBySet.size) return fail("invalid-reference", primitiveEntry.path, "JOINTS_n and WEIGHTS_n attributes must be paired");
|
|
1488
|
+
for (let setIndex = 0; setIndex < jointsBySet.size; setIndex += 1) {
|
|
1489
|
+
if (!jointsBySet.has(setIndex) || !weightsBySet.has(setIndex)) return fail("invalid-reference", primitiveEntry.path, "joint and weight attribute sets must be contiguous from zero");
|
|
1490
|
+
}
|
|
1491
|
+
if (weightSet.influencesPerVertex !== jointsBySet.size * 4) return fail("invalid-reference", `$.skins[${skinIndex}].weightSets`, "influencesPerVertex does not match joint/weight attribute sets");
|
|
1492
|
+
if (weightSet.joints.length !== primitiveEntry.position.count || weightSet.weights.length !== primitiveEntry.position.count) {
|
|
1493
|
+
return fail("invalid-reference", `$.skins[${skinIndex}].weightSets`, "weight row count must match POSITION");
|
|
1494
|
+
}
|
|
1495
|
+
for (let vertex = 0; vertex < primitiveEntry.position.count; vertex += 1) {
|
|
1496
|
+
const decodedJoints = [];
|
|
1497
|
+
const decodedWeights = [];
|
|
1498
|
+
for (let setIndex = 0; setIndex < jointsBySet.size; setIndex += 1) {
|
|
1499
|
+
decodedJoints.push(...requireReference(readers, jointsBySet.get(setIndex).id, primitiveEntry.path, "joint reader").read(vertex));
|
|
1500
|
+
decodedWeights.push(...requireReference(readers, weightsBySet.get(setIndex).id, primitiveEntry.path, "weight reader").read(vertex));
|
|
1501
|
+
}
|
|
1502
|
+
let total = 0;
|
|
1503
|
+
for (let influence = 0; influence < decodedJoints.length; influence += 1) {
|
|
1504
|
+
const jointIndexValue = decodedJoints[influence];
|
|
1505
|
+
const weight = decodedWeights[influence];
|
|
1506
|
+
if (!Number.isSafeInteger(jointIndexValue) || jointIndexValue < 0 || jointIndexValue >= skin.jointIds.length) return fail("invalid-reference", primitiveEntry.path, "joint payload index exceeds the skin joint list");
|
|
1507
|
+
if (!Number.isFinite(weight) || weight < 0 || weight > 1) return fail("invalid-value", primitiveEntry.path, "weight payload contains an invalid value");
|
|
1508
|
+
if (weightSet.joints[vertex][influence] !== jointIndexValue || !almostEqual(weightSet.weights[vertex][influence], weight)) {
|
|
1509
|
+
return fail("invalid-reference", `$.skins[${skinIndex}].weightSets`, "authored weight rows do not match verified accessor bytes");
|
|
1510
|
+
}
|
|
1511
|
+
total += weight;
|
|
1512
|
+
}
|
|
1513
|
+
if (!almostEqual(total, 1, 1e-4)) return fail("invalid-value", primitiveEntry.path, "vertex weights must sum to one without repair");
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
for (const primitiveId of weightedPrimitiveIds) {
|
|
1518
|
+
if (!skinValidatedPrimitiveIds.has(primitiveId)) return fail("invalid-reference", `$.meshes[].primitives.${primitiveId}`, "joint/weight attributes require a linked skin and mesh node");
|
|
1519
|
+
}
|
|
1520
|
+
for (let shapeIndex = 0; shapeIndex < document.blendShapes.length; shapeIndex += 1) {
|
|
1521
|
+
const shape = document.blendShapes[shapeIndex];
|
|
1522
|
+
const mesh = requireReference(meshes, shape.meshId, `$.blendShapes[${shapeIndex}].meshId`, "mesh");
|
|
1523
|
+
const meshEvidence = requireReference(meshEvidenceById, mesh.id, `$.blendShapes[${shapeIndex}].meshId`, "mesh validation evidence");
|
|
1524
|
+
for (let deltaIndex = 0; deltaIndex < shape.deltas.length; deltaIndex += 1) {
|
|
1525
|
+
const delta = shape.deltas[deltaIndex];
|
|
1526
|
+
const path = `$.blendShapes[${shapeIndex}].deltas[${deltaIndex}]`;
|
|
1527
|
+
if (!meshEvidence.primitiveIds.has(delta.primitiveId)) return fail("invalid-reference", `${path}.primitiveId`, "blend-shape primitive is not part of its mesh");
|
|
1528
|
+
const base = requireReference(primitiveById, delta.primitiveId, path, "primitive").position;
|
|
1529
|
+
for (const key of ["positionAccessorId", "normalAccessorId", "tangentAccessorId"]) {
|
|
1530
|
+
const accessorId = delta[key];
|
|
1531
|
+
if (!accessorId) continue;
|
|
1532
|
+
consumeBudget(
|
|
1533
|
+
budget,
|
|
1534
|
+
"blendShapeAccessorReferences",
|
|
1535
|
+
1,
|
|
1536
|
+
GPU_MODEL_DOCUMENT_LIMITS.blendShapeAccessorReferences,
|
|
1537
|
+
`${path}.${key}`,
|
|
1538
|
+
"blend-shape accessor reference"
|
|
1539
|
+
);
|
|
1540
|
+
const accessor = requireReference(accessors, accessorId, `${path}.${key}`, "accessor");
|
|
1541
|
+
if (accessor.componentType !== "f32" || accessor.elementType !== "vec3" || accessor.count !== base.count || accessor.normalized === true) {
|
|
1542
|
+
return fail("invalid-reference", `${path}.${key}`, "blend-shape accessor has an invalid representation");
|
|
1543
|
+
}
|
|
1544
|
+
requireReference(payloadEvidence, accessor.id, `${path}.${key}`, "verified accessor payload evidence");
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
for (let animationIndex = 0; animationIndex < document.animations.length; animationIndex += 1) {
|
|
1549
|
+
const animation = document.animations[animationIndex];
|
|
1550
|
+
const samplers = indexById(animation.samplers, `$.animations[${animationIndex}].samplers`);
|
|
1551
|
+
indexById(animation.channels, `$.animations[${animationIndex}].channels`);
|
|
1552
|
+
const usedSamplerIds = /* @__PURE__ */ new Set();
|
|
1553
|
+
const targetTypesBySampler = /* @__PURE__ */ new Map();
|
|
1554
|
+
for (let channelIndex = 0; channelIndex < animation.channels.length; channelIndex += 1) {
|
|
1555
|
+
const channel = animation.channels[channelIndex];
|
|
1556
|
+
const path = `$.animations[${animationIndex}].channels[${channelIndex}]`;
|
|
1557
|
+
const sampler = requireReference(samplers, channel.samplerId, `${path}.samplerId`, "animation sampler");
|
|
1558
|
+
usedSamplerIds.add(sampler.id);
|
|
1559
|
+
const previousTarget = targetTypesBySampler.get(sampler.id);
|
|
1560
|
+
if (previousTarget && previousTarget !== channel.targetType) return fail("invalid-reference", `${path}.samplerId`, "one animation sampler cannot drive incompatible target types");
|
|
1561
|
+
targetTypesBySampler.set(sampler.id, channel.targetType);
|
|
1562
|
+
if (channel.targetType === "weights") requireReference(blendShapes, channel.targetId, `${path}.targetId`, "blend shape");
|
|
1563
|
+
else requireReference(nodes, channel.targetId, `${path}.targetId`, "node");
|
|
1564
|
+
}
|
|
1565
|
+
if (usedSamplerIds.size !== animation.samplers.length) return fail("invalid-reference", `$.animations[${animationIndex}].samplers`, "animation contains an unused sampler");
|
|
1566
|
+
for (let samplerIndex = 0; samplerIndex < animation.samplers.length; samplerIndex += 1) {
|
|
1567
|
+
const sampler = animation.samplers[samplerIndex];
|
|
1568
|
+
const path = `$.animations[${animationIndex}].samplers[${samplerIndex}]`;
|
|
1569
|
+
const input = requireReference(accessors, sampler.inputAccessorId, `${path}.inputAccessorId`, "accessor");
|
|
1570
|
+
const output = requireReference(accessors, sampler.outputAccessorId, `${path}.outputAccessorId`, "accessor");
|
|
1571
|
+
if (input.componentType !== "f32" || input.elementType !== "scalar" || input.normalized === true) return fail("invalid-reference", `${path}.inputAccessorId`, "animation input must be a non-normalized f32 scalar accessor");
|
|
1572
|
+
if (input.count !== sampler.inputTimesSeconds.length) return fail("invalid-reference", `${path}.inputTimesSeconds`, "authored input time count does not match its accessor");
|
|
1573
|
+
const inputReader = requireReference(readers, input.id, `${path}.inputAccessorId`, "accessor reader");
|
|
1574
|
+
for (let key = 0; key < input.count; key += 1) {
|
|
1575
|
+
const decoded = inputReader.read(key)[0];
|
|
1576
|
+
if (!Number.isFinite(decoded) || !almostEqual(decoded, sampler.inputTimesSeconds[key])) return fail("invalid-reference", `${path}.inputTimesSeconds`, "authored input times do not match verified accessor bytes");
|
|
1577
|
+
}
|
|
1578
|
+
if (sampler.inputTimesSeconds.at(-1) > animation.durationSeconds + EPSILON) return fail("invalid-value", `${path}.inputTimesSeconds`, "animation key time exceeds clip duration");
|
|
1579
|
+
const targetType = requireReference(targetTypesBySampler, sampler.id, path, "sampler target type");
|
|
1580
|
+
const expectedElementType = targetType === "node-rotation" ? "vec4" : targetType === "weights" ? "scalar" : "vec3";
|
|
1581
|
+
if (output.componentType !== "f32" || output.elementType !== expectedElementType || output.normalized === true) return fail("invalid-reference", `${path}.outputAccessorId`, "animation output has an invalid accessor representation");
|
|
1582
|
+
const multiplier = sampler.interpolation === "cubic-spline" ? 3 : 1;
|
|
1583
|
+
if (output.count !== input.count * multiplier) return fail("invalid-reference", `${path}.outputAccessorId`, "animation input and output element counts are inconsistent");
|
|
1584
|
+
const expectedValues = output.count * ELEMENT_COMPONENTS[output.elementType];
|
|
1585
|
+
if (sampler.outputValues.length !== expectedValues) return fail("invalid-reference", `${path}.outputValues`, "authored output value count does not match its accessor");
|
|
1586
|
+
const outputReader = requireReference(readers, output.id, `${path}.outputAccessorId`, "accessor reader");
|
|
1587
|
+
let flatIndex = 0;
|
|
1588
|
+
for (let element = 0; element < output.count; element += 1) {
|
|
1589
|
+
for (const decoded of outputReader.read(element)) {
|
|
1590
|
+
if (!Number.isFinite(decoded) || !almostEqual(decoded, sampler.outputValues[flatIndex])) return fail("invalid-reference", `${path}.outputValues`, "authored output values do not match verified accessor bytes");
|
|
1591
|
+
flatIndex += 1;
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
for (let index = 0; index < document.analyticGeometry.length; index += 1) {
|
|
1597
|
+
const analytic = document.analyticGeometry[index];
|
|
1598
|
+
const source = requireReference(resources, analytic.sourceResourceId, `$.analyticGeometry[${index}].sourceResourceId`, "resource");
|
|
1599
|
+
if (source.kind !== "buffer") return fail("invalid-reference", `$.analyticGeometry[${index}].sourceResourceId`, "analytic source must be a buffer resource");
|
|
1600
|
+
if (analytic.tessellatedMeshId) requireReference(meshes, analytic.tessellatedMeshId, `$.analyticGeometry[${index}].tessellatedMeshId`, "mesh");
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function parseVerifiedResource(value, path) {
|
|
1604
|
+
if (typeof value !== "object" || value === null || !verifiedResources.has(value) || !verifiedResourceBytes.has(value)) {
|
|
1605
|
+
return fail("resource-unverified", path, "expected a privately verified resource; use createAndVerifyGpuModelDocument");
|
|
1606
|
+
}
|
|
1607
|
+
return value;
|
|
1608
|
+
}
|
|
1609
|
+
function deepFreeze(value) {
|
|
1610
|
+
if (typeof value !== "object" || value === null || value instanceof Blob) return value;
|
|
1611
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
1612
|
+
return Object.freeze(value);
|
|
1613
|
+
}
|
|
1614
|
+
var DOCUMENT_KEYS = Object.freeze([
|
|
1615
|
+
"schemaVersion",
|
|
1616
|
+
"coordinateSystem",
|
|
1617
|
+
"roots",
|
|
1618
|
+
"nodes",
|
|
1619
|
+
"resources",
|
|
1620
|
+
"accessors",
|
|
1621
|
+
"meshes",
|
|
1622
|
+
"materials",
|
|
1623
|
+
"textures",
|
|
1624
|
+
"skeletons",
|
|
1625
|
+
"joints",
|
|
1626
|
+
"skins",
|
|
1627
|
+
"blendShapes",
|
|
1628
|
+
"animations",
|
|
1629
|
+
"analyticGeometry",
|
|
1630
|
+
"bounds",
|
|
1631
|
+
"provenance",
|
|
1632
|
+
"diagnostics",
|
|
1633
|
+
"metadata"
|
|
1634
|
+
]);
|
|
1635
|
+
function parseGpuModelDocument(value) {
|
|
1636
|
+
const path = "$";
|
|
1637
|
+
const record = readRecord(value, path, DOCUMENT_KEYS);
|
|
1638
|
+
const schemaVersion = requireKey(record, "schemaVersion", path);
|
|
1639
|
+
if (schemaVersion !== GPU_MODEL_DOCUMENT_SCHEMA_VERSION) return fail("invalid-value", "$.schemaVersion", "unsupported schema version");
|
|
1640
|
+
const parseCollection = (key, maximum, parser, minimum = 0) => {
|
|
1641
|
+
const input = readArray(requireKey(record, key, path), `$.${key}`, maximum, minimum);
|
|
1642
|
+
return Object.freeze(input.map((entry, index) => parser(entry, `$.${key}[${index}]`)));
|
|
1643
|
+
};
|
|
1644
|
+
const budget = {
|
|
1645
|
+
metadataValues: 0,
|
|
1646
|
+
sceneEdges: 0,
|
|
1647
|
+
primitiveAttributes: 0,
|
|
1648
|
+
accessorElements: 0,
|
|
1649
|
+
primitives: 0,
|
|
1650
|
+
skinJoints: 0,
|
|
1651
|
+
weightVertices: 0,
|
|
1652
|
+
weightInfluences: 0,
|
|
1653
|
+
animationSamplers: 0,
|
|
1654
|
+
animationChannels: 0,
|
|
1655
|
+
animationValues: 0,
|
|
1656
|
+
blendShapeDeltas: 0,
|
|
1657
|
+
blendShapeAccessorReferences: 0,
|
|
1658
|
+
geometryIndexElements: 0,
|
|
1659
|
+
geometryWorldAccessorInstances: 0,
|
|
1660
|
+
geometryWorldVertices: 0
|
|
1661
|
+
};
|
|
1662
|
+
const roots = Object.freeze(readArray(requireKey(record, "roots", path), "$.roots", GPU_MODEL_DOCUMENT_LIMITS.roots, 1).map((entry, index) => readId(entry, `$.roots[${index}]`)));
|
|
1663
|
+
const document = {
|
|
1664
|
+
schemaVersion,
|
|
1665
|
+
coordinateSystem: parseCoordinateSystem(requireKey(record, "coordinateSystem", path)),
|
|
1666
|
+
roots,
|
|
1667
|
+
nodes: parseCollection("nodes", GPU_MODEL_DOCUMENT_LIMITS.nodes, (entry, entryPath) => parseNode(entry, entryPath, budget), 1),
|
|
1668
|
+
resources: parseCollection("resources", GPU_MODEL_DOCUMENT_LIMITS.resources, parseVerifiedResource, 1),
|
|
1669
|
+
accessors: parseCollection("accessors", GPU_MODEL_DOCUMENT_LIMITS.accessors, (entry, entryPath) => parseAccessor(entry, entryPath, budget)),
|
|
1670
|
+
meshes: parseCollection("meshes", GPU_MODEL_DOCUMENT_LIMITS.meshes, (entry, entryPath) => parseMesh(entry, entryPath, budget)),
|
|
1671
|
+
materials: parseCollection("materials", GPU_MODEL_DOCUMENT_LIMITS.materials, (entry, entryPath) => parseMaterial(entry, entryPath, budget)),
|
|
1672
|
+
textures: parseCollection("textures", GPU_MODEL_DOCUMENT_LIMITS.textures, (entry, entryPath) => parseTexture(entry, entryPath, budget)),
|
|
1673
|
+
skeletons: parseCollection("skeletons", GPU_MODEL_DOCUMENT_LIMITS.skeletons, (entry, entryPath) => parseSkeleton(entry, entryPath, budget)),
|
|
1674
|
+
joints: parseCollection("joints", GPU_MODEL_DOCUMENT_LIMITS.joints, (entry, entryPath) => parseJoint(entry, entryPath, budget)),
|
|
1675
|
+
skins: parseCollection("skins", GPU_MODEL_DOCUMENT_LIMITS.skins, (entry, entryPath) => parseSkin(entry, entryPath, budget)),
|
|
1676
|
+
blendShapes: parseCollection("blendShapes", GPU_MODEL_DOCUMENT_LIMITS.blendShapes, (entry, entryPath) => parseBlendShape(entry, entryPath, budget)),
|
|
1677
|
+
animations: parseCollection("animations", GPU_MODEL_DOCUMENT_LIMITS.animations, (entry, entryPath) => parseAnimation(entry, entryPath, budget)),
|
|
1678
|
+
analyticGeometry: parseCollection("analyticGeometry", GPU_MODEL_DOCUMENT_LIMITS.analyticGeometry, (entry, entryPath) => parseAnalyticGeometry(entry, entryPath, budget)),
|
|
1679
|
+
bounds: parseBounds(requireKey(record, "bounds", path), "$.bounds"),
|
|
1680
|
+
provenance: parseProvenance(requireKey(record, "provenance", path), budget),
|
|
1681
|
+
diagnostics: parseCollection("diagnostics", GPU_MODEL_DOCUMENT_LIMITS.diagnostics, (entry, entryPath) => parseDiagnostic(entry, entryPath, budget)),
|
|
1682
|
+
metadata: readMetadata(requireKey(record, "metadata", path), "$.metadata", budget)
|
|
1683
|
+
};
|
|
1684
|
+
if (document.meshes.length === 0) return fail("invalid-value", "$.meshes", "v1 document bounds require at least one canonical mesh");
|
|
1685
|
+
validateReferences(document, budget);
|
|
1686
|
+
const frozen = deepFreeze(document);
|
|
1687
|
+
validatedDocuments.add(frozen);
|
|
1688
|
+
return frozen;
|
|
1689
|
+
}
|
|
1690
|
+
function createGpuModelDocument(value) {
|
|
1691
|
+
if (typeof value === "object" && value !== null && validatedDocuments.has(value)) return value;
|
|
1692
|
+
try {
|
|
1693
|
+
return parseGpuModelDocument(value);
|
|
1694
|
+
} catch (error) {
|
|
1695
|
+
if (error instanceof GpuModelDocumentError) throw error;
|
|
1696
|
+
throw new GpuModelDocumentError("invalid-type", "$", "input could not be safely inspected");
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
function canCreateGpuModelDocument(value) {
|
|
1700
|
+
try {
|
|
1701
|
+
createGpuModelDocument(value);
|
|
1702
|
+
return true;
|
|
1703
|
+
} catch {
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
function isGpuModelDocument(value) {
|
|
1708
|
+
return typeof value === "object" && value !== null && validatedDocuments.has(value);
|
|
1709
|
+
}
|
|
1710
|
+
async function createAndVerifyGpuModelDocument(value, port, options) {
|
|
1711
|
+
try {
|
|
1712
|
+
const limits = resolveVerificationLimits(options);
|
|
1713
|
+
if (options?.signal?.aborted) return fail("resource-verification-failed", "$.resources", "resource verification was aborted");
|
|
1714
|
+
if (isGpuModelDocument(value)) {
|
|
1715
|
+
preflightResourceBudget(value.resources, limits);
|
|
1716
|
+
value.resources.forEach((resource, index) => assertVerifiedResourceEvidence(resource, limits, `$.resources[${index}]`));
|
|
1717
|
+
return value;
|
|
1718
|
+
}
|
|
1719
|
+
const record = readRecord(value, "$", DOCUMENT_KEYS);
|
|
1720
|
+
const resourceInput = readArray(requireKey(record, "resources", "$"), "$.resources", GPU_MODEL_DOCUMENT_LIMITS.resources, 1);
|
|
1721
|
+
const parsed = resourceInput.map((entry, index) => {
|
|
1722
|
+
if (typeof entry === "object" && entry !== null && verifiedResources.has(entry) && verifiedResourceBytes.has(entry)) return entry;
|
|
1723
|
+
return parseResourceHeader(entry, `$.resources[${index}]`);
|
|
1724
|
+
});
|
|
1725
|
+
ensureUnique(parsed.map(({ id }) => id), "$.resources");
|
|
1726
|
+
preflightResourceBudget(parsed, limits);
|
|
1727
|
+
const verified = [];
|
|
1728
|
+
for (let index = 0; index < parsed.length; index += 1) {
|
|
1729
|
+
verified.push(await verifyParsedResource(parsed[index], port, limits, options, `$.resources[${index}]`));
|
|
1730
|
+
}
|
|
1731
|
+
const candidate = {};
|
|
1732
|
+
for (const key of DOCUMENT_KEYS) {
|
|
1733
|
+
if (Object.hasOwn(record, key)) candidate[key] = record[key];
|
|
1734
|
+
}
|
|
1735
|
+
candidate.resources = verified;
|
|
1736
|
+
return createGpuModelDocument(candidate);
|
|
1737
|
+
} catch (error) {
|
|
1738
|
+
if (error instanceof GpuModelDocumentError) throw error;
|
|
1739
|
+
throw new GpuModelDocumentError("invalid-type", "$", "input could not be safely inspected or verified");
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
var GPU_MODEL_STATIC_DEMO_PROFILE_VERSION = "plasius.gpu-model-static-demo/1";
|
|
1743
|
+
var GPU_MODEL_STATIC_DEMO_FEATURE_FLAG = "asset.pipeline.pvox-models.enabled";
|
|
1744
|
+
var GPU_MODEL_STATIC_DEMO_MAX_ABSOLUTE_COORDINATE_METRES = 1048576;
|
|
1745
|
+
var GPU_MODEL_STATIC_DEMO_MAX_EXTENT_METRES = GPU_MODEL_STATIC_DEMO_MAX_ABSOLUTE_COORDINATE_METRES * 2;
|
|
1746
|
+
var GPU_MODEL_STATIC_DEMO_MAX_DIAGONAL_METRES = Math.hypot(
|
|
1747
|
+
GPU_MODEL_STATIC_DEMO_MAX_EXTENT_METRES,
|
|
1748
|
+
GPU_MODEL_STATIC_DEMO_MAX_EXTENT_METRES,
|
|
1749
|
+
GPU_MODEL_STATIC_DEMO_MAX_EXTENT_METRES
|
|
1750
|
+
);
|
|
1751
|
+
var GPU_MODEL_STATIC_DEMO_LIMITS = Object.freeze({
|
|
1752
|
+
maxTriangles: 2e5,
|
|
1753
|
+
maxNodes: 4096,
|
|
1754
|
+
maxPrimitives: 16384,
|
|
1755
|
+
maxMaterials: 4096,
|
|
1756
|
+
maxResourceBytes: 16 * 1024 * 1024,
|
|
1757
|
+
maxAggregateResourceBytes: 16 * 1024 * 1024
|
|
1758
|
+
});
|
|
1759
|
+
var GPU_MODEL_STATIC_DEMO_NORMALIZATION_CONTRACT = Object.freeze({
|
|
1760
|
+
...CANONICAL_GPU_MODEL_COORDINATE_SYSTEM,
|
|
1761
|
+
floorPlane: "y=0",
|
|
1762
|
+
horizontalCenter: "bounds-centre-xz-at-origin"
|
|
1763
|
+
});
|
|
1764
|
+
var CANONICAL_DOCUMENT_ENCODING_VERSION = "plasius.gpu-model-document-canonical-json/1";
|
|
1765
|
+
function canonicalJson(value) {
|
|
1766
|
+
if (value === null) return "null";
|
|
1767
|
+
if (typeof value === "string" || typeof value === "boolean") return JSON.stringify(value);
|
|
1768
|
+
if (typeof value === "number") {
|
|
1769
|
+
if (!Number.isFinite(value)) return fail("invalid-value", "$", "canonical encoding encountered a non-finite number");
|
|
1770
|
+
return JSON.stringify(Object.is(value, -0) ? 0 : value);
|
|
1771
|
+
}
|
|
1772
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
1773
|
+
if (typeof value !== "object" || value === null || value instanceof Blob) {
|
|
1774
|
+
return fail("invalid-type", "$", "canonical encoding encountered an unsupported value");
|
|
1775
|
+
}
|
|
1776
|
+
const entries = Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
|
|
1777
|
+
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`).join(",")}}`;
|
|
1778
|
+
}
|
|
1779
|
+
function canonicalDocumentRecord(document) {
|
|
1780
|
+
const resources = document.resources.map(({ payload: _payload, ...resource }) => resource);
|
|
1781
|
+
return {
|
|
1782
|
+
encodingVersion: CANONICAL_DOCUMENT_ENCODING_VERSION,
|
|
1783
|
+
document: { ...document, resources }
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
function encodeGpuModelDocumentCanonical(document) {
|
|
1787
|
+
if (!isGpuModelDocument(document)) return fail("invalid-type", "$", "canonical encoding requires a verified GPU model document");
|
|
1788
|
+
return new TextEncoder().encode(canonicalJson(canonicalDocumentRecord(document)));
|
|
1789
|
+
}
|
|
1790
|
+
async function hashGpuModelDocumentCanonical(document) {
|
|
1791
|
+
const bytes = encodeGpuModelDocumentCanonical(document);
|
|
1792
|
+
const subtle = globalThis.crypto?.subtle;
|
|
1793
|
+
if (!subtle) return fail("unsupported-feature", "$", "SHA-256 Web Crypto is unavailable");
|
|
1794
|
+
const digest = await subtle.digest("SHA-256", Uint8Array.from(bytes).buffer);
|
|
1795
|
+
return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
|
|
1796
|
+
}
|
|
1797
|
+
function resolveStaticDemoLimits(options) {
|
|
1798
|
+
const record = readRecord(options, "$.options", ["pvoxModelsEnabled", "limits"]);
|
|
1799
|
+
const enabled = readBoolean(requireKey(record, "pvoxModelsEnabled", "$.options"), "$.options.pvoxModelsEnabled");
|
|
1800
|
+
if (!enabled) return fail("feature-disabled", "$.options.pvoxModelsEnabled", `${GPU_MODEL_STATIC_DEMO_FEATURE_FLAG} is disabled`);
|
|
1801
|
+
if (!Object.hasOwn(record, "limits")) return GPU_MODEL_STATIC_DEMO_LIMITS;
|
|
1802
|
+
const overrides = readRecord(record.limits, "$.options.limits", Object.keys(GPU_MODEL_STATIC_DEMO_LIMITS));
|
|
1803
|
+
const limits = { ...GPU_MODEL_STATIC_DEMO_LIMITS };
|
|
1804
|
+
for (const [key, ceiling] of Object.entries(GPU_MODEL_STATIC_DEMO_LIMITS)) {
|
|
1805
|
+
if (Object.hasOwn(overrides, key)) limits[key] = readSafeInteger(overrides[key], `$.options.limits.${key}`, 1, ceiling);
|
|
1806
|
+
}
|
|
1807
|
+
return Object.freeze(limits);
|
|
1808
|
+
}
|
|
1809
|
+
function assertGpuModelStaticDemoNormalization(document) {
|
|
1810
|
+
if (!isGpuModelDocument(document)) return fail("invalid-type", "$", "normalization requires a verified GPU model document");
|
|
1811
|
+
const extents = document.bounds.max.map((value, index) => value - document.bounds.min[index]);
|
|
1812
|
+
const diagonal = Math.hypot(...extents);
|
|
1813
|
+
if (!Number.isFinite(diagonal) || diagonal <= 0) return fail("invalid-value", "$.bounds", "static demo bounds must have a finite non-zero diagonal");
|
|
1814
|
+
if ([...document.bounds.min, ...document.bounds.max].some((value) => Math.abs(value) > GPU_MODEL_STATIC_DEMO_MAX_ABSOLUTE_COORDINATE_METRES)) {
|
|
1815
|
+
return fail("limit-exceeded", "$.bounds", "static demo coordinate magnitude exceeds the fixed PVOX ceiling");
|
|
1816
|
+
}
|
|
1817
|
+
if (extents.some((extent) => extent > GPU_MODEL_STATIC_DEMO_MAX_EXTENT_METRES) || diagonal > GPU_MODEL_STATIC_DEMO_MAX_DIAGONAL_METRES) {
|
|
1818
|
+
return fail("limit-exceeded", "$.bounds", "static demo extent exceeds the fixed PVOX ceiling");
|
|
1819
|
+
}
|
|
1820
|
+
const tolerance = Math.min(1e-3, Math.max(1, diagonal) * EPSILON);
|
|
1821
|
+
const floorAligned = Math.abs(document.bounds.min[1]) <= tolerance;
|
|
1822
|
+
const centeredX = Math.abs(document.bounds.min[0] + document.bounds.max[0]) <= tolerance * 2;
|
|
1823
|
+
const centeredZ = Math.abs(document.bounds.min[2] + document.bounds.max[2]) <= tolerance * 2;
|
|
1824
|
+
if (!floorAligned || !centeredX || !centeredZ) {
|
|
1825
|
+
return fail("invalid-value", "$.bounds", "static demo geometry must be floor-centred at the canonical origin");
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
function determinant3(matrix) {
|
|
1829
|
+
const a00 = matrix[0], a01 = matrix[4], a02 = matrix[8];
|
|
1830
|
+
const a10 = matrix[1], a11 = matrix[5], a12 = matrix[9];
|
|
1831
|
+
const a20 = matrix[2], a21 = matrix[6], a22 = matrix[10];
|
|
1832
|
+
return a00 * (a11 * a22 - a12 * a21) - a01 * (a10 * a22 - a12 * a20) + a02 * (a10 * a21 - a11 * a20);
|
|
1833
|
+
}
|
|
1834
|
+
function normalizedVector(value, path) {
|
|
1835
|
+
const length = Math.hypot(value[0], value[1], value[2]);
|
|
1836
|
+
if (!Number.isFinite(length) || length <= 1e-12) return fail("invalid-value", path, "triangle or normal vector is degenerate");
|
|
1837
|
+
return Object.freeze([value[0] / length, value[1] / length, value[2] / length]);
|
|
1838
|
+
}
|
|
1839
|
+
function faceNormal(positions, path) {
|
|
1840
|
+
const ab = positions[1].map((value, index) => value - positions[0][index]);
|
|
1841
|
+
const ac = positions[2].map((value, index) => value - positions[0][index]);
|
|
1842
|
+
return normalizedVector([
|
|
1843
|
+
ab[1] * ac[2] - ab[2] * ac[1],
|
|
1844
|
+
ab[2] * ac[0] - ab[0] * ac[2],
|
|
1845
|
+
ab[0] * ac[1] - ab[1] * ac[0]
|
|
1846
|
+
], path);
|
|
1847
|
+
}
|
|
1848
|
+
function transformNormal(matrix, normal, determinant, fallback) {
|
|
1849
|
+
const a00 = matrix[0], a01 = matrix[4], a02 = matrix[8];
|
|
1850
|
+
const a10 = matrix[1], a11 = matrix[5], a12 = matrix[9];
|
|
1851
|
+
const a20 = matrix[2], a21 = matrix[6], a22 = matrix[10];
|
|
1852
|
+
const transformed = [
|
|
1853
|
+
((a11 * a22 - a12 * a21) * normal[0] + (a12 * a20 - a10 * a22) * normal[1] + (a10 * a21 - a11 * a20) * normal[2]) / determinant,
|
|
1854
|
+
((a02 * a21 - a01 * a22) * normal[0] + (a00 * a22 - a02 * a20) * normal[1] + (a01 * a20 - a00 * a21) * normal[2]) / determinant,
|
|
1855
|
+
((a01 * a12 - a02 * a11) * normal[0] + (a02 * a10 - a00 * a12) * normal[1] + (a00 * a11 - a01 * a10) * normal[2]) / determinant
|
|
1856
|
+
];
|
|
1857
|
+
const length = Math.hypot(...transformed);
|
|
1858
|
+
return !Number.isFinite(length) || length <= 1e-12 ? fallback : Object.freeze(transformed.map((value) => value / length));
|
|
1859
|
+
}
|
|
1860
|
+
function triangleBounds(positions) {
|
|
1861
|
+
const min = [0, 1, 2].map((component) => Math.min(...positions.map((position) => position[component])));
|
|
1862
|
+
const max = [0, 1, 2].map((component) => Math.max(...positions.map((position) => position[component])));
|
|
1863
|
+
return { min, max };
|
|
1864
|
+
}
|
|
1865
|
+
function assertStaticDemoTriangleCoordinates(positions, path) {
|
|
1866
|
+
for (let corner = 0; corner < positions.length; corner += 1) {
|
|
1867
|
+
for (let component = 0; component < positions[corner].length; component += 1) {
|
|
1868
|
+
if (Math.abs(positions[corner][component]) > GPU_MODEL_STATIC_DEMO_MAX_ABSOLUTE_COORDINATE_METRES) {
|
|
1869
|
+
return fail("limit-exceeded", path, "static demo triangle coordinate exceeds the fixed PVOX ceiling");
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
function staticDemoMaterial(material) {
|
|
1875
|
+
if (material.workflow !== "metallic-roughness" && material.workflow !== "unlit") {
|
|
1876
|
+
return fail("unsupported-feature", `$.materials.${material.id}.workflow`, "static demo supports metallic-roughness and unlit materials only");
|
|
1877
|
+
}
|
|
1878
|
+
const unsupportedSurfaceExtension = (material.transmissionFactor ?? 0) > 0 || (material.clearcoatFactor ?? 0) > 0 || (material.sheenColorFactor?.some((value) => value > 0) ?? false) || Object.keys(material.extensions).length > 0;
|
|
1879
|
+
if (material.alphaMode !== "opaque" || material.baseColorFactor[3] !== 1 || unsupportedSurfaceExtension) {
|
|
1880
|
+
return fail("unsupported-feature", `$.materials.${material.id}`, "static demo supports opaque fixed-factor materials without unsupported surface extensions");
|
|
1881
|
+
}
|
|
1882
|
+
if (Object.keys(material.textures).length > 0) {
|
|
1883
|
+
return fail("unsupported-feature", `$.materials.${material.id}.textures`, "static demo accepts fixed surface factors, not texture bindings");
|
|
1884
|
+
}
|
|
1885
|
+
const output = {
|
|
1886
|
+
sourceMaterialId: material.id,
|
|
1887
|
+
sourceWorkflow: material.workflow,
|
|
1888
|
+
baseColorFactor: material.baseColorFactor,
|
|
1889
|
+
metallicFactor: material.metallicFactor ?? 0,
|
|
1890
|
+
roughnessFactor: material.roughnessFactor ?? 1,
|
|
1891
|
+
emissiveFactor: material.emissiveFactor ?? [0, 0, 0],
|
|
1892
|
+
doubleSided: material.doubleSided ?? false
|
|
1893
|
+
};
|
|
1894
|
+
if (material.name !== void 0) output.sourceMaterialName = material.name;
|
|
1895
|
+
return output;
|
|
1896
|
+
}
|
|
1897
|
+
function defaultStaticDemoMaterial() {
|
|
1898
|
+
return {
|
|
1899
|
+
sourceMaterialId: null,
|
|
1900
|
+
sourceWorkflow: "metallic-roughness",
|
|
1901
|
+
baseColorFactor: [1, 1, 1, 1],
|
|
1902
|
+
metallicFactor: 0,
|
|
1903
|
+
roughnessFactor: 1,
|
|
1904
|
+
emissiveFactor: [0, 0, 0],
|
|
1905
|
+
doubleSided: false
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
async function createGpuModelStaticDemoCompilerInput(document, options) {
|
|
1909
|
+
if (!isGpuModelDocument(document)) return fail("invalid-type", "$", "static demo compilation requires a verified GPU model document");
|
|
1910
|
+
const limits = resolveStaticDemoLimits(options);
|
|
1911
|
+
assertGpuModelStaticDemoNormalization(document);
|
|
1912
|
+
if (document.nodes.length > limits.maxNodes) return fail("limit-exceeded", "$.nodes", "static demo node limit exceeded");
|
|
1913
|
+
const primitiveCount = document.meshes.reduce((total, mesh) => total + mesh.primitives.length, 0);
|
|
1914
|
+
if (primitiveCount > limits.maxPrimitives) return fail("limit-exceeded", "$.meshes", "static demo primitive limit exceeded");
|
|
1915
|
+
if (document.materials.length > limits.maxMaterials) return fail("limit-exceeded", "$.materials", "static demo material limit exceeded");
|
|
1916
|
+
if (document.textures.length > 0) return fail("unsupported-feature", "$.textures", "static demo does not retain texture resources");
|
|
1917
|
+
for (const [key, values] of [
|
|
1918
|
+
["skeletons", document.skeletons],
|
|
1919
|
+
["joints", document.joints],
|
|
1920
|
+
["skins", document.skins],
|
|
1921
|
+
["blendShapes", document.blendShapes],
|
|
1922
|
+
["animations", document.animations],
|
|
1923
|
+
["analyticGeometry", document.analyticGeometry]
|
|
1924
|
+
]) {
|
|
1925
|
+
if (values.length > 0) return fail("unsupported-feature", `$.${key}`, "static demo supports rigid triangle geometry only");
|
|
1926
|
+
}
|
|
1927
|
+
let aggregateResourceBytes = 0;
|
|
1928
|
+
for (let index = 0; index < document.resources.length; index += 1) {
|
|
1929
|
+
const resource = document.resources[index];
|
|
1930
|
+
if (resource.kind !== "buffer") return fail("unsupported-feature", `$.resources[${index}]`, "static demo permits buffer resources only");
|
|
1931
|
+
if (resource.byteLength > limits.maxResourceBytes) return fail("limit-exceeded", `$.resources[${index}].byteLength`, "static demo resource limit exceeded");
|
|
1932
|
+
aggregateResourceBytes += resource.byteLength;
|
|
1933
|
+
if (!Number.isSafeInteger(aggregateResourceBytes) || aggregateResourceBytes > limits.maxAggregateResourceBytes) {
|
|
1934
|
+
return fail("limit-exceeded", "$.resources", "static demo aggregate resource limit exceeded");
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
const nodes = indexById(document.nodes, "$.nodes");
|
|
1938
|
+
const meshes = indexById(document.meshes, "$.meshes");
|
|
1939
|
+
const accessors = indexById(document.accessors, "$.accessors");
|
|
1940
|
+
const resources = indexById(document.resources, "$.resources");
|
|
1941
|
+
const scene = validateScene(document.roots, document.nodes, nodes);
|
|
1942
|
+
const readers = /* @__PURE__ */ new Map();
|
|
1943
|
+
const readerFor = (accessor, path) => {
|
|
1944
|
+
const existing = readers.get(accessor.id);
|
|
1945
|
+
if (existing) return existing;
|
|
1946
|
+
const resource = requireReference(resources, accessor.resourceId, path, "resource");
|
|
1947
|
+
const reader = createAccessorReader(accessor, resource, path);
|
|
1948
|
+
readers.set(accessor.id, reader);
|
|
1949
|
+
return reader;
|
|
1950
|
+
};
|
|
1951
|
+
const materials = document.materials.map(staticDemoMaterial);
|
|
1952
|
+
const materialIndices = new Map(document.materials.map((material, index) => [material.id, index]));
|
|
1953
|
+
let defaultMaterialIndex;
|
|
1954
|
+
const worldTriangles = [];
|
|
1955
|
+
for (let nodeIndex = 0; nodeIndex < document.nodes.length; nodeIndex += 1) {
|
|
1956
|
+
const node = document.nodes[nodeIndex];
|
|
1957
|
+
if (node.skinId !== void 0) return fail("unsupported-feature", `$.nodes[${nodeIndex}].skinId`, "static demo nodes cannot reference skins");
|
|
1958
|
+
if (!node.meshId) continue;
|
|
1959
|
+
const mesh = requireReference(meshes, node.meshId, `$.nodes[${nodeIndex}].meshId`, "mesh");
|
|
1960
|
+
const world = requireReference(scene.worldByNode, node.id, `$.nodes[${nodeIndex}]`, "world transform");
|
|
1961
|
+
const determinant = determinant3(world);
|
|
1962
|
+
if (!Number.isFinite(determinant) || Math.abs(determinant) <= 1e-12) {
|
|
1963
|
+
return fail("invalid-value", `$.nodes[${nodeIndex}].localMatrix`, "static demo world transform must be invertible");
|
|
1964
|
+
}
|
|
1965
|
+
for (let primitiveIndex = 0; primitiveIndex < mesh.primitives.length; primitiveIndex += 1) {
|
|
1966
|
+
const primitive = mesh.primitives[primitiveIndex];
|
|
1967
|
+
const path = `$.meshes.${mesh.id}.primitives[${primitiveIndex}]`;
|
|
1968
|
+
if (primitive.topology !== "triangles") return fail("unsupported-feature", `${path}.topology`, "static demo requires explicit triangle topology");
|
|
1969
|
+
const unsupportedAttribute = primitive.attributes.find(({ semantic }) => semantic !== "POSITION" && semantic !== "NORMAL");
|
|
1970
|
+
if (unsupportedAttribute) return fail("unsupported-feature", `${path}.attributes`, `static demo does not consume ${unsupportedAttribute.semantic}`);
|
|
1971
|
+
const positionAttribute = primitive.attributes.find(({ semantic }) => semantic === "POSITION");
|
|
1972
|
+
if (!positionAttribute) return fail("invalid-reference", `${path}.attributes`, "static demo requires POSITION");
|
|
1973
|
+
const positionAccessor = requireReference(accessors, positionAttribute.accessorId, `${path}.attributes.POSITION`, "accessor");
|
|
1974
|
+
const positionReader = readerFor(positionAccessor, `${path}.attributes.POSITION`);
|
|
1975
|
+
const normalAttribute = primitive.attributes.find(({ semantic }) => semantic === "NORMAL");
|
|
1976
|
+
const normalReader = normalAttribute ? readerFor(requireReference(accessors, normalAttribute.accessorId, `${path}.attributes.NORMAL`, "accessor"), `${path}.attributes.NORMAL`) : void 0;
|
|
1977
|
+
const indexReader = primitive.indicesAccessorId ? readerFor(requireReference(accessors, primitive.indicesAccessorId, `${path}.indicesAccessorId`, "accessor"), `${path}.indicesAccessorId`) : void 0;
|
|
1978
|
+
const elementCount = indexReader?.accessor.count ?? positionAccessor.count;
|
|
1979
|
+
if (elementCount % 3 !== 0) return fail("invalid-value", path, "triangle element count must be divisible by three");
|
|
1980
|
+
const triangleCount = elementCount / 3;
|
|
1981
|
+
if (worldTriangles.length + triangleCount > limits.maxTriangles) return fail("limit-exceeded", path, "static demo triangle limit exceeded");
|
|
1982
|
+
let materialIndex;
|
|
1983
|
+
if (primitive.materialId !== void 0) {
|
|
1984
|
+
const resolvedMaterialIndex = materialIndices.get(primitive.materialId);
|
|
1985
|
+
if (resolvedMaterialIndex === void 0) return fail("missing-reference", `${path}.materialId`, `missing material index: ${primitive.materialId}`);
|
|
1986
|
+
materialIndex = resolvedMaterialIndex;
|
|
1987
|
+
} else {
|
|
1988
|
+
if (defaultMaterialIndex === void 0) {
|
|
1989
|
+
if (materials.length >= limits.maxMaterials) return fail("limit-exceeded", "$.materials", "static demo material limit exceeded");
|
|
1990
|
+
defaultMaterialIndex = materials.length;
|
|
1991
|
+
materials.push(defaultStaticDemoMaterial());
|
|
1992
|
+
}
|
|
1993
|
+
materialIndex = defaultMaterialIndex;
|
|
1994
|
+
}
|
|
1995
|
+
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
|
|
1996
|
+
const sourceIndices = [0, 1, 2].map((corner) => {
|
|
1997
|
+
const element = triangleIndex * 3 + corner;
|
|
1998
|
+
return indexReader ? indexReader.read(element)[0] : element;
|
|
1999
|
+
});
|
|
2000
|
+
if (determinant < 0) [sourceIndices[1], sourceIndices[2]] = [sourceIndices[2], sourceIndices[1]];
|
|
2001
|
+
const positions = Object.freeze(sourceIndices.map((index) => Object.freeze(transformPosition(world, positionReader.read(index)))));
|
|
2002
|
+
assertStaticDemoTriangleCoordinates(positions, `${path}.triangles[${triangleIndex}]`);
|
|
2003
|
+
const fallback = faceNormal(positions, `${path}.triangles[${triangleIndex}]`);
|
|
2004
|
+
const normals = Object.freeze(sourceIndices.map((index) => normalReader ? transformNormal(world, normalReader.read(index), determinant, fallback) : fallback));
|
|
2005
|
+
worldTriangles.push({
|
|
2006
|
+
sourceNodeId: node.id,
|
|
2007
|
+
sourceMeshId: mesh.id,
|
|
2008
|
+
sourcePrimitiveId: primitive.id,
|
|
2009
|
+
sourceTriangleIndex: triangleIndex,
|
|
2010
|
+
materialIndex,
|
|
2011
|
+
positions,
|
|
2012
|
+
normals,
|
|
2013
|
+
bounds: triangleBounds(positions)
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
if (worldTriangles.length === 0) return fail("invalid-value", "$.meshes", "static demo requires at least one world triangle");
|
|
2019
|
+
const sourceEvidence = {
|
|
2020
|
+
sourceFormat: document.provenance.sourceFormat,
|
|
2021
|
+
sourceContentHash: document.provenance.sourceContentHash,
|
|
2022
|
+
converterId: document.provenance.converterId,
|
|
2023
|
+
converterVersion: document.provenance.converterVersion,
|
|
2024
|
+
...document.provenance.provider !== void 0 ? { provider: document.provenance.provider } : {}
|
|
2025
|
+
};
|
|
2026
|
+
return deepFreeze({
|
|
2027
|
+
profileVersion: GPU_MODEL_STATIC_DEMO_PROFILE_VERSION,
|
|
2028
|
+
canonicalDocumentHash: await hashGpuModelDocumentCanonical(document),
|
|
2029
|
+
coordinateSystem: CANONICAL_GPU_MODEL_COORDINATE_SYSTEM,
|
|
2030
|
+
bounds: document.bounds,
|
|
2031
|
+
sourceEvidence,
|
|
2032
|
+
materials,
|
|
2033
|
+
worldTriangles
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// src/index.ts
|
|
2038
|
+
var packageName = "@plasius/gpu-model-core";
|
|
2039
|
+
var packageBootstrap = Object.freeze({
|
|
2040
|
+
packageName,
|
|
2041
|
+
featureFlag: "gpu.model.conversion.enabled",
|
|
2042
|
+
status: "bootstrap"
|
|
2043
|
+
});
|
|
2044
|
+
export {
|
|
2045
|
+
CANONICAL_GPU_MODEL_COORDINATE_SYSTEM,
|
|
2046
|
+
GPU_MODEL_DOCUMENT_LIMITS,
|
|
2047
|
+
GPU_MODEL_DOCUMENT_SCHEMA_VERSION,
|
|
2048
|
+
GPU_MODEL_RESOURCE_VERIFICATION_LIMITS,
|
|
2049
|
+
GPU_MODEL_STATIC_DEMO_FEATURE_FLAG,
|
|
2050
|
+
GPU_MODEL_STATIC_DEMO_LIMITS,
|
|
2051
|
+
GPU_MODEL_STATIC_DEMO_MAX_ABSOLUTE_COORDINATE_METRES,
|
|
2052
|
+
GPU_MODEL_STATIC_DEMO_MAX_DIAGONAL_METRES,
|
|
2053
|
+
GPU_MODEL_STATIC_DEMO_MAX_EXTENT_METRES,
|
|
2054
|
+
GPU_MODEL_STATIC_DEMO_NORMALIZATION_CONTRACT,
|
|
2055
|
+
GPU_MODEL_STATIC_DEMO_PROFILE_VERSION,
|
|
2056
|
+
GpuModelDocumentError,
|
|
2057
|
+
assertGpuModelStaticDemoNormalization,
|
|
2058
|
+
canCreateGpuModelDocument,
|
|
2059
|
+
createAndVerifyGpuModelDocument,
|
|
2060
|
+
createGpuModelDocument,
|
|
2061
|
+
createGpuModelStaticDemoCompilerInput,
|
|
2062
|
+
encodeGpuModelDocumentCanonical,
|
|
2063
|
+
hashGpuModelDocumentCanonical,
|
|
2064
|
+
isGpuModelDocument,
|
|
2065
|
+
packageBootstrap,
|
|
2066
|
+
packageName,
|
|
2067
|
+
verifyGpuModelResource
|
|
2068
|
+
};
|
|
2069
|
+
//# sourceMappingURL=index.js.map
|