@selvajs/visualization 1.7.1-beta.0 → 1.7.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/parse.cjs +108 -82
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.js +108 -82
- package/dist/parse.js.map +1 -1
- package/package.json +1 -1
package/dist/parse.cjs
CHANGED
|
@@ -776,6 +776,95 @@ function textureToDataUri(payload) {
|
|
|
776
776
|
for (let i = 0; i < data.byteLength; i += step) binary += String.fromCharCode(...data.subarray(i, i + step));
|
|
777
777
|
return `data:${mime};base64,${btoa(binary)}`;
|
|
778
778
|
}
|
|
779
|
+
// src/parse/webdisplay/batch/metadata.ts
|
|
780
|
+
function metadataFail(message, context) {
|
|
781
|
+
return new require_gpu_dispose.VisualizationError(message, require_gpu_dispose.ErrorCodes.VALIDATION_ERROR, { context });
|
|
782
|
+
}
|
|
783
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
784
|
+
/**
|
|
785
|
+
* Validates the batch's material and group/mesh metadata against the decoded geometry buffers
|
|
786
|
+
* before any of it is used arithmetically. Throws on the first inconsistency — a material, group or
|
|
787
|
+
* mesh that isn't an object, out-of-range `materialId`, non-integer or negative offsets/counts, or a
|
|
788
|
+
* vertex/index window that overruns the buffers — so malformed or version-skewed metadata fails the
|
|
789
|
+
* parse loudly instead of corrupting the render.
|
|
790
|
+
*/
|
|
791
|
+
function validateBatchMetadata(materials, groups, totalVertexCount, totalIndexCount) {
|
|
792
|
+
if (!Array.isArray(materials)) throw metadataFail("Batch materials must be an array.", { materials: typeof materials });
|
|
793
|
+
const badMaterial = materials.findIndex((material) => !isObject(material));
|
|
794
|
+
if (badMaterial !== -1) throw metadataFail("Each material entry must be an object.", { materialIndex: badMaterial });
|
|
795
|
+
const materialCount = materials.length;
|
|
796
|
+
if (!Array.isArray(groups)) throw metadataFail("Batch groups must be an array.", { groups: typeof groups });
|
|
797
|
+
groups.forEach((group, groupIndex) => {
|
|
798
|
+
if (!isObject(group) || !Array.isArray(group.meshes)) throw metadataFail("Each group must be an object with a meshes array.", { groupIndex });
|
|
799
|
+
if (!group.meshes.every(isObject)) throw metadataFail("Each mesh entry must be an object.", { groupIndex });
|
|
800
|
+
});
|
|
801
|
+
for (const group of groups) {
|
|
802
|
+
if (!Number.isInteger(group.materialId) || group.materialId < 0 || group.materialId >= materialCount) throw metadataFail("Group materialId out of range of the materials array.", {
|
|
803
|
+
materialId: group.materialId,
|
|
804
|
+
materialCount
|
|
805
|
+
});
|
|
806
|
+
for (const mesh of group.meshes) {
|
|
807
|
+
const fields = {
|
|
808
|
+
vertexStart: mesh.vertexStart,
|
|
809
|
+
vertexCount: mesh.vertexCount,
|
|
810
|
+
indexStart: mesh.indexStart,
|
|
811
|
+
indexCount: mesh.indexCount
|
|
812
|
+
};
|
|
813
|
+
for (const [field, value] of Object.entries(fields)) if (!Number.isInteger(value) || value < 0) throw metadataFail(`Mesh metadata field "${field}" must be a non-negative integer.`, {
|
|
814
|
+
meshName: mesh.name,
|
|
815
|
+
field,
|
|
816
|
+
value
|
|
817
|
+
});
|
|
818
|
+
if (mesh.vertexStart + mesh.vertexCount > totalVertexCount) throw metadataFail("Mesh vertex window exceeds the batch vertex buffer.", {
|
|
819
|
+
meshName: mesh.name,
|
|
820
|
+
vertexStart: mesh.vertexStart,
|
|
821
|
+
vertexCount: mesh.vertexCount,
|
|
822
|
+
totalVertexCount
|
|
823
|
+
});
|
|
824
|
+
if (mesh.indexStart + mesh.indexCount > totalIndexCount) throw metadataFail("Mesh index window exceeds the batch index buffer.", {
|
|
825
|
+
meshName: mesh.name,
|
|
826
|
+
indexStart: mesh.indexStart,
|
|
827
|
+
indexCount: mesh.indexCount,
|
|
828
|
+
totalIndexCount
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Error for an index outside its mesh's declared vertex window
|
|
835
|
+
* `[vertexStart, vertexStart + vertexCount)`. Rebasing (`index - vertexStart`) writes into an
|
|
836
|
+
* unsigned array, so an out-of-window index would otherwise wrap to ~4 billion and corrupt the
|
|
837
|
+
* geometry. Range checks live inline in the copy loops (a function call per index measured
|
|
838
|
+
* noticeably slower at millions of indices) — this only builds the failure.
|
|
839
|
+
*/
|
|
840
|
+
function indexOutOfWindow(indexValue, meshMeta) {
|
|
841
|
+
return metadataFail("Index references a vertex outside its mesh's vertex window.", {
|
|
842
|
+
meshName: meshMeta.name,
|
|
843
|
+
indexValue,
|
|
844
|
+
vertexStart: meshMeta.vertexStart,
|
|
845
|
+
vertexCount: meshMeta.vertexCount
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Reconstructs world-unit float32 positions from int16 quantized values:
|
|
850
|
+
* `world = origin + (q + 32767) * scale`. No rotation — the Three scene uses Rhino's Z-up frame,
|
|
851
|
+
* so vertices pass through as they arrived.
|
|
852
|
+
*/
|
|
853
|
+
function dequantizeInt16(q, origin, scale) {
|
|
854
|
+
const out = new Float32Array(q.length);
|
|
855
|
+
const ox = origin[0];
|
|
856
|
+
const oy = origin[1];
|
|
857
|
+
const oz = origin[2];
|
|
858
|
+
const sx = scale[0];
|
|
859
|
+
const sy = scale[1];
|
|
860
|
+
const sz = scale[2];
|
|
861
|
+
for (let i = 0; i < q.length; i += 3) {
|
|
862
|
+
out[i] = ox + (q[i] + 32767) * sx;
|
|
863
|
+
out[i + 1] = oy + (q[i + 1] + 32767) * sy;
|
|
864
|
+
out[i + 2] = oz + (q[i + 2] + 32767) * sz;
|
|
865
|
+
}
|
|
866
|
+
return out;
|
|
867
|
+
}
|
|
779
868
|
// src/parse/webdisplay/binary-parser.ts
|
|
780
869
|
/** Second half of {@link parseBinaryMeshBatch}, for a caller that already holds the raw view. */
|
|
781
870
|
function decodeRawMeshBatch(raw) {
|
|
@@ -847,6 +936,7 @@ function parseBinaryMeshBatchRaw(input) {
|
|
|
847
936
|
} catch (error) {
|
|
848
937
|
throw fail(`Failed to parse metadata JSON: ${error instanceof Error ? error.message : String(error)}`, { metadataLen });
|
|
849
938
|
}
|
|
939
|
+
if (!isMetadataShape(metadata)) throw fail("Metadata JSON must be an object whose materials and groups are arrays.", { metadataLen });
|
|
850
940
|
if (offset + 56 > bytes.byteLength) throw fail("Insufficient data to read geometry header.", {
|
|
851
941
|
expectedBytes: 56,
|
|
852
942
|
availableBytes: bytes.byteLength - offset,
|
|
@@ -937,6 +1027,12 @@ function parseBinaryMeshBatchRaw(input) {
|
|
|
937
1027
|
colors
|
|
938
1028
|
};
|
|
939
1029
|
}
|
|
1030
|
+
/** Missing `materials`/`groups` are allowed: the JSON envelope's copies fill them in. */
|
|
1031
|
+
function isMetadataShape(value) {
|
|
1032
|
+
if (!isObject(value)) return false;
|
|
1033
|
+
const { materials, groups } = value;
|
|
1034
|
+
return (materials === void 0 || Array.isArray(materials)) && (groups === void 0 || Array.isArray(groups));
|
|
1035
|
+
}
|
|
940
1036
|
// src/parse/webdisplay/closed-solid.ts
|
|
941
1037
|
/**
|
|
942
1038
|
* True when every member of a geometry is a closed, consistently wound, outward-facing triangle
|
|
@@ -2300,84 +2396,6 @@ function applyVertexColorSRGBDecode(material) {
|
|
|
2300
2396
|
#endif`);
|
|
2301
2397
|
});
|
|
2302
2398
|
}
|
|
2303
|
-
// src/parse/webdisplay/batch/metadata.ts
|
|
2304
|
-
function metadataFail(message, context) {
|
|
2305
|
-
return new require_gpu_dispose.VisualizationError(message, require_gpu_dispose.ErrorCodes.VALIDATION_ERROR, { context });
|
|
2306
|
-
}
|
|
2307
|
-
/**
|
|
2308
|
-
* Validates the batch's group/mesh metadata against the decoded geometry buffers before any of it
|
|
2309
|
-
* is used arithmetically. Throws on the first inconsistency — out-of-range `materialId`,
|
|
2310
|
-
* non-integer or negative offsets/counts, or a vertex/index window that overruns the buffers — so
|
|
2311
|
-
* malformed or version-skewed metadata fails the parse loudly instead of corrupting the render.
|
|
2312
|
-
*/
|
|
2313
|
-
function validateGroupMetadata(groups, materialCount, totalVertexCount, totalIndexCount) {
|
|
2314
|
-
for (const group of groups) {
|
|
2315
|
-
if (!Number.isInteger(group.materialId) || group.materialId < 0 || group.materialId >= materialCount) throw metadataFail("Group materialId out of range of the materials array.", {
|
|
2316
|
-
materialId: group.materialId,
|
|
2317
|
-
materialCount
|
|
2318
|
-
});
|
|
2319
|
-
for (const mesh of group.meshes) {
|
|
2320
|
-
const fields = {
|
|
2321
|
-
vertexStart: mesh.vertexStart,
|
|
2322
|
-
vertexCount: mesh.vertexCount,
|
|
2323
|
-
indexStart: mesh.indexStart,
|
|
2324
|
-
indexCount: mesh.indexCount
|
|
2325
|
-
};
|
|
2326
|
-
for (const [field, value] of Object.entries(fields)) if (!Number.isInteger(value) || value < 0) throw metadataFail(`Mesh metadata field "${field}" must be a non-negative integer.`, {
|
|
2327
|
-
meshName: mesh.name,
|
|
2328
|
-
field,
|
|
2329
|
-
value
|
|
2330
|
-
});
|
|
2331
|
-
if (mesh.vertexStart + mesh.vertexCount > totalVertexCount) throw metadataFail("Mesh vertex window exceeds the batch vertex buffer.", {
|
|
2332
|
-
meshName: mesh.name,
|
|
2333
|
-
vertexStart: mesh.vertexStart,
|
|
2334
|
-
vertexCount: mesh.vertexCount,
|
|
2335
|
-
totalVertexCount
|
|
2336
|
-
});
|
|
2337
|
-
if (mesh.indexStart + mesh.indexCount > totalIndexCount) throw metadataFail("Mesh index window exceeds the batch index buffer.", {
|
|
2338
|
-
meshName: mesh.name,
|
|
2339
|
-
indexStart: mesh.indexStart,
|
|
2340
|
-
indexCount: mesh.indexCount,
|
|
2341
|
-
totalIndexCount
|
|
2342
|
-
});
|
|
2343
|
-
}
|
|
2344
|
-
}
|
|
2345
|
-
}
|
|
2346
|
-
/**
|
|
2347
|
-
* Error for an index outside its mesh's declared vertex window
|
|
2348
|
-
* `[vertexStart, vertexStart + vertexCount)`. Rebasing (`index - vertexStart`) writes into an
|
|
2349
|
-
* unsigned array, so an out-of-window index would otherwise wrap to ~4 billion and corrupt the
|
|
2350
|
-
* geometry. Range checks live inline in the copy loops (a function call per index measured
|
|
2351
|
-
* noticeably slower at millions of indices) — this only builds the failure.
|
|
2352
|
-
*/
|
|
2353
|
-
function indexOutOfWindow(indexValue, meshMeta) {
|
|
2354
|
-
return metadataFail("Index references a vertex outside its mesh's vertex window.", {
|
|
2355
|
-
meshName: meshMeta.name,
|
|
2356
|
-
indexValue,
|
|
2357
|
-
vertexStart: meshMeta.vertexStart,
|
|
2358
|
-
vertexCount: meshMeta.vertexCount
|
|
2359
|
-
});
|
|
2360
|
-
}
|
|
2361
|
-
/**
|
|
2362
|
-
* Reconstructs world-unit float32 positions from int16 quantized values:
|
|
2363
|
-
* `world = origin + (q + 32767) * scale`. No rotation — the Three scene uses Rhino's Z-up frame,
|
|
2364
|
-
* so vertices pass through as they arrived.
|
|
2365
|
-
*/
|
|
2366
|
-
function dequantizeInt16(q, origin, scale) {
|
|
2367
|
-
const out = new Float32Array(q.length);
|
|
2368
|
-
const ox = origin[0];
|
|
2369
|
-
const oy = origin[1];
|
|
2370
|
-
const oz = origin[2];
|
|
2371
|
-
const sx = scale[0];
|
|
2372
|
-
const sy = scale[1];
|
|
2373
|
-
const sz = scale[2];
|
|
2374
|
-
for (let i = 0; i < q.length; i += 3) {
|
|
2375
|
-
out[i] = ox + (q[i] + 32767) * sx;
|
|
2376
|
-
out[i + 1] = oy + (q[i + 1] + 32767) * sy;
|
|
2377
|
-
out[i + 2] = oz + (q[i + 2] + 32767) * sz;
|
|
2378
|
-
}
|
|
2379
|
-
return out;
|
|
2380
|
-
}
|
|
2381
2399
|
// src/parse/webdisplay/batch/merge.ts
|
|
2382
2400
|
/**
|
|
2383
2401
|
* Split a material group into one sub-group per layer, preserving mesh order.
|
|
@@ -2767,7 +2785,7 @@ function buildMeshesFromParsed(parsed, opts) {
|
|
|
2767
2785
|
const materialsSrc = parsed.metadata.materials ?? fallback?.materials ?? [];
|
|
2768
2786
|
const groups = parsed.metadata.groups ?? fallback?.groups ?? [];
|
|
2769
2787
|
const isFloat32 = (parsed.flags & 1) !== 0;
|
|
2770
|
-
|
|
2788
|
+
validateBatchMetadata(materialsSrc, groups, parsed.vertices.length / 3, parsed.indices.length);
|
|
2771
2789
|
const worldVertices = isFloat32 ? parsed.vertices : dequantizeInt16(parsed.vertices, parsed.origin, parsed.scale);
|
|
2772
2790
|
if (debug) {
|
|
2773
2791
|
const wireBytes = parsed.vertices.byteLength + parsed.indices.byteLength;
|
|
@@ -2855,7 +2873,7 @@ function buildFromFinishedCache(container, opts) {
|
|
|
2855
2873
|
const { jobs, jobRefs } = planJobs(groups, opts.mergeByMaterial);
|
|
2856
2874
|
const cached = getFinishedBatch(container.geometryBlob, jobs);
|
|
2857
2875
|
if (!cached) return null;
|
|
2858
|
-
|
|
2876
|
+
validateBatchMetadata(materialsSrc, groups, cached.vertexCount, cached.indexCount);
|
|
2859
2877
|
return meshesFromBuilt(cached, jobRefs, materialsSrc, opts.material);
|
|
2860
2878
|
}
|
|
2861
2879
|
/** One job per merged group or per unmerged mesh, with a parallel ref list to unwrap results by index. */
|
|
@@ -2907,7 +2925,7 @@ async function tryBuildViaAssembly(opened, opts) {
|
|
|
2907
2925
|
if (indexCount / 3 < 5e4) return null;
|
|
2908
2926
|
const materialsSrc = raw.metadata.materials ?? opts.fallback?.materials ?? [];
|
|
2909
2927
|
const groups = raw.metadata.groups ?? opts.fallback?.groups ?? [];
|
|
2910
|
-
|
|
2928
|
+
validateBatchMetadata(materialsSrc, groups, raw.vertexCount, indexCount);
|
|
2911
2929
|
const { jobs, jobRefs } = planJobs(groups, opts.mergeByMaterial);
|
|
2912
2930
|
const assembled = await assemble(raw, jobs, opened.source);
|
|
2913
2931
|
if (!assembled) {
|
|
@@ -2947,7 +2965,15 @@ async function assemble(raw, jobs, source) {
|
|
|
2947
2965
|
colors: raw.colors,
|
|
2948
2966
|
jobs
|
|
2949
2967
|
};
|
|
2950
|
-
if (!worker)
|
|
2968
|
+
if (!worker) {
|
|
2969
|
+
let geometries;
|
|
2970
|
+
try {
|
|
2971
|
+
geometries = assembleGeometries(input);
|
|
2972
|
+
} catch (error) {
|
|
2973
|
+
throw new require_gpu_dispose.VisualizationError(error instanceof Error ? error.message : String(error), require_gpu_dispose.ErrorCodes.VALIDATION_ERROR, { originalError: error instanceof Error ? error : void 0 });
|
|
2974
|
+
}
|
|
2975
|
+
return geometries.map((geometry, i) => finishGeometry(geometry, jobs[i].windows));
|
|
2976
|
+
}
|
|
2951
2977
|
if (!ownsBuffer(raw, source)) {
|
|
2952
2978
|
input.vertexData = raw.vertexData.slice();
|
|
2953
2979
|
input.indexData = raw.indexData.slice();
|