@plasius/gpu-model-core 0.1.0 → 0.2.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/README.md +65 -0
- package/dist/index.cjs +476 -214
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -1
- package/dist/index.d.ts +151 -1
- package/dist/index.js +472 -214
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -172,9 +172,9 @@ function readRecord(value, path, allowedKeys) {
|
|
|
172
172
|
}
|
|
173
173
|
return value;
|
|
174
174
|
}
|
|
175
|
-
function requireKey(
|
|
176
|
-
if (!Object.hasOwn(
|
|
177
|
-
return
|
|
175
|
+
function requireKey(record2, key, path) {
|
|
176
|
+
if (!Object.hasOwn(record2, key)) return fail("invalid-value", `${path}.${key}`, "required key is missing");
|
|
177
|
+
return record2[key];
|
|
178
178
|
}
|
|
179
179
|
function readArray(value, path, maximum, minimum = 0) {
|
|
180
180
|
if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype) {
|
|
@@ -253,9 +253,9 @@ function readNumberTuple(value, path, length, minimum, maximum) {
|
|
|
253
253
|
const input = readArray(value, path, length, length);
|
|
254
254
|
return Object.freeze(input.map((entry, index) => readFinite(entry, `${path}[${index}]`, minimum, maximum)));
|
|
255
255
|
}
|
|
256
|
-
function readOptional(
|
|
257
|
-
if (!Object.hasOwn(
|
|
258
|
-
return parser(
|
|
256
|
+
function readOptional(record2, key, parser, path) {
|
|
257
|
+
if (!Object.hasOwn(record2, key)) return void 0;
|
|
258
|
+
return parser(record2[key], `${path}.${key}`);
|
|
259
259
|
}
|
|
260
260
|
function assignOptional(target, key, value) {
|
|
261
261
|
if (value !== void 0) Object.assign(target, { [key]: value });
|
|
@@ -277,17 +277,17 @@ function readMetadataValue(value, path, depth, budget) {
|
|
|
277
277
|
return Object.freeze(input.map((entry, index) => readMetadataValue(entry, `${path}[${index}]`, depth + 1, budget)));
|
|
278
278
|
}
|
|
279
279
|
if (typeof value !== "object" || value === null) return fail("invalid-type", path, "metadata value is not JSON-compatible");
|
|
280
|
-
const
|
|
281
|
-
const prototype = Object.getPrototypeOf(
|
|
280
|
+
const record2 = value;
|
|
281
|
+
const prototype = Object.getPrototypeOf(record2);
|
|
282
282
|
if (prototype !== Object.prototype && prototype !== null) return fail("invalid-type", path, "metadata objects must be plain objects");
|
|
283
|
-
const keys = Reflect.ownKeys(
|
|
283
|
+
const keys = Reflect.ownKeys(record2);
|
|
284
284
|
if (keys.length > GPU_MODEL_DOCUMENT_LIMITS.metadataObjectKeys) return fail("limit-exceeded", path, "metadata object has too many keys");
|
|
285
285
|
const output = {};
|
|
286
286
|
for (const key of keys) {
|
|
287
287
|
if (typeof key !== "string") return fail("unknown-key", path, "metadata symbol keys are not permitted");
|
|
288
288
|
if (PROHIBITED_KEYS.has(key)) return fail("unknown-key", `${path}.${key}`, "prohibited metadata key");
|
|
289
289
|
readString(key, `${path}.[key]`, GPU_MODEL_DOCUMENT_LIMITS.metadataKeyLength, METADATA_KEY);
|
|
290
|
-
const descriptor = Object.getOwnPropertyDescriptor(
|
|
290
|
+
const descriptor = Object.getOwnPropertyDescriptor(record2, key);
|
|
291
291
|
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) return fail("invalid-type", `${path}.${key}`, "metadata properties must be enumerable data properties");
|
|
292
292
|
output[key] = readMetadataValue(descriptor.value, `${path}.${key}`, depth + 1, budget);
|
|
293
293
|
}
|
|
@@ -300,9 +300,9 @@ function readMetadata(value, path, budget) {
|
|
|
300
300
|
}
|
|
301
301
|
function parseCoordinateSystem(value) {
|
|
302
302
|
const path = "$.coordinateSystem";
|
|
303
|
-
const
|
|
303
|
+
const record2 = readRecord(value, path, ["unit", "upAxis", "forwardAxis", "handedness", "winding", "origin"]);
|
|
304
304
|
for (const [key, expected] of Object.entries(CANONICAL_GPU_MODEL_COORDINATE_SYSTEM)) {
|
|
305
|
-
if (requireKey(
|
|
305
|
+
if (requireKey(record2, key, path) !== expected) return fail("invalid-value", `${path}.${key}`, "coordinate system is not canonical");
|
|
306
306
|
}
|
|
307
307
|
return CANONICAL_GPU_MODEL_COORDINATE_SYSTEM;
|
|
308
308
|
}
|
|
@@ -314,19 +314,19 @@ function parseMatrix(value, path, requireAffine = true) {
|
|
|
314
314
|
return matrix;
|
|
315
315
|
}
|
|
316
316
|
function parseBounds(value, path) {
|
|
317
|
-
const
|
|
318
|
-
const min = readNumberTuple(requireKey(
|
|
319
|
-
const max = readNumberTuple(requireKey(
|
|
317
|
+
const record2 = readRecord(value, path, ["min", "max"]);
|
|
318
|
+
const min = readNumberTuple(requireKey(record2, "min", path), `${path}.min`, 3);
|
|
319
|
+
const max = readNumberTuple(requireKey(record2, "max", path), `${path}.max`, 3);
|
|
320
320
|
for (let index = 0; index < 3; index += 1) {
|
|
321
321
|
if (min[index] > max[index]) return fail("invalid-value", path, "bounds min cannot exceed max");
|
|
322
322
|
}
|
|
323
323
|
return { min, max };
|
|
324
324
|
}
|
|
325
325
|
function parseTransformTrs(value, path) {
|
|
326
|
-
const
|
|
327
|
-
const translation = readNumberTuple(requireKey(
|
|
328
|
-
const rotation = readNumberTuple(requireKey(
|
|
329
|
-
const scale = readNumberTuple(requireKey(
|
|
326
|
+
const record2 = readRecord(value, path, ["translation", "rotation", "scale"]);
|
|
327
|
+
const translation = readNumberTuple(requireKey(record2, "translation", path), `${path}.translation`, 3);
|
|
328
|
+
const rotation = readNumberTuple(requireKey(record2, "rotation", path), `${path}.rotation`, 4);
|
|
329
|
+
const scale = readNumberTuple(requireKey(record2, "scale", path), `${path}.scale`, 3);
|
|
330
330
|
const magnitude = Math.hypot(...rotation);
|
|
331
331
|
if (!almostEqual(magnitude, 1, 1e-4)) return fail("invalid-value", `${path}.rotation`, "rotation quaternion must be normalized");
|
|
332
332
|
return { translation, rotation, scale };
|
|
@@ -348,25 +348,25 @@ function intrinsicBlobArrayBuffer(blob) {
|
|
|
348
348
|
return Reflect.apply(blobArrayBuffer, blob, []);
|
|
349
349
|
}
|
|
350
350
|
function parseResourceHeader(value, path) {
|
|
351
|
-
const
|
|
352
|
-
const kind = readEnum(requireKey(
|
|
353
|
-
const mimeType = readString(requireKey(
|
|
351
|
+
const record2 = readRecord(value, path, ["id", "kind", "contentHash", "byteLength", "mimeType", "payload"]);
|
|
352
|
+
const kind = readEnum(requireKey(record2, "kind", path), `${path}.kind`, ["buffer", "image"]);
|
|
353
|
+
const mimeType = readString(requireKey(record2, "mimeType", path), `${path}.mimeType`, 128, MIME_TYPE).toLowerCase();
|
|
354
354
|
if (kind === "image" ? !IMAGE_MIME_TYPES.has(mimeType) : !BUFFER_MIME_TYPES.has(mimeType)) {
|
|
355
355
|
return fail("invalid-value", `${path}.mimeType`, `unsupported ${kind} MIME type`);
|
|
356
356
|
}
|
|
357
|
-
const payload = requireKey(
|
|
357
|
+
const payload = requireKey(record2, "payload", path);
|
|
358
358
|
if (!(payload instanceof Blob)) return fail("invalid-type", `${path}.payload`, "expected an immutable Blob payload");
|
|
359
359
|
if (Object.getPrototypeOf(payload) !== Blob.prototype || Reflect.ownKeys(payload).some((key) => typeof key === "string")) {
|
|
360
360
|
return fail("invalid-type", `${path}.payload`, "expected a native Blob without caller-defined properties");
|
|
361
361
|
}
|
|
362
|
-
const byteLength = readSafeInteger(requireKey(
|
|
362
|
+
const byteLength = readSafeInteger(requireKey(record2, "byteLength", path), `${path}.byteLength`, 1);
|
|
363
363
|
if (intrinsicBlobSize(payload) !== byteLength) return fail("payload-length-mismatch", `${path}.payload`, "payload byte length does not match byteLength");
|
|
364
364
|
const payloadType = intrinsicBlobType(payload);
|
|
365
365
|
if (payloadType && payloadType.toLowerCase() !== mimeType) return fail("invalid-value", `${path}.payload`, "payload MIME type does not match mimeType");
|
|
366
366
|
return {
|
|
367
|
-
id: readId(requireKey(
|
|
367
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
368
368
|
kind,
|
|
369
|
-
contentHash: readSha256(requireKey(
|
|
369
|
+
contentHash: readSha256(requireKey(record2, "contentHash", path), `${path}.contentHash`),
|
|
370
370
|
byteLength,
|
|
371
371
|
mimeType,
|
|
372
372
|
payload
|
|
@@ -375,11 +375,11 @@ function parseResourceHeader(value, path) {
|
|
|
375
375
|
function resolveVerificationLimits(options) {
|
|
376
376
|
const overrides = options?.limits;
|
|
377
377
|
if (overrides === void 0) return GPU_MODEL_RESOURCE_VERIFICATION_LIMITS;
|
|
378
|
-
const
|
|
378
|
+
const record2 = readRecord(overrides, "$.verification.limits", Object.keys(GPU_MODEL_RESOURCE_VERIFICATION_LIMITS));
|
|
379
379
|
const output = { ...GPU_MODEL_RESOURCE_VERIFICATION_LIMITS };
|
|
380
380
|
for (const [key, ceiling] of Object.entries(GPU_MODEL_RESOURCE_VERIFICATION_LIMITS)) {
|
|
381
|
-
if (!Object.hasOwn(
|
|
382
|
-
const value = readSafeInteger(
|
|
381
|
+
if (!Object.hasOwn(record2, key)) continue;
|
|
382
|
+
const value = readSafeInteger(record2[key], `$.verification.limits.${key}`, 1, ceiling);
|
|
383
383
|
Object.assign(output, { [key]: value });
|
|
384
384
|
}
|
|
385
385
|
if (output.maxImageBytes > output.maxResourceBytes) output.maxImageBytes = output.maxResourceBytes;
|
|
@@ -432,7 +432,7 @@ function verificationAbortError(signal, path) {
|
|
|
432
432
|
function throwIfVerificationAborted(signal, path) {
|
|
433
433
|
if (signal.aborted) throw verificationAbortError(signal, path);
|
|
434
434
|
}
|
|
435
|
-
function createBlobChunks(blob, chunkBytes, observed, signal, path,
|
|
435
|
+
function createBlobChunks(blob, chunkBytes, observed, signal, path, snapshot2) {
|
|
436
436
|
return {
|
|
437
437
|
async *[Symbol.asyncIterator]() {
|
|
438
438
|
const size = intrinsicBlobSize(blob);
|
|
@@ -442,7 +442,7 @@ function createBlobChunks(blob, chunkBytes, observed, signal, path, snapshot) {
|
|
|
442
442
|
const chunk = new Uint8Array(await intrinsicBlobArrayBuffer(intrinsicBlobSlice(blob, offset, end)));
|
|
443
443
|
throwIfVerificationAborted(signal, path);
|
|
444
444
|
observed.bytes += chunk.byteLength;
|
|
445
|
-
if (
|
|
445
|
+
if (snapshot2) snapshot2.set(chunk, offset);
|
|
446
446
|
yield chunk;
|
|
447
447
|
}
|
|
448
448
|
throwIfVerificationAborted(signal, path);
|
|
@@ -491,13 +491,13 @@ function createVerificationDeadline(timeoutMs, callerSignal, path) {
|
|
|
491
491
|
};
|
|
492
492
|
}
|
|
493
493
|
function parseInspection(value, path) {
|
|
494
|
-
const
|
|
494
|
+
const record2 = readRecord(value, path, ["valid", "detectedMimeType", "width", "height"]);
|
|
495
495
|
const output = {
|
|
496
|
-
valid: readBoolean(requireKey(
|
|
497
|
-
detectedMimeType: readString(requireKey(
|
|
496
|
+
valid: readBoolean(requireKey(record2, "valid", path), `${path}.valid`),
|
|
497
|
+
detectedMimeType: readString(requireKey(record2, "detectedMimeType", path), `${path}.detectedMimeType`, 128, MIME_TYPE).toLowerCase()
|
|
498
498
|
};
|
|
499
|
-
assignOptional(output, "width", readOptional(
|
|
500
|
-
assignOptional(output, "height", readOptional(
|
|
499
|
+
assignOptional(output, "width", readOptional(record2, "width", (entry, entryPath) => readSafeInteger(entry, entryPath, 1), path));
|
|
500
|
+
assignOptional(output, "height", readOptional(record2, "height", (entry, entryPath) => readSafeInteger(entry, entryPath, 1), path));
|
|
501
501
|
return output;
|
|
502
502
|
}
|
|
503
503
|
function bytesStartWith(bytes, signature) {
|
|
@@ -570,12 +570,12 @@ async function verifyParsedResource(resource, port, limits, options, path = "$.r
|
|
|
570
570
|
} else if (inspection.width !== void 0 || inspection.height !== void 0) {
|
|
571
571
|
return fail("resource-verification-failed", path, "buffer inspection must not report image dimensions");
|
|
572
572
|
}
|
|
573
|
-
const
|
|
573
|
+
const snapshot2 = new Uint8Array(resource.byteLength);
|
|
574
574
|
const digestObserved = { bytes: 0 };
|
|
575
575
|
let digest;
|
|
576
576
|
try {
|
|
577
577
|
digest = await deadline.race(() => port.digestSha256(
|
|
578
|
-
createBlobChunks(resource.payload, limits.chunkBytes, digestObserved, deadline.signal, path,
|
|
578
|
+
createBlobChunks(resource.payload, limits.chunkBytes, digestObserved, deadline.signal, path, snapshot2),
|
|
579
579
|
context
|
|
580
580
|
));
|
|
581
581
|
} catch (error) {
|
|
@@ -588,13 +588,13 @@ async function verifyParsedResource(resource, port, limits, options, path = "$.r
|
|
|
588
588
|
if (typeof digest !== "string" || !SHA_256.test(digest) || digest !== resource.contentHash) {
|
|
589
589
|
return fail("resource-verification-failed", path, "resource digest does not match contentHash");
|
|
590
590
|
}
|
|
591
|
-
if (resource.kind === "image") validateImageMagic(resource,
|
|
591
|
+
if (resource.kind === "image") validateImageMagic(resource, snapshot2, inspection, path);
|
|
592
592
|
const verified = Object.freeze({
|
|
593
593
|
...resource,
|
|
594
|
-
payload: new Blob([
|
|
594
|
+
payload: new Blob([snapshot2], { type: resource.mimeType })
|
|
595
595
|
});
|
|
596
596
|
verifiedResources.add(verified);
|
|
597
|
-
verifiedResourceBytes.set(verified,
|
|
597
|
+
verifiedResourceBytes.set(verified, snapshot2);
|
|
598
598
|
verifiedResourceEvidence.set(verified, Object.freeze({ ...inspection }));
|
|
599
599
|
return verified;
|
|
600
600
|
} finally {
|
|
@@ -612,31 +612,31 @@ async function verifyGpuModelResource(value, port, options) {
|
|
|
612
612
|
}
|
|
613
613
|
}
|
|
614
614
|
function parseNode(value, path, budget) {
|
|
615
|
-
const
|
|
616
|
-
const children = Object.freeze(readArray(requireKey(
|
|
615
|
+
const record2 = readRecord(value, path, ["id", "name", "children", "meshId", "skinId", "localMatrix", "metadata"]);
|
|
616
|
+
const children = Object.freeze(readArray(requireKey(record2, "children", path), `${path}.children`, GPU_MODEL_DOCUMENT_LIMITS.nodes).map((entry, index) => readId(entry, `${path}.children[${index}]`)));
|
|
617
617
|
consumeBudget(budget, "sceneEdges", children.length, GPU_MODEL_DOCUMENT_LIMITS.sceneEdges, path, "scene edge");
|
|
618
618
|
ensureUnique(children, `${path}.children`, "duplicate child node");
|
|
619
619
|
const output = {
|
|
620
|
-
id: readId(requireKey(
|
|
620
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
621
621
|
children,
|
|
622
|
-
localMatrix: parseMatrix(requireKey(
|
|
622
|
+
localMatrix: parseMatrix(requireKey(record2, "localMatrix", path), `${path}.localMatrix`)
|
|
623
623
|
};
|
|
624
|
-
assignOptional(output, "name", readOptional(
|
|
625
|
-
assignOptional(output, "meshId", readOptional(
|
|
626
|
-
assignOptional(output, "skinId", readOptional(
|
|
627
|
-
assignOptional(output, "metadata", readOptional(
|
|
624
|
+
assignOptional(output, "name", readOptional(record2, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
625
|
+
assignOptional(output, "meshId", readOptional(record2, "meshId", readId, path));
|
|
626
|
+
assignOptional(output, "skinId", readOptional(record2, "skinId", readId, path));
|
|
627
|
+
assignOptional(output, "metadata", readOptional(record2, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
628
628
|
return output;
|
|
629
629
|
}
|
|
630
630
|
function parseAccessor(value, path, budget) {
|
|
631
|
-
const
|
|
632
|
-
const componentType = readEnum(requireKey(
|
|
633
|
-
const elementType = readEnum(requireKey(
|
|
634
|
-
const normalized = readOptional(
|
|
631
|
+
const record2 = readRecord(value, path, ["id", "resourceId", "byteOffset", "byteStride", "count", "componentType", "elementType", "normalized", "min", "max"]);
|
|
632
|
+
const componentType = readEnum(requireKey(record2, "componentType", path), `${path}.componentType`, ["i8", "u8", "i16", "u16", "u32", "f32"]);
|
|
633
|
+
const elementType = readEnum(requireKey(record2, "elementType", path), `${path}.elementType`, ["scalar", "vec2", "vec3", "vec4", "mat2", "mat3", "mat4"]);
|
|
634
|
+
const normalized = readOptional(record2, "normalized", readBoolean, path);
|
|
635
635
|
if (normalized === true && componentType === "f32") return fail("invalid-value", `${path}.normalized`, "floating-point accessors cannot be normalized");
|
|
636
636
|
if (elementType.startsWith("mat") && componentType !== "f32") return fail("invalid-value", `${path}.componentType`, "matrix accessors must use f32 components");
|
|
637
637
|
const expectedComponents = ELEMENT_COMPONENTS[elementType];
|
|
638
|
-
const min = readOptional(
|
|
639
|
-
const max = readOptional(
|
|
638
|
+
const min = readOptional(record2, "min", (entry, entryPath) => readNumberTuple(entry, entryPath, expectedComponents), path);
|
|
639
|
+
const max = readOptional(record2, "max", (entry, entryPath) => readNumberTuple(entry, entryPath, expectedComponents), path);
|
|
640
640
|
if (min === void 0 !== (max === void 0)) return fail("invalid-value", path, "accessor min and max must be supplied together");
|
|
641
641
|
if (min && max) {
|
|
642
642
|
const [componentMinimum, componentMaximum, integer] = COMPONENT_RANGES[componentType];
|
|
@@ -649,25 +649,25 @@ function parseAccessor(value, path, budget) {
|
|
|
649
649
|
}
|
|
650
650
|
}
|
|
651
651
|
}
|
|
652
|
-
const count = readSafeInteger(requireKey(
|
|
652
|
+
const count = readSafeInteger(requireKey(record2, "count", path), `${path}.count`, 1, GPU_MODEL_DOCUMENT_LIMITS.weightVertices);
|
|
653
653
|
consumeBudget(budget, "accessorElements", count * expectedComponents, GPU_MODEL_DOCUMENT_LIMITS.accessorElements, path, "accessor element component");
|
|
654
654
|
const output = {
|
|
655
|
-
id: readId(requireKey(
|
|
656
|
-
resourceId: readId(requireKey(
|
|
657
|
-
byteOffset: readSafeInteger(requireKey(
|
|
655
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
656
|
+
resourceId: readId(requireKey(record2, "resourceId", path), `${path}.resourceId`),
|
|
657
|
+
byteOffset: readSafeInteger(requireKey(record2, "byteOffset", path), `${path}.byteOffset`),
|
|
658
658
|
count,
|
|
659
659
|
componentType,
|
|
660
660
|
elementType
|
|
661
661
|
};
|
|
662
|
-
assignOptional(output, "byteStride", readOptional(
|
|
662
|
+
assignOptional(output, "byteStride", readOptional(record2, "byteStride", (entry, entryPath) => readSafeInteger(entry, entryPath, 1, 2048), path));
|
|
663
663
|
assignOptional(output, "normalized", normalized);
|
|
664
664
|
assignOptional(output, "min", min);
|
|
665
665
|
assignOptional(output, "max", max);
|
|
666
666
|
return output;
|
|
667
667
|
}
|
|
668
668
|
function parsePrimitive(value, path, budget) {
|
|
669
|
-
const
|
|
670
|
-
const attributesInput = readArray(requireKey(
|
|
669
|
+
const record2 = readRecord(value, path, ["id", "topology", "attributes", "indicesAccessorId", "materialId", "metadata"]);
|
|
670
|
+
const attributesInput = readArray(requireKey(record2, "attributes", path), `${path}.attributes`, 64, 1);
|
|
671
671
|
consumeBudget(budget, "primitiveAttributes", attributesInput.length, GPU_MODEL_DOCUMENT_LIMITS.primitiveAttributes, path, "primitive attribute");
|
|
672
672
|
const attributes = Object.freeze(attributesInput.map((entry, index) => {
|
|
673
673
|
const entryPath = `${path}.attributes[${index}]`;
|
|
@@ -680,53 +680,53 @@ function parsePrimitive(value, path, budget) {
|
|
|
680
680
|
ensureUnique(attributes.map(({ semantic }) => semantic), `${path}.attributes`, "duplicate attribute semantic");
|
|
681
681
|
if (!attributes.some(({ semantic }) => semantic === "POSITION")) return fail("invalid-value", `${path}.attributes`, "primitive requires POSITION");
|
|
682
682
|
const output = {
|
|
683
|
-
id: readId(requireKey(
|
|
684
|
-
topology: readEnum(requireKey(
|
|
683
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
684
|
+
topology: readEnum(requireKey(record2, "topology", path), `${path}.topology`, ["points", "lines", "line-strip", "triangles", "triangle-strip"]),
|
|
685
685
|
attributes
|
|
686
686
|
};
|
|
687
|
-
assignOptional(output, "indicesAccessorId", readOptional(
|
|
688
|
-
assignOptional(output, "materialId", readOptional(
|
|
689
|
-
assignOptional(output, "metadata", readOptional(
|
|
687
|
+
assignOptional(output, "indicesAccessorId", readOptional(record2, "indicesAccessorId", readId, path));
|
|
688
|
+
assignOptional(output, "materialId", readOptional(record2, "materialId", readId, path));
|
|
689
|
+
assignOptional(output, "metadata", readOptional(record2, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
690
690
|
return output;
|
|
691
691
|
}
|
|
692
692
|
function parseMesh(value, path, budget) {
|
|
693
|
-
const
|
|
694
|
-
const input = readArray(requireKey(
|
|
693
|
+
const record2 = readRecord(value, path, ["id", "name", "primitives", "metadata"]);
|
|
694
|
+
const input = readArray(requireKey(record2, "primitives", path), `${path}.primitives`, GPU_MODEL_DOCUMENT_LIMITS.primitives, 1);
|
|
695
695
|
consumeBudget(budget, "primitives", input.length, GPU_MODEL_DOCUMENT_LIMITS.primitives, path, "primitive");
|
|
696
696
|
const output = {
|
|
697
|
-
id: readId(requireKey(
|
|
697
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
698
698
|
primitives: Object.freeze(input.map((entry, index) => parsePrimitive(entry, `${path}.primitives[${index}]`, budget)))
|
|
699
699
|
};
|
|
700
|
-
assignOptional(output, "name", readOptional(
|
|
701
|
-
assignOptional(output, "metadata", readOptional(
|
|
700
|
+
assignOptional(output, "name", readOptional(record2, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
701
|
+
assignOptional(output, "metadata", readOptional(record2, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
702
702
|
return output;
|
|
703
703
|
}
|
|
704
704
|
function parseTextureTransform(value, path) {
|
|
705
|
-
const
|
|
706
|
-
if (Reflect.ownKeys(
|
|
705
|
+
const record2 = readRecord(value, path, ["offset", "rotationRadians", "scale"]);
|
|
706
|
+
if (Reflect.ownKeys(record2).length === 0) return fail("invalid-value", path, "texture transform cannot be empty");
|
|
707
707
|
const output = {};
|
|
708
|
-
assignOptional(output, "offset", readOptional(
|
|
709
|
-
assignOptional(output, "rotationRadians", readOptional(
|
|
710
|
-
assignOptional(output, "scale", readOptional(
|
|
708
|
+
assignOptional(output, "offset", readOptional(record2, "offset", (entry, entryPath) => readNumberTuple(entry, entryPath, 2), path));
|
|
709
|
+
assignOptional(output, "rotationRadians", readOptional(record2, "rotationRadians", readFinite, path));
|
|
710
|
+
assignOptional(output, "scale", readOptional(record2, "scale", (entry, entryPath) => readNumberTuple(entry, entryPath, 2), path));
|
|
711
711
|
return output;
|
|
712
712
|
}
|
|
713
713
|
function parseTextureBinding(value, path) {
|
|
714
|
-
const
|
|
714
|
+
const record2 = readRecord(value, path, ["textureId", "samplerId", "texCoordSet", "transform", "colorSpace", "intendedUsage"]);
|
|
715
715
|
const output = {
|
|
716
|
-
textureId: readId(requireKey(
|
|
717
|
-
colorSpace: readEnum(requireKey(
|
|
718
|
-
intendedUsage: readEnum(requireKey(
|
|
716
|
+
textureId: readId(requireKey(record2, "textureId", path), `${path}.textureId`),
|
|
717
|
+
colorSpace: readEnum(requireKey(record2, "colorSpace", path), `${path}.colorSpace`, ["srgb", "linear", "normal-map", "data"]),
|
|
718
|
+
intendedUsage: readEnum(requireKey(record2, "intendedUsage", path), `${path}.intendedUsage`, TEXTURE_USAGES)
|
|
719
719
|
};
|
|
720
|
-
assignOptional(output, "samplerId", readOptional(
|
|
721
|
-
assignOptional(output, "texCoordSet", readOptional(
|
|
722
|
-
assignOptional(output, "transform", readOptional(
|
|
720
|
+
assignOptional(output, "samplerId", readOptional(record2, "samplerId", readId, path));
|
|
721
|
+
assignOptional(output, "texCoordSet", readOptional(record2, "texCoordSet", (entry, entryPath) => readSafeInteger(entry, entryPath, 0, 7), path));
|
|
722
|
+
assignOptional(output, "transform", readOptional(record2, "transform", parseTextureTransform, path));
|
|
723
723
|
return output;
|
|
724
724
|
}
|
|
725
725
|
function parseMaterialTextures(value, path) {
|
|
726
726
|
const slots = Object.keys(MATERIAL_TEXTURE_SLOTS);
|
|
727
|
-
const
|
|
727
|
+
const record2 = readRecord(value, path, slots);
|
|
728
728
|
const output = {};
|
|
729
|
-
for (const slot of slots) assignOptional(output, slot, readOptional(
|
|
729
|
+
for (const slot of slots) assignOptional(output, slot, readOptional(record2, slot, parseTextureBinding, path));
|
|
730
730
|
return output;
|
|
731
731
|
}
|
|
732
732
|
function parseMaterial(value, path, budget) {
|
|
@@ -752,88 +752,88 @@ function parseMaterial(value, path, budget) {
|
|
|
752
752
|
"extensions",
|
|
753
753
|
"sourceMetadata"
|
|
754
754
|
];
|
|
755
|
-
const
|
|
756
|
-
const alphaMode = readEnum(requireKey(
|
|
757
|
-
const alphaCutoff = readOptional(
|
|
755
|
+
const record2 = readRecord(value, path, keys);
|
|
756
|
+
const alphaMode = readEnum(requireKey(record2, "alphaMode", path), `${path}.alphaMode`, ["opaque", "mask", "blend"]);
|
|
757
|
+
const alphaCutoff = readOptional(record2, "alphaCutoff", (entry, entryPath) => readFinite(entry, entryPath, 0, 1), path);
|
|
758
758
|
if (alphaMode === "mask" && alphaCutoff === void 0) return fail("invalid-value", `${path}.alphaCutoff`, "mask materials require alphaCutoff");
|
|
759
759
|
if (alphaMode !== "mask" && alphaCutoff !== void 0) return fail("invalid-value", `${path}.alphaCutoff`, "alphaCutoff is only valid for mask materials");
|
|
760
760
|
const output = {
|
|
761
|
-
id: readId(requireKey(
|
|
762
|
-
workflow: readEnum(requireKey(
|
|
761
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
762
|
+
workflow: readEnum(requireKey(record2, "workflow", path), `${path}.workflow`, ["metallic-roughness", "specular-glossiness", "unlit", "custom"]),
|
|
763
763
|
alphaMode,
|
|
764
|
-
baseColorFactor: readNumberTuple(requireKey(
|
|
765
|
-
textures: parseMaterialTextures(requireKey(
|
|
766
|
-
extensions: readMetadata(requireKey(
|
|
767
|
-
sourceMetadata: readMetadata(requireKey(
|
|
764
|
+
baseColorFactor: readNumberTuple(requireKey(record2, "baseColorFactor", path), `${path}.baseColorFactor`, 4, 0, 1),
|
|
765
|
+
textures: parseMaterialTextures(requireKey(record2, "textures", path), `${path}.textures`),
|
|
766
|
+
extensions: readMetadata(requireKey(record2, "extensions", path), `${path}.extensions`, budget),
|
|
767
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
768
768
|
};
|
|
769
|
-
assignOptional(output, "name", readOptional(
|
|
769
|
+
assignOptional(output, "name", readOptional(record2, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
770
770
|
assignOptional(output, "alphaCutoff", alphaCutoff);
|
|
771
|
-
assignOptional(output, "doubleSided", readOptional(
|
|
771
|
+
assignOptional(output, "doubleSided", readOptional(record2, "doubleSided", readBoolean, path));
|
|
772
772
|
for (const key of ["metallicFactor", "roughnessFactor", "transmissionFactor", "clearcoatFactor", "clearcoatRoughnessFactor", "sheenRoughnessFactor", "occlusionStrength"]) {
|
|
773
|
-
assignOptional(output, key, readOptional(
|
|
773
|
+
assignOptional(output, key, readOptional(record2, key, (entry, entryPath) => readFinite(entry, entryPath, 0, 1), path));
|
|
774
774
|
}
|
|
775
|
-
assignOptional(output, "normalScale", readOptional(
|
|
776
|
-
assignOptional(output, "emissiveFactor", readOptional(
|
|
777
|
-
assignOptional(output, "sheenColorFactor", readOptional(
|
|
775
|
+
assignOptional(output, "normalScale", readOptional(record2, "normalScale", readFinite, path));
|
|
776
|
+
assignOptional(output, "emissiveFactor", readOptional(record2, "emissiveFactor", (entry, entryPath) => readNumberTuple(entry, entryPath, 3, 0), path));
|
|
777
|
+
assignOptional(output, "sheenColorFactor", readOptional(record2, "sheenColorFactor", (entry, entryPath) => readNumberTuple(entry, entryPath, 3, 0, 1), path));
|
|
778
778
|
return output;
|
|
779
779
|
}
|
|
780
780
|
function parseTextureSampler(value, path) {
|
|
781
|
-
const
|
|
782
|
-
const output = { id: readId(requireKey(
|
|
783
|
-
assignOptional(output, "wrapS", readOptional(
|
|
784
|
-
assignOptional(output, "wrapT", readOptional(
|
|
785
|
-
assignOptional(output, "minFilter", readOptional(
|
|
786
|
-
assignOptional(output, "magFilter", readOptional(
|
|
787
|
-
assignOptional(output, "mipFilter", readOptional(
|
|
781
|
+
const record2 = readRecord(value, path, ["id", "wrapS", "wrapT", "minFilter", "magFilter", "mipFilter"]);
|
|
782
|
+
const output = { id: readId(requireKey(record2, "id", path), `${path}.id`) };
|
|
783
|
+
assignOptional(output, "wrapS", readOptional(record2, "wrapS", (entry, entryPath) => readEnum(entry, entryPath, ["repeat", "mirrored-repeat", "clamp-to-edge"]), path));
|
|
784
|
+
assignOptional(output, "wrapT", readOptional(record2, "wrapT", (entry, entryPath) => readEnum(entry, entryPath, ["repeat", "mirrored-repeat", "clamp-to-edge"]), path));
|
|
785
|
+
assignOptional(output, "minFilter", readOptional(record2, "minFilter", (entry, entryPath) => readString(entry, entryPath, 128, TOKEN), path));
|
|
786
|
+
assignOptional(output, "magFilter", readOptional(record2, "magFilter", (entry, entryPath) => readString(entry, entryPath, 128, TOKEN), path));
|
|
787
|
+
assignOptional(output, "mipFilter", readOptional(record2, "mipFilter", (entry, entryPath) => readEnum(entry, entryPath, ["nearest", "linear"]), path));
|
|
788
788
|
return output;
|
|
789
789
|
}
|
|
790
790
|
function parseTexture(value, path, budget) {
|
|
791
|
-
const
|
|
792
|
-
const usages = Object.freeze(readArray(requireKey(
|
|
791
|
+
const record2 = readRecord(value, path, ["id", "fileName", "relativePath", "mimeType", "contentHash", "imageResourceId", "byteLength", "intendedUsage", "colorSpace", "sampler", "exportEmbeddingPolicy", "sourceMetadata"]);
|
|
792
|
+
const usages = Object.freeze(readArray(requireKey(record2, "intendedUsage", path), `${path}.intendedUsage`, TEXTURE_USAGES.length, 1).map((entry, index) => readEnum(entry, `${path}.intendedUsage[${index}]`, TEXTURE_USAGES)));
|
|
793
793
|
ensureUnique(usages, `${path}.intendedUsage`, "duplicate intended texture usage");
|
|
794
794
|
const output = {
|
|
795
|
-
id: readId(requireKey(
|
|
796
|
-
contentHash: readSha256(requireKey(
|
|
797
|
-
imageResourceId: readId(requireKey(
|
|
795
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
796
|
+
contentHash: readSha256(requireKey(record2, "contentHash", path), `${path}.contentHash`),
|
|
797
|
+
imageResourceId: readId(requireKey(record2, "imageResourceId", path), `${path}.imageResourceId`),
|
|
798
798
|
intendedUsage: usages,
|
|
799
|
-
colorSpace: readEnum(requireKey(
|
|
800
|
-
exportEmbeddingPolicy: readEnum(requireKey(
|
|
801
|
-
sourceMetadata: readMetadata(requireKey(
|
|
799
|
+
colorSpace: readEnum(requireKey(record2, "colorSpace", path), `${path}.colorSpace`, ["srgb", "linear", "normal-map", "data"]),
|
|
800
|
+
exportEmbeddingPolicy: readEnum(requireKey(record2, "exportEmbeddingPolicy", path), `${path}.exportEmbeddingPolicy`, ["inherit-source", "prefer-embedded", "prefer-external", "forbid-embedding"]),
|
|
801
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
802
802
|
};
|
|
803
|
-
assignOptional(output, "fileName", readOptional(
|
|
804
|
-
assignOptional(output, "relativePath", readOptional(
|
|
805
|
-
assignOptional(output, "mimeType", readOptional(
|
|
806
|
-
assignOptional(output, "byteLength", readOptional(
|
|
807
|
-
assignOptional(output, "sampler", readOptional(
|
|
803
|
+
assignOptional(output, "fileName", readOptional(record2, "fileName", (entry, entryPath) => readString(entry, entryPath, 512, FILE_NAME), path));
|
|
804
|
+
assignOptional(output, "relativePath", readOptional(record2, "relativePath", (entry, entryPath) => readString(entry, entryPath, 2048, RELATIVE_PATH), path));
|
|
805
|
+
assignOptional(output, "mimeType", readOptional(record2, "mimeType", (entry, entryPath) => readString(entry, entryPath, 128, MIME_TYPE).toLowerCase(), path));
|
|
806
|
+
assignOptional(output, "byteLength", readOptional(record2, "byteLength", (entry, entryPath) => readSafeInteger(entry, entryPath, 1), path));
|
|
807
|
+
assignOptional(output, "sampler", readOptional(record2, "sampler", parseTextureSampler, path));
|
|
808
808
|
return output;
|
|
809
809
|
}
|
|
810
810
|
function parseSkeleton(value, path, budget) {
|
|
811
|
-
const
|
|
812
|
-
const jointIds = Object.freeze(readArray(requireKey(
|
|
813
|
-
const rootJointIds = Object.freeze(readArray(requireKey(
|
|
811
|
+
const record2 = readRecord(value, path, ["id", "name", "jointIds", "rootJointIds", "sourceMetadata"]);
|
|
812
|
+
const jointIds = Object.freeze(readArray(requireKey(record2, "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(record2, "rootJointIds", path), `${path}.rootJointIds`, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, 1).map((entry, index) => readId(entry, `${path}.rootJointIds[${index}]`)));
|
|
814
814
|
consumeBudget(budget, "skinJoints", jointIds.length + rootJointIds.length, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, path, "rig joint");
|
|
815
815
|
ensureUnique(jointIds, `${path}.jointIds`, "duplicate skeleton joint");
|
|
816
816
|
ensureUnique(rootJointIds, `${path}.rootJointIds`, "duplicate root joint");
|
|
817
817
|
const output = {
|
|
818
|
-
id: readId(requireKey(
|
|
818
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
819
819
|
jointIds,
|
|
820
820
|
rootJointIds,
|
|
821
|
-
sourceMetadata: readMetadata(requireKey(
|
|
821
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
822
822
|
};
|
|
823
|
-
assignOptional(output, "name", readOptional(
|
|
823
|
+
assignOptional(output, "name", readOptional(record2, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
824
824
|
return output;
|
|
825
825
|
}
|
|
826
826
|
function parseJoint(value, path, budget) {
|
|
827
|
-
const
|
|
827
|
+
const record2 = readRecord(value, path, ["id", "name", "nodeId", "parentJointId", "restLocalTransform", "inverseBindMatrix", "sourceMetadata"]);
|
|
828
828
|
const output = {
|
|
829
|
-
id: readId(requireKey(
|
|
830
|
-
nodeId: readId(requireKey(
|
|
831
|
-
restLocalTransform: parseTransformTrs(requireKey(
|
|
832
|
-
sourceMetadata: readMetadata(requireKey(
|
|
829
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
830
|
+
nodeId: readId(requireKey(record2, "nodeId", path), `${path}.nodeId`),
|
|
831
|
+
restLocalTransform: parseTransformTrs(requireKey(record2, "restLocalTransform", path), `${path}.restLocalTransform`),
|
|
832
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
833
833
|
};
|
|
834
|
-
assignOptional(output, "name", readOptional(
|
|
835
|
-
assignOptional(output, "parentJointId", readOptional(
|
|
836
|
-
assignOptional(output, "inverseBindMatrix", readOptional(
|
|
834
|
+
assignOptional(output, "name", readOptional(record2, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
835
|
+
assignOptional(output, "parentJointId", readOptional(record2, "parentJointId", readId, path));
|
|
836
|
+
assignOptional(output, "inverseBindMatrix", readOptional(record2, "inverseBindMatrix", parseMatrix, path));
|
|
837
837
|
return output;
|
|
838
838
|
}
|
|
839
839
|
function parseWeightRows(value, path, rows, columns, integer, budget) {
|
|
@@ -847,43 +847,43 @@ function parseWeightRows(value, path, rows, columns, integer, budget) {
|
|
|
847
847
|
}));
|
|
848
848
|
}
|
|
849
849
|
function parseVertexWeightSet(value, path, budget) {
|
|
850
|
-
const
|
|
851
|
-
const influences = readSafeInteger(requireKey(
|
|
850
|
+
const record2 = readRecord(value, path, ["primitiveId", "influencesPerVertex", "joints", "weights"]);
|
|
851
|
+
const influences = readSafeInteger(requireKey(record2, "influencesPerVertex", path), `${path}.influencesPerVertex`, 1, 32);
|
|
852
852
|
if (influences % 4 !== 0) return fail("invalid-value", `${path}.influencesPerVertex`, "influencesPerVertex must be a multiple of four");
|
|
853
|
-
const jointRows = readArray(requireKey(
|
|
854
|
-
const weightRows = readArray(requireKey(
|
|
853
|
+
const jointRows = readArray(requireKey(record2, "joints", path), `${path}.joints`, GPU_MODEL_DOCUMENT_LIMITS.weightVertices, 1);
|
|
854
|
+
const weightRows = readArray(requireKey(record2, "weights", path), `${path}.weights`, GPU_MODEL_DOCUMENT_LIMITS.weightVertices, 1);
|
|
855
855
|
if (jointRows.length !== weightRows.length) return fail("invalid-value", path, "joint and weight row counts must match");
|
|
856
856
|
return {
|
|
857
|
-
primitiveId: readId(requireKey(
|
|
857
|
+
primitiveId: readId(requireKey(record2, "primitiveId", path), `${path}.primitiveId`),
|
|
858
858
|
influencesPerVertex: influences,
|
|
859
859
|
joints: parseWeightRows(jointRows, `${path}.joints`, jointRows.length, influences, true, budget),
|
|
860
860
|
weights: parseWeightRows(weightRows, `${path}.weights`, weightRows.length, influences, false, budget)
|
|
861
861
|
};
|
|
862
862
|
}
|
|
863
863
|
function parseSkin(value, path, budget) {
|
|
864
|
-
const
|
|
865
|
-
const jointIds = Object.freeze(readArray(requireKey(
|
|
864
|
+
const record2 = readRecord(value, path, ["id", "skeletonId", "jointIds", "inverseBindMatrices", "weightSets", "sourceMetadata"]);
|
|
865
|
+
const jointIds = Object.freeze(readArray(requireKey(record2, "jointIds", path), `${path}.jointIds`, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, 1).map((entry, index) => readId(entry, `${path}.jointIds[${index}]`)));
|
|
866
866
|
consumeBudget(budget, "skinJoints", jointIds.length, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, path, "skin joint");
|
|
867
867
|
ensureUnique(jointIds, `${path}.jointIds`, "duplicate skin joint");
|
|
868
|
-
const inverseInput = readArray(requireKey(
|
|
868
|
+
const inverseInput = readArray(requireKey(record2, "inverseBindMatrices", path), `${path}.inverseBindMatrices`, GPU_MODEL_DOCUMENT_LIMITS.skinJoints, jointIds.length);
|
|
869
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(
|
|
870
|
+
const weightInput = readArray(requireKey(record2, "weightSets", path), `${path}.weightSets`, GPU_MODEL_DOCUMENT_LIMITS.primitives, 1);
|
|
871
871
|
const weightSets = Object.freeze(weightInput.map((entry, index) => parseVertexWeightSet(entry, `${path}.weightSets[${index}]`, budget)));
|
|
872
872
|
ensureUnique(weightSets.map(({ primitiveId }) => primitiveId), `${path}.weightSets`, "duplicate primitive weight set");
|
|
873
873
|
return {
|
|
874
|
-
id: readId(requireKey(
|
|
875
|
-
skeletonId: readId(requireKey(
|
|
874
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
875
|
+
skeletonId: readId(requireKey(record2, "skeletonId", path), `${path}.skeletonId`),
|
|
876
876
|
jointIds,
|
|
877
877
|
inverseBindMatrices: Object.freeze(inverseInput.map((entry, index) => parseMatrix(entry, `${path}.inverseBindMatrices[${index}]`))),
|
|
878
878
|
weightSets,
|
|
879
|
-
sourceMetadata: readMetadata(requireKey(
|
|
879
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
880
880
|
};
|
|
881
881
|
}
|
|
882
882
|
function parseBlendShapeDelta(value, path) {
|
|
883
|
-
const
|
|
884
|
-
const output = { primitiveId: readId(requireKey(
|
|
883
|
+
const record2 = readRecord(value, path, ["primitiveId", "positionAccessorId", "normalAccessorId", "tangentAccessorId"]);
|
|
884
|
+
const output = { primitiveId: readId(requireKey(record2, "primitiveId", path), `${path}.primitiveId`) };
|
|
885
885
|
for (const key of ["positionAccessorId", "normalAccessorId", "tangentAccessorId"]) {
|
|
886
|
-
assignOptional(output, key, readOptional(
|
|
886
|
+
assignOptional(output, key, readOptional(record2, key, readId, path));
|
|
887
887
|
}
|
|
888
888
|
if (!output.positionAccessorId && !output.normalAccessorId && !output.tangentAccessorId) {
|
|
889
889
|
return fail("invalid-value", path, "blend-shape delta requires at least one accessor");
|
|
@@ -891,8 +891,8 @@ function parseBlendShapeDelta(value, path) {
|
|
|
891
891
|
return output;
|
|
892
892
|
}
|
|
893
893
|
function parseBlendShape(value, path, budget) {
|
|
894
|
-
const
|
|
895
|
-
const deltaInput = readArray(requireKey(
|
|
894
|
+
const record2 = readRecord(value, path, ["id", "meshId", "name", "defaultWeight", "deltas", "sourceMetadata"]);
|
|
895
|
+
const deltaInput = readArray(requireKey(record2, "deltas", path), `${path}.deltas`, GPU_MODEL_DOCUMENT_LIMITS.primitives, 1);
|
|
896
896
|
consumeBudget(
|
|
897
897
|
budget,
|
|
898
898
|
"blendShapeDeltas",
|
|
@@ -904,96 +904,96 @@ function parseBlendShape(value, path, budget) {
|
|
|
904
904
|
const deltas = Object.freeze(deltaInput.map((entry, index) => parseBlendShapeDelta(entry, `${path}.deltas[${index}]`)));
|
|
905
905
|
ensureUnique(deltas.map(({ primitiveId }) => primitiveId), `${path}.deltas`, "duplicate blend-shape primitive delta");
|
|
906
906
|
const output = {
|
|
907
|
-
id: readId(requireKey(
|
|
908
|
-
meshId: readId(requireKey(
|
|
907
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
908
|
+
meshId: readId(requireKey(record2, "meshId", path), `${path}.meshId`),
|
|
909
909
|
deltas,
|
|
910
|
-
sourceMetadata: readMetadata(requireKey(
|
|
910
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
911
911
|
};
|
|
912
|
-
assignOptional(output, "name", readOptional(
|
|
913
|
-
assignOptional(output, "defaultWeight", readOptional(
|
|
912
|
+
assignOptional(output, "name", readOptional(record2, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
913
|
+
assignOptional(output, "defaultWeight", readOptional(record2, "defaultWeight", (entry, entryPath) => readFinite(entry, entryPath, 0, 1), path));
|
|
914
914
|
return output;
|
|
915
915
|
}
|
|
916
916
|
function parseAnimationSampler(value, path, budget) {
|
|
917
|
-
const
|
|
918
|
-
const timesInput = readArray(requireKey(
|
|
919
|
-
const valuesInput = readArray(requireKey(
|
|
917
|
+
const record2 = readRecord(value, path, ["id", "inputAccessorId", "outputAccessorId", "inputTimesSeconds", "outputValues", "interpolation", "sourceMetadata"]);
|
|
918
|
+
const timesInput = readArray(requireKey(record2, "inputTimesSeconds", path), `${path}.inputTimesSeconds`, GPU_MODEL_DOCUMENT_LIMITS.animationValues, 1);
|
|
919
|
+
const valuesInput = readArray(requireKey(record2, "outputValues", path), `${path}.outputValues`, GPU_MODEL_DOCUMENT_LIMITS.animationValues, 1);
|
|
920
920
|
consumeBudget(budget, "animationValues", timesInput.length + valuesInput.length, GPU_MODEL_DOCUMENT_LIMITS.animationValues, path, "animation value");
|
|
921
921
|
const times = Object.freeze(timesInput.map((entry, index) => readFinite(entry, `${path}.inputTimesSeconds[${index}]`, 0)));
|
|
922
922
|
for (let index = 1; index < times.length; index += 1) {
|
|
923
923
|
if (times[index] <= times[index - 1]) return fail("invalid-value", `${path}.inputTimesSeconds`, "animation input times must be strictly increasing");
|
|
924
924
|
}
|
|
925
925
|
return {
|
|
926
|
-
id: readId(requireKey(
|
|
927
|
-
inputAccessorId: readId(requireKey(
|
|
928
|
-
outputAccessorId: readId(requireKey(
|
|
926
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
927
|
+
inputAccessorId: readId(requireKey(record2, "inputAccessorId", path), `${path}.inputAccessorId`),
|
|
928
|
+
outputAccessorId: readId(requireKey(record2, "outputAccessorId", path), `${path}.outputAccessorId`),
|
|
929
929
|
inputTimesSeconds: times,
|
|
930
930
|
outputValues: Object.freeze(valuesInput.map((entry, index) => readFinite(entry, `${path}.outputValues[${index}]`))),
|
|
931
|
-
interpolation: readEnum(requireKey(
|
|
932
|
-
sourceMetadata: readMetadata(requireKey(
|
|
931
|
+
interpolation: readEnum(requireKey(record2, "interpolation", path), `${path}.interpolation`, ["step", "linear", "cubic-spline"]),
|
|
932
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
933
933
|
};
|
|
934
934
|
}
|
|
935
935
|
function parseAnimationChannel(value, path, budget) {
|
|
936
|
-
const
|
|
936
|
+
const record2 = readRecord(value, path, ["id", "samplerId", "targetType", "targetId", "sourceMetadata"]);
|
|
937
937
|
return {
|
|
938
|
-
id: readId(requireKey(
|
|
939
|
-
samplerId: readId(requireKey(
|
|
940
|
-
targetType: readEnum(requireKey(
|
|
941
|
-
targetId: readId(requireKey(
|
|
942
|
-
sourceMetadata: readMetadata(requireKey(
|
|
938
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
939
|
+
samplerId: readId(requireKey(record2, "samplerId", path), `${path}.samplerId`),
|
|
940
|
+
targetType: readEnum(requireKey(record2, "targetType", path), `${path}.targetType`, ["node-translation", "node-rotation", "node-scale", "weights"]),
|
|
941
|
+
targetId: readId(requireKey(record2, "targetId", path), `${path}.targetId`),
|
|
942
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
943
943
|
};
|
|
944
944
|
}
|
|
945
945
|
function parseAnimation(value, path, budget) {
|
|
946
|
-
const
|
|
947
|
-
const samplerInput = readArray(requireKey(
|
|
948
|
-
const channelInput = readArray(requireKey(
|
|
946
|
+
const record2 = readRecord(value, path, ["id", "name", "durationSeconds", "samplers", "channels", "sourceMetadata"]);
|
|
947
|
+
const samplerInput = readArray(requireKey(record2, "samplers", path), `${path}.samplers`, GPU_MODEL_DOCUMENT_LIMITS.animationSamplers, 1);
|
|
948
|
+
const channelInput = readArray(requireKey(record2, "channels", path), `${path}.channels`, GPU_MODEL_DOCUMENT_LIMITS.animationChannels, 1);
|
|
949
949
|
consumeBudget(budget, "animationSamplers", samplerInput.length, GPU_MODEL_DOCUMENT_LIMITS.animationSamplers, path, "animation sampler");
|
|
950
950
|
consumeBudget(budget, "animationChannels", channelInput.length, GPU_MODEL_DOCUMENT_LIMITS.animationChannels, path, "animation channel");
|
|
951
951
|
const output = {
|
|
952
|
-
id: readId(requireKey(
|
|
953
|
-
durationSeconds: readFinite(requireKey(
|
|
952
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
953
|
+
durationSeconds: readFinite(requireKey(record2, "durationSeconds", path), `${path}.durationSeconds`, 0),
|
|
954
954
|
samplers: Object.freeze(samplerInput.map((entry, index) => parseAnimationSampler(entry, `${path}.samplers[${index}]`, budget))),
|
|
955
955
|
channels: Object.freeze(channelInput.map((entry, index) => parseAnimationChannel(entry, `${path}.channels[${index}]`, budget))),
|
|
956
|
-
sourceMetadata: readMetadata(requireKey(
|
|
956
|
+
sourceMetadata: readMetadata(requireKey(record2, "sourceMetadata", path), `${path}.sourceMetadata`, budget)
|
|
957
957
|
};
|
|
958
|
-
assignOptional(output, "name", readOptional(
|
|
958
|
+
assignOptional(output, "name", readOptional(record2, "name", (entry, entryPath) => readString(entry, entryPath, GPU_MODEL_DOCUMENT_LIMITS.nameLength), path));
|
|
959
959
|
return output;
|
|
960
960
|
}
|
|
961
961
|
function parseAnalyticGeometry(value, path, budget) {
|
|
962
|
-
const
|
|
963
|
-
if (requireKey(
|
|
962
|
+
const record2 = readRecord(value, path, ["id", "kind", "sourceResourceId", "sourceUnit", "tessellatedMeshId", "metadata"]);
|
|
963
|
+
if (requireKey(record2, "kind", path) !== "brep-placeholder") return fail("invalid-value", `${path}.kind`, "unsupported analytic geometry kind");
|
|
964
964
|
const output = {
|
|
965
|
-
id: readId(requireKey(
|
|
965
|
+
id: readId(requireKey(record2, "id", path), `${path}.id`),
|
|
966
966
|
kind: "brep-placeholder",
|
|
967
|
-
sourceResourceId: readId(requireKey(
|
|
968
|
-
sourceUnit: readEnum(requireKey(
|
|
967
|
+
sourceResourceId: readId(requireKey(record2, "sourceResourceId", path), `${path}.sourceResourceId`),
|
|
968
|
+
sourceUnit: readEnum(requireKey(record2, "sourceUnit", path), `${path}.sourceUnit`, ["metre", "millimetre", "centimetre", "inch", "foot", "unitless"])
|
|
969
969
|
};
|
|
970
|
-
assignOptional(output, "tessellatedMeshId", readOptional(
|
|
971
|
-
assignOptional(output, "metadata", readOptional(
|
|
970
|
+
assignOptional(output, "tessellatedMeshId", readOptional(record2, "tessellatedMeshId", readId, path));
|
|
971
|
+
assignOptional(output, "metadata", readOptional(record2, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
972
972
|
return output;
|
|
973
973
|
}
|
|
974
974
|
function parseProvenance(value, budget) {
|
|
975
975
|
const path = "$.provenance";
|
|
976
|
-
const
|
|
976
|
+
const record2 = readRecord(value, path, ["sourceFormat", "sourceContentHash", "converterId", "converterVersion", "provider", "sourceIdentifier", "metadata"]);
|
|
977
977
|
const output = {
|
|
978
|
-
sourceFormat: readString(requireKey(
|
|
979
|
-
sourceContentHash: readSha256(requireKey(
|
|
980
|
-
converterId: readString(requireKey(
|
|
981
|
-
converterVersion: readString(requireKey(
|
|
978
|
+
sourceFormat: readString(requireKey(record2, "sourceFormat", path), `${path}.sourceFormat`, 128, TOKEN),
|
|
979
|
+
sourceContentHash: readSha256(requireKey(record2, "sourceContentHash", path), `${path}.sourceContentHash`),
|
|
980
|
+
converterId: readString(requireKey(record2, "converterId", path), `${path}.converterId`, 128, TOKEN),
|
|
981
|
+
converterVersion: readString(requireKey(record2, "converterVersion", path), `${path}.converterVersion`, 128, TOKEN)
|
|
982
982
|
};
|
|
983
|
-
assignOptional(output, "provider", readOptional(
|
|
984
|
-
assignOptional(output, "sourceIdentifier", readOptional(
|
|
985
|
-
assignOptional(output, "metadata", readOptional(
|
|
983
|
+
assignOptional(output, "provider", readOptional(record2, "provider", (entry, entryPath) => readString(entry, entryPath, 128, TOKEN), path));
|
|
984
|
+
assignOptional(output, "sourceIdentifier", readOptional(record2, "sourceIdentifier", (entry, entryPath) => readString(entry, entryPath, 2048), path));
|
|
985
|
+
assignOptional(output, "metadata", readOptional(record2, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
986
986
|
return output;
|
|
987
987
|
}
|
|
988
988
|
function parseDiagnostic(value, path, budget) {
|
|
989
|
-
const
|
|
989
|
+
const record2 = readRecord(value, path, ["severity", "code", "message", "path", "metadata"]);
|
|
990
990
|
const output = {
|
|
991
|
-
severity: readEnum(requireKey(
|
|
992
|
-
code: readString(requireKey(
|
|
993
|
-
message: readString(requireKey(
|
|
991
|
+
severity: readEnum(requireKey(record2, "severity", path), `${path}.severity`, ["info", "warning", "error"]),
|
|
992
|
+
code: readString(requireKey(record2, "code", path), `${path}.code`, 128, TOKEN),
|
|
993
|
+
message: readString(requireKey(record2, "message", path), `${path}.message`, GPU_MODEL_DOCUMENT_LIMITS.diagnosticMessageLength)
|
|
994
994
|
};
|
|
995
|
-
assignOptional(output, "path", readOptional(
|
|
996
|
-
assignOptional(output, "metadata", readOptional(
|
|
995
|
+
assignOptional(output, "path", readOptional(record2, "path", (entry, entryPath) => readString(entry, entryPath, 512), path));
|
|
996
|
+
assignOptional(output, "metadata", readOptional(record2, "metadata", (entry, entryPath) => readMetadata(entry, entryPath, budget), path));
|
|
997
997
|
return output;
|
|
998
998
|
}
|
|
999
999
|
function ensureUnique(values, path, detail = "duplicate identity") {
|
|
@@ -1634,11 +1634,11 @@ var DOCUMENT_KEYS = Object.freeze([
|
|
|
1634
1634
|
]);
|
|
1635
1635
|
function parseGpuModelDocument(value) {
|
|
1636
1636
|
const path = "$";
|
|
1637
|
-
const
|
|
1638
|
-
const schemaVersion = requireKey(
|
|
1637
|
+
const record2 = readRecord(value, path, DOCUMENT_KEYS);
|
|
1638
|
+
const schemaVersion = requireKey(record2, "schemaVersion", path);
|
|
1639
1639
|
if (schemaVersion !== GPU_MODEL_DOCUMENT_SCHEMA_VERSION) return fail("invalid-value", "$.schemaVersion", "unsupported schema version");
|
|
1640
1640
|
const parseCollection = (key, maximum, parser, minimum = 0) => {
|
|
1641
|
-
const input = readArray(requireKey(
|
|
1641
|
+
const input = readArray(requireKey(record2, key, path), `$.${key}`, maximum, minimum);
|
|
1642
1642
|
return Object.freeze(input.map((entry, index) => parser(entry, `$.${key}[${index}]`)));
|
|
1643
1643
|
};
|
|
1644
1644
|
const budget = {
|
|
@@ -1659,10 +1659,10 @@ function parseGpuModelDocument(value) {
|
|
|
1659
1659
|
geometryWorldAccessorInstances: 0,
|
|
1660
1660
|
geometryWorldVertices: 0
|
|
1661
1661
|
};
|
|
1662
|
-
const roots = Object.freeze(readArray(requireKey(
|
|
1662
|
+
const roots = Object.freeze(readArray(requireKey(record2, "roots", path), "$.roots", GPU_MODEL_DOCUMENT_LIMITS.roots, 1).map((entry, index) => readId(entry, `$.roots[${index}]`)));
|
|
1663
1663
|
const document = {
|
|
1664
1664
|
schemaVersion,
|
|
1665
|
-
coordinateSystem: parseCoordinateSystem(requireKey(
|
|
1665
|
+
coordinateSystem: parseCoordinateSystem(requireKey(record2, "coordinateSystem", path)),
|
|
1666
1666
|
roots,
|
|
1667
1667
|
nodes: parseCollection("nodes", GPU_MODEL_DOCUMENT_LIMITS.nodes, (entry, entryPath) => parseNode(entry, entryPath, budget), 1),
|
|
1668
1668
|
resources: parseCollection("resources", GPU_MODEL_DOCUMENT_LIMITS.resources, parseVerifiedResource, 1),
|
|
@@ -1676,10 +1676,10 @@ function parseGpuModelDocument(value) {
|
|
|
1676
1676
|
blendShapes: parseCollection("blendShapes", GPU_MODEL_DOCUMENT_LIMITS.blendShapes, (entry, entryPath) => parseBlendShape(entry, entryPath, budget)),
|
|
1677
1677
|
animations: parseCollection("animations", GPU_MODEL_DOCUMENT_LIMITS.animations, (entry, entryPath) => parseAnimation(entry, entryPath, budget)),
|
|
1678
1678
|
analyticGeometry: parseCollection("analyticGeometry", GPU_MODEL_DOCUMENT_LIMITS.analyticGeometry, (entry, entryPath) => parseAnalyticGeometry(entry, entryPath, budget)),
|
|
1679
|
-
bounds: parseBounds(requireKey(
|
|
1680
|
-
provenance: parseProvenance(requireKey(
|
|
1679
|
+
bounds: parseBounds(requireKey(record2, "bounds", path), "$.bounds"),
|
|
1680
|
+
provenance: parseProvenance(requireKey(record2, "provenance", path), budget),
|
|
1681
1681
|
diagnostics: parseCollection("diagnostics", GPU_MODEL_DOCUMENT_LIMITS.diagnostics, (entry, entryPath) => parseDiagnostic(entry, entryPath, budget)),
|
|
1682
|
-
metadata: readMetadata(requireKey(
|
|
1682
|
+
metadata: readMetadata(requireKey(record2, "metadata", path), "$.metadata", budget)
|
|
1683
1683
|
};
|
|
1684
1684
|
if (document.meshes.length === 0) return fail("invalid-value", "$.meshes", "v1 document bounds require at least one canonical mesh");
|
|
1685
1685
|
validateReferences(document, budget);
|
|
@@ -1716,8 +1716,8 @@ async function createAndVerifyGpuModelDocument(value, port, options) {
|
|
|
1716
1716
|
value.resources.forEach((resource, index) => assertVerifiedResourceEvidence(resource, limits, `$.resources[${index}]`));
|
|
1717
1717
|
return value;
|
|
1718
1718
|
}
|
|
1719
|
-
const
|
|
1720
|
-
const resourceInput = readArray(requireKey(
|
|
1719
|
+
const record2 = readRecord(value, "$", DOCUMENT_KEYS);
|
|
1720
|
+
const resourceInput = readArray(requireKey(record2, "resources", "$"), "$.resources", GPU_MODEL_DOCUMENT_LIMITS.resources, 1);
|
|
1721
1721
|
const parsed = resourceInput.map((entry, index) => {
|
|
1722
1722
|
if (typeof entry === "object" && entry !== null && verifiedResources.has(entry) && verifiedResourceBytes.has(entry)) return entry;
|
|
1723
1723
|
return parseResourceHeader(entry, `$.resources[${index}]`);
|
|
@@ -1730,7 +1730,7 @@ async function createAndVerifyGpuModelDocument(value, port, options) {
|
|
|
1730
1730
|
}
|
|
1731
1731
|
const candidate = {};
|
|
1732
1732
|
for (const key of DOCUMENT_KEYS) {
|
|
1733
|
-
if (Object.hasOwn(
|
|
1733
|
+
if (Object.hasOwn(record2, key)) candidate[key] = record2[key];
|
|
1734
1734
|
}
|
|
1735
1735
|
candidate.resources = verified;
|
|
1736
1736
|
return createGpuModelDocument(candidate);
|
|
@@ -1795,11 +1795,11 @@ async function hashGpuModelDocumentCanonical(document) {
|
|
|
1795
1795
|
return Array.from(new Uint8Array(digest), (value) => value.toString(16).padStart(2, "0")).join("");
|
|
1796
1796
|
}
|
|
1797
1797
|
function resolveStaticDemoLimits(options) {
|
|
1798
|
-
const
|
|
1799
|
-
const enabled = readBoolean(requireKey(
|
|
1798
|
+
const record2 = readRecord(options, "$.options", ["pvoxModelsEnabled", "limits"]);
|
|
1799
|
+
const enabled = readBoolean(requireKey(record2, "pvoxModelsEnabled", "$.options"), "$.options.pvoxModelsEnabled");
|
|
1800
1800
|
if (!enabled) return fail("feature-disabled", "$.options.pvoxModelsEnabled", `${GPU_MODEL_STATIC_DEMO_FEATURE_FLAG} is disabled`);
|
|
1801
|
-
if (!Object.hasOwn(
|
|
1802
|
-
const overrides = readRecord(
|
|
1801
|
+
if (!Object.hasOwn(record2, "limits")) return GPU_MODEL_STATIC_DEMO_LIMITS;
|
|
1802
|
+
const overrides = readRecord(record2.limits, "$.options.limits", Object.keys(GPU_MODEL_STATIC_DEMO_LIMITS));
|
|
1803
1803
|
const limits = { ...GPU_MODEL_STATIC_DEMO_LIMITS };
|
|
1804
1804
|
for (const [key, ceiling] of Object.entries(GPU_MODEL_STATIC_DEMO_LIMITS)) {
|
|
1805
1805
|
if (Object.hasOwn(overrides, key)) limits[key] = readSafeInteger(overrides[key], `$.options.limits.${key}`, 1, ceiling);
|
|
@@ -2034,6 +2034,260 @@ async function createGpuModelStaticDemoCompilerInput(document, options) {
|
|
|
2034
2034
|
});
|
|
2035
2035
|
}
|
|
2036
2036
|
|
|
2037
|
+
// src/diagnostics.ts
|
|
2038
|
+
var GPU_MODEL_REPAIR_RULES = Object.freeze([
|
|
2039
|
+
{
|
|
2040
|
+
"id": "missing-normals",
|
|
2041
|
+
"action": "Generate deterministic vertex normals from validated triangle geometry.",
|
|
2042
|
+
"severity": "warning",
|
|
2043
|
+
"confidence": "high",
|
|
2044
|
+
"standardIds": [
|
|
2045
|
+
"gltf-glb",
|
|
2046
|
+
"obj-mtl",
|
|
2047
|
+
"fbx",
|
|
2048
|
+
"cad-bim"
|
|
2049
|
+
],
|
|
2050
|
+
"policy": "Generate deterministic vertex normals from validated triangle geometry and record the generation basis."
|
|
2051
|
+
},
|
|
2052
|
+
{
|
|
2053
|
+
"id": "bad-winding",
|
|
2054
|
+
"action": "Normalize winding to the adapter coordinate convention.",
|
|
2055
|
+
"severity": "warning",
|
|
2056
|
+
"confidence": "medium",
|
|
2057
|
+
"standardIds": [
|
|
2058
|
+
"gltf-glb",
|
|
2059
|
+
"obj-mtl",
|
|
2060
|
+
"fbx",
|
|
2061
|
+
"cad-bim"
|
|
2062
|
+
],
|
|
2063
|
+
"policy": "Normalize winding to the adapter coordinate convention and record the inferred orientation."
|
|
2064
|
+
},
|
|
2065
|
+
{
|
|
2066
|
+
"id": "invalid-indices",
|
|
2067
|
+
"action": "Drop the invalid primitive.",
|
|
2068
|
+
"severity": "error",
|
|
2069
|
+
"confidence": "medium",
|
|
2070
|
+
"standardIds": [
|
|
2071
|
+
"gltf-glb",
|
|
2072
|
+
"obj-mtl",
|
|
2073
|
+
"fbx",
|
|
2074
|
+
"cad-bim"
|
|
2075
|
+
],
|
|
2076
|
+
"policy": "Reject the invalid primitive or clamp only when the fixture and adapter policy explicitly prove a safe repair; retain the original index in the record."
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
"id": "nan-transforms",
|
|
2080
|
+
"action": "Replace non-finite transform components with identity components.",
|
|
2081
|
+
"severity": "error",
|
|
2082
|
+
"confidence": "high",
|
|
2083
|
+
"standardIds": [
|
|
2084
|
+
"gltf-glb",
|
|
2085
|
+
"obj-mtl",
|
|
2086
|
+
"fbx",
|
|
2087
|
+
"cad-bim"
|
|
2088
|
+
],
|
|
2089
|
+
"policy": "Replace the non-finite component with the identity transform component and record the source value and fallback."
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
"id": "non-normalized-weights",
|
|
2093
|
+
"action": "Normalize finite non-negative skin weights.",
|
|
2094
|
+
"severity": "warning",
|
|
2095
|
+
"confidence": "high",
|
|
2096
|
+
"standardIds": [
|
|
2097
|
+
"gltf-glb",
|
|
2098
|
+
"fbx",
|
|
2099
|
+
"cad-bim"
|
|
2100
|
+
],
|
|
2101
|
+
"policy": "Normalize finite, non-negative influences and record the pre-normalization weights and discarded influence policy."
|
|
2102
|
+
},
|
|
2103
|
+
{
|
|
2104
|
+
"id": "missing-texture-path",
|
|
2105
|
+
"action": "Leave the missing or unsafe texture slot unresolved.",
|
|
2106
|
+
"severity": "warning",
|
|
2107
|
+
"confidence": "low",
|
|
2108
|
+
"standardIds": [
|
|
2109
|
+
"gltf-glb",
|
|
2110
|
+
"obj-mtl",
|
|
2111
|
+
"fbx",
|
|
2112
|
+
"cad-bim"
|
|
2113
|
+
],
|
|
2114
|
+
"policy": "Resolve only an explicitly bounded package-relative fallback; otherwise keep the material slot unresolved and emit a visible warning."
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
"id": "malformed-material-reference",
|
|
2118
|
+
"action": "Attach the canonical default material.",
|
|
2119
|
+
"severity": "error",
|
|
2120
|
+
"confidence": "medium",
|
|
2121
|
+
"standardIds": [
|
|
2122
|
+
"gltf-glb",
|
|
2123
|
+
"obj-mtl",
|
|
2124
|
+
"fbx",
|
|
2125
|
+
"cad-bim"
|
|
2126
|
+
],
|
|
2127
|
+
"policy": "Attach the canonical default material only when the target policy permits it and preserve the unresolved source reference."
|
|
2128
|
+
}
|
|
2129
|
+
].map((rule) => Object.freeze({ ...rule, standardIds: Object.freeze(rule.standardIds) })));
|
|
2130
|
+
var GPU_MODEL_DIAGNOSTIC_LIMITS = Object.freeze({
|
|
2131
|
+
issues: 4096,
|
|
2132
|
+
values: 16384,
|
|
2133
|
+
depth: 12,
|
|
2134
|
+
members: 1024,
|
|
2135
|
+
string: 65536,
|
|
2136
|
+
bytes: 1048576
|
|
2137
|
+
});
|
|
2138
|
+
var GpuModelDiagnosticsError = class extends TypeError {
|
|
2139
|
+
constructor() {
|
|
2140
|
+
super("Invalid or over-budget GPU model diagnostics report");
|
|
2141
|
+
this.name = "GpuModelDiagnosticsError";
|
|
2142
|
+
}
|
|
2143
|
+
};
|
|
2144
|
+
function fail2() {
|
|
2145
|
+
throw new GpuModelDiagnosticsError();
|
|
2146
|
+
}
|
|
2147
|
+
function charge(budget, bytes) {
|
|
2148
|
+
budget.bytes += bytes;
|
|
2149
|
+
if (budget.bytes > GPU_MODEL_DIAGNOSTIC_LIMITS.bytes) fail2();
|
|
2150
|
+
}
|
|
2151
|
+
function textBytes(value) {
|
|
2152
|
+
return new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
2153
|
+
}
|
|
2154
|
+
function snapshot(value, depth, budget, ancestors, memberLimit = GPU_MODEL_DIAGNOSTIC_LIMITS.members) {
|
|
2155
|
+
if (++budget.values > GPU_MODEL_DIAGNOSTIC_LIMITS.values || depth > GPU_MODEL_DIAGNOSTIC_LIMITS.depth) fail2();
|
|
2156
|
+
if (value === null || typeof value === "boolean") {
|
|
2157
|
+
charge(budget, value === null ? 4 : value ? 4 : 5);
|
|
2158
|
+
return value;
|
|
2159
|
+
}
|
|
2160
|
+
if (typeof value === "number") {
|
|
2161
|
+
if (!Number.isFinite(value)) fail2();
|
|
2162
|
+
charge(budget, JSON.stringify(value).length);
|
|
2163
|
+
return Object.is(value, -0) ? 0 : value;
|
|
2164
|
+
}
|
|
2165
|
+
if (typeof value === "string") {
|
|
2166
|
+
if (value.length > GPU_MODEL_DIAGNOSTIC_LIMITS.string) fail2();
|
|
2167
|
+
charge(budget, textBytes(value));
|
|
2168
|
+
return value;
|
|
2169
|
+
}
|
|
2170
|
+
if (typeof value !== "object" || ancestors.has(value)) fail2();
|
|
2171
|
+
const array = Array.isArray(value);
|
|
2172
|
+
const prototype = Object.getPrototypeOf(value);
|
|
2173
|
+
if (array ? prototype !== Array.prototype : prototype !== Object.prototype && prototype !== null) fail2();
|
|
2174
|
+
const keys = Reflect.ownKeys(value);
|
|
2175
|
+
const maxMembers = memberLimit;
|
|
2176
|
+
if (keys.length > maxMembers + (array ? 1 : 0)) fail2();
|
|
2177
|
+
ancestors.add(value);
|
|
2178
|
+
charge(budget, 2);
|
|
2179
|
+
if (array) {
|
|
2180
|
+
const length = Object.getOwnPropertyDescriptor(value, "length").value;
|
|
2181
|
+
if (length > maxMembers || keys.length !== length + 1) fail2();
|
|
2182
|
+
const result2 = [];
|
|
2183
|
+
for (let i = 0; i < length; i++) {
|
|
2184
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(i));
|
|
2185
|
+
if (!descriptor || !("value" in descriptor) || !descriptor.enumerable) fail2();
|
|
2186
|
+
if (i > 0) charge(budget, 1);
|
|
2187
|
+
result2.push(snapshot(descriptor.value, depth + 1, budget, ancestors));
|
|
2188
|
+
}
|
|
2189
|
+
ancestors.delete(value);
|
|
2190
|
+
return Object.freeze(result2);
|
|
2191
|
+
}
|
|
2192
|
+
const result = {};
|
|
2193
|
+
let count = 0;
|
|
2194
|
+
for (const key of keys) {
|
|
2195
|
+
if (typeof key !== "string" || key.length > 256 || ["__proto__", "constructor", "prototype"].includes(key)) fail2();
|
|
2196
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
2197
|
+
if (!("value" in descriptor) || !descriptor.enumerable) fail2();
|
|
2198
|
+
if (count++ > 0) charge(budget, 1);
|
|
2199
|
+
charge(budget, textBytes(key) + 1);
|
|
2200
|
+
result[key] = snapshot(descriptor.value, depth + 1, budget, ancestors, depth === 0 && key === "issues" ? GPU_MODEL_DIAGNOSTIC_LIMITS.issues : GPU_MODEL_DIAGNOSTIC_LIMITS.members);
|
|
2201
|
+
}
|
|
2202
|
+
ancestors.delete(value);
|
|
2203
|
+
return Object.freeze(result);
|
|
2204
|
+
}
|
|
2205
|
+
function record(value) {
|
|
2206
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return fail2();
|
|
2207
|
+
return value;
|
|
2208
|
+
}
|
|
2209
|
+
function keysOnly(value, allowed) {
|
|
2210
|
+
if (Object.keys(value).some((key) => !allowed.includes(key))) fail2();
|
|
2211
|
+
}
|
|
2212
|
+
function enumValue(value, allowed) {
|
|
2213
|
+
if (typeof value !== "string" || !allowed.includes(value)) return fail2();
|
|
2214
|
+
return value;
|
|
2215
|
+
}
|
|
2216
|
+
function hasUnsafeText(value) {
|
|
2217
|
+
for (const character of value) {
|
|
2218
|
+
const point = character.codePointAt(0);
|
|
2219
|
+
if (point < 32 || point >= 127 && point <= 159 || point >= 8232 && point <= 8238 || point >= 8294 && point <= 8297 || point >= 55296 && point <= 57343) return true;
|
|
2220
|
+
}
|
|
2221
|
+
return false;
|
|
2222
|
+
}
|
|
2223
|
+
function text(value, max) {
|
|
2224
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max || hasUnsafeText(value)) return fail2();
|
|
2225
|
+
return value;
|
|
2226
|
+
}
|
|
2227
|
+
function boolean(value) {
|
|
2228
|
+
if (typeof value !== "boolean") return fail2();
|
|
2229
|
+
return value;
|
|
2230
|
+
}
|
|
2231
|
+
function parseIssue(value) {
|
|
2232
|
+
const issue = record(value);
|
|
2233
|
+
const kind = enumValue(issue.kind, ["violation", "unsupported", "warning", "loss"]);
|
|
2234
|
+
const base = { code: text(issue.code, 128), path: text(issue.path, 1024), message: text(issue.message, 2048) };
|
|
2235
|
+
if (!base.path.startsWith("/")) fail2();
|
|
2236
|
+
const common = ["kind", "code", "path", "message"];
|
|
2237
|
+
if (kind === "violation") {
|
|
2238
|
+
keysOnly(issue, [...common, "ruleId", "originalValue", "repairApplied"]);
|
|
2239
|
+
if (!Object.hasOwn(issue, "originalValue")) fail2();
|
|
2240
|
+
return { ...base, kind, ruleId: text(issue.ruleId, 128), originalValue: issue.originalValue, repairApplied: boolean(issue.repairApplied) };
|
|
2241
|
+
}
|
|
2242
|
+
if (kind === "loss") {
|
|
2243
|
+
keysOnly(issue, [...common, "effect"]);
|
|
2244
|
+
return { ...base, kind, effect: enumValue(issue.effect, ["dropped", "approximated"]) };
|
|
2245
|
+
}
|
|
2246
|
+
keysOnly(issue, common);
|
|
2247
|
+
return { ...base, kind };
|
|
2248
|
+
}
|
|
2249
|
+
function evaluateGpuModelDiagnostics(value) {
|
|
2250
|
+
const budget = { values: 0, bytes: 0 };
|
|
2251
|
+
const args = record(snapshot(value, 0, budget, /* @__PURE__ */ new Set()));
|
|
2252
|
+
keysOnly(args, ["standard", "mode", "allowLoss", "issues", "rawSource", "maxEvidenceBytes"]);
|
|
2253
|
+
const standard = enumValue(args.standard, ["gltf-glb", "obj-mtl", "fbx", "cad-bim"]);
|
|
2254
|
+
const mode = args.mode === void 0 ? "strict" : enumValue(args.mode, ["strict", "tolerant", "forensic"]);
|
|
2255
|
+
const allowLoss = args.allowLoss === void 0 ? false : boolean(args.allowLoss);
|
|
2256
|
+
if (args.maxEvidenceBytes !== void 0 && (typeof args.maxEvidenceBytes !== "number" || !Number.isSafeInteger(args.maxEvidenceBytes) || args.maxEvidenceBytes < 1 || args.maxEvidenceBytes > GPU_MODEL_DIAGNOSTIC_LIMITS.bytes || budget.bytes > args.maxEvidenceBytes)) fail2();
|
|
2257
|
+
if (mode === "forensic" ? !Object.hasOwn(args, "rawSource") : Object.hasOwn(args, "rawSource")) fail2();
|
|
2258
|
+
if (!Array.isArray(args.issues) || args.issues.length > GPU_MODEL_DIAGNOSTIC_LIMITS.issues) fail2();
|
|
2259
|
+
const issues = args.issues.map(parseIssue);
|
|
2260
|
+
const lossPaths = new Set(issues.filter((issue) => issue.kind === "loss").map((issue) => issue.path));
|
|
2261
|
+
const diagnostics = [];
|
|
2262
|
+
const repairs = [];
|
|
2263
|
+
const losses = [];
|
|
2264
|
+
let accepted = true;
|
|
2265
|
+
for (const issue of issues) {
|
|
2266
|
+
const base = { code: issue.code, path: issue.path, message: issue.message };
|
|
2267
|
+
if (issue.kind === "violation") {
|
|
2268
|
+
const rule = GPU_MODEL_REPAIR_RULES.find((rule2) => rule2.id === issue.ruleId);
|
|
2269
|
+
const requiresLoss = ["invalid-indices", "missing-texture-path", "malformed-material-reference"].includes(issue.ruleId);
|
|
2270
|
+
const repaired = mode !== "strict" && issue.repairApplied && rule && rule.standardIds.includes(standard) && (!requiresLoss || lossPaths.has(issue.path));
|
|
2271
|
+
if (repaired) {
|
|
2272
|
+
repairs.push(Object.freeze({ ruleId: rule.id, originalValue: issue.originalValue, action: rule.action, severity: rule.severity, affectedPath: issue.path, confidence: rule.confidence }));
|
|
2273
|
+
diagnostics.push(Object.freeze({ ...base, kind: "repair", severity: rule.severity, ruleId: rule.id }));
|
|
2274
|
+
} else {
|
|
2275
|
+
accepted = false;
|
|
2276
|
+
diagnostics.push(Object.freeze({ ...base, kind: "blocking-error", severity: "error", ruleId: issue.ruleId }));
|
|
2277
|
+
}
|
|
2278
|
+
} else if (issue.kind === "unsupported") {
|
|
2279
|
+
accepted = false;
|
|
2280
|
+
diagnostics.push(Object.freeze({ ...issue, severity: "error" }));
|
|
2281
|
+
} else if (issue.kind === "loss") {
|
|
2282
|
+
if (!allowLoss) accepted = false;
|
|
2283
|
+
const loss = Object.freeze({ ...issue, severity: "warning" });
|
|
2284
|
+
losses.push(loss);
|
|
2285
|
+
diagnostics.push(loss);
|
|
2286
|
+
} else diagnostics.push(Object.freeze({ ...issue, severity: "warning" }));
|
|
2287
|
+
}
|
|
2288
|
+
return Object.freeze({ mode, standard, accepted, diagnostics: Object.freeze(diagnostics), repairs: Object.freeze(repairs), losses: Object.freeze(losses), ...mode === "forensic" ? { rawSource: args.rawSource } : {} });
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2037
2291
|
// src/index.ts
|
|
2038
2292
|
var packageName = "@plasius/gpu-model-core";
|
|
2039
2293
|
var packageBootstrap = Object.freeze({
|
|
@@ -2043,8 +2297,10 @@ var packageBootstrap = Object.freeze({
|
|
|
2043
2297
|
});
|
|
2044
2298
|
export {
|
|
2045
2299
|
CANONICAL_GPU_MODEL_COORDINATE_SYSTEM,
|
|
2300
|
+
GPU_MODEL_DIAGNOSTIC_LIMITS,
|
|
2046
2301
|
GPU_MODEL_DOCUMENT_LIMITS,
|
|
2047
2302
|
GPU_MODEL_DOCUMENT_SCHEMA_VERSION,
|
|
2303
|
+
GPU_MODEL_REPAIR_RULES,
|
|
2048
2304
|
GPU_MODEL_RESOURCE_VERIFICATION_LIMITS,
|
|
2049
2305
|
GPU_MODEL_STATIC_DEMO_FEATURE_FLAG,
|
|
2050
2306
|
GPU_MODEL_STATIC_DEMO_LIMITS,
|
|
@@ -2053,6 +2309,7 @@ export {
|
|
|
2053
2309
|
GPU_MODEL_STATIC_DEMO_MAX_EXTENT_METRES,
|
|
2054
2310
|
GPU_MODEL_STATIC_DEMO_NORMALIZATION_CONTRACT,
|
|
2055
2311
|
GPU_MODEL_STATIC_DEMO_PROFILE_VERSION,
|
|
2312
|
+
GpuModelDiagnosticsError,
|
|
2056
2313
|
GpuModelDocumentError,
|
|
2057
2314
|
assertGpuModelStaticDemoNormalization,
|
|
2058
2315
|
canCreateGpuModelDocument,
|
|
@@ -2060,6 +2317,7 @@ export {
|
|
|
2060
2317
|
createGpuModelDocument,
|
|
2061
2318
|
createGpuModelStaticDemoCompilerInput,
|
|
2062
2319
|
encodeGpuModelDocumentCanonical,
|
|
2320
|
+
evaluateGpuModelDiagnostics,
|
|
2063
2321
|
hashGpuModelDocumentCanonical,
|
|
2064
2322
|
isGpuModelDocument,
|
|
2065
2323
|
packageBootstrap,
|