@loaders.gl/tile-converter 5.0.0-alpha.10 → 5.0.0-alpha.12
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 +211 -2
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +162 -163
- package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
- package/dist/index.cjs +1933 -2232
- package/dist/index.js +1925 -2235
- package/dist/v5/adapters.cjs +3186 -208
- package/dist/v5/adapters.d.ts +29 -2
- package/dist/v5/adapters.js +3202 -196
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +30 -0
- package/dist/v5/browser.d.ts +26 -2
- package/dist/v5/browser.js +45 -1
- package/dist/v5/conversion-api.d.ts +9 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1214 -0
- package/dist/v5/core.cjs +515 -50
- package/dist/v5/core.d.ts +2 -0
- package/dist/v5/core.js +515 -50
- package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +20 -0
- package/dist/v5/index.d.ts +18 -2
- package/dist/v5/index.js +31 -1
- package/dist/v5/mesh-conversion.d.ts +29 -2
- package/dist/v5/mesh-features.d.ts +9 -0
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +2 -1
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +2 -0
- package/dist/v5/mesh-source.d.ts +13 -3
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +6 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +32 -0
- package/dist/v5/node-feature-schema.d.ts +3 -0
- package/dist/v5/node-i3s-source.d.ts +41 -0
- package/dist/v5/node-source.d.ts +47 -0
- package/dist/v5/node.cjs +900 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +877 -0
- package/dist/v5/point-cloud-file.d.ts +83 -0
- package/dist/v5/point-cloud-potree.d.ts +47 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +26 -1
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- package/dist/v5/spatial-conversion.d.ts +3 -1
- package/package.json +33 -25
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
- package/src/v4/converter-cli.ts +8 -2
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
- package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
- package/src/v4/i3s-converter/json-templates/store.ts +2 -1
- package/src/v5/adapters.ts +69 -2
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +81 -2
- package/src/v5/conversion-api.ts +69 -9
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +6 -0
- package/src/v5/i3s-mesh-conversion.ts +97 -19
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +228 -0
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +60 -2
- package/src/v5/mesh-conversion.ts +61 -11
- package/src/v5/mesh-draco.ts +47 -3
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +73 -10
- package/src/v5/mesh-source-schema.ts +134 -0
- package/src/v5/mesh-source-texture.ts +47 -0
- package/src/v5/mesh-source.ts +99 -25
- package/src/v5/mesh-tileset-sink.ts +1 -1
- package/src/v5/mesh.ts +16 -7
- package/src/v5/node-cli.ts +197 -0
- package/src/v5/node-conversion.ts +345 -0
- package/src/v5/node-feature-schema.ts +155 -0
- package/src/v5/node-i3s-source.ts +181 -0
- package/src/v5/node-source.ts +328 -0
- package/src/v5/node.ts +16 -0
- package/src/v5/point-cloud-conversion.ts +9 -1
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +130 -0
- package/src/v5/point-cloud-source-encoder.ts +31 -20
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +159 -32
- package/src/v5/resource-fetcher.ts +113 -0
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- package/src/v5/spatial-conversion.ts +161 -6
package/dist/v5/adapters.cjs
CHANGED
|
@@ -20,21 +20,41 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/v5/adapters.ts
|
|
21
21
|
var adapters_exports = {};
|
|
22
22
|
__export(adapters_exports, {
|
|
23
|
+
SELECTED_MESH_LIMITS: () => SELECTED_MESH_LIMITS,
|
|
23
24
|
convertFeatureAttributesToArrowBatches: () => convertFeatureAttributesToArrowBatches,
|
|
24
25
|
convertPointCloudSource: () => convertPointCloudSource,
|
|
26
|
+
convertPointCloudToPotree: () => convertPointCloudToPotree,
|
|
27
|
+
convertPointCloudToTileset: () => convertPointCloudToTileset,
|
|
25
28
|
createI3SMeshConversionCodec: () => createI3SMeshConversionCodec,
|
|
29
|
+
createI3SMeshSink: () => createI3SMeshSink,
|
|
30
|
+
createI3SMeshTilesetConversionSource: () => createI3SMeshTilesetConversionSource,
|
|
26
31
|
createMeshConversionCodec: () => createMeshConversionCodec,
|
|
32
|
+
createMeshHierarchyTilesetSink: () => createMeshHierarchyTilesetSink,
|
|
33
|
+
createMeshSourceFeatureOptions: () => createMeshSourceFeatureOptions,
|
|
27
34
|
createMeshTilesetConversionSource: () => createMeshTilesetConversionSource,
|
|
28
35
|
createMeshTilesetSink: () => createMeshTilesetSink,
|
|
36
|
+
createPointCloudTilesetSink: () => createPointCloudTilesetSink,
|
|
37
|
+
createSelectedI3SMeshConversionSource: () => createSelectedI3SMeshConversionSource,
|
|
38
|
+
createSelectedMeshConversionSource: () => createSelectedMeshConversionSource,
|
|
29
39
|
createSingleMeshI3SSink: () => createSingleMeshI3SSink,
|
|
30
40
|
createSingleMeshTilesetArchive: () => createSingleMeshTilesetArchive,
|
|
31
41
|
createSingleMeshTilesetSink: () => createSingleMeshTilesetSink,
|
|
32
42
|
createTileConversionArchive: () => createTileConversionArchive,
|
|
43
|
+
createTileConversionArchiveTransport: () => createTileConversionArchiveTransport,
|
|
44
|
+
createTileConversionResourceFetcher: () => createTileConversionResourceFetcher,
|
|
33
45
|
encodeMeshTile: () => encodeMeshTile,
|
|
34
46
|
encodePointCloudSource: () => encodePointCloudSource,
|
|
35
47
|
encodePointCloudSourceTile: () => encodePointCloudSourceTile,
|
|
36
48
|
encodePointCloudTile: () => encodePointCloudTile,
|
|
37
|
-
|
|
49
|
+
encodePointCloudTileWithMetadata: () => encodePointCloudTileWithMetadata,
|
|
50
|
+
encodePotreeArchive: () => encodePotreeArchive,
|
|
51
|
+
encodeTileConversionArchiveInBatches: () => encodeTileConversionArchiveInBatches,
|
|
52
|
+
extractMeshVertices: () => extractMeshVertices,
|
|
53
|
+
inspectI3SMeshConversionSource: () => inspectI3SMeshConversionSource,
|
|
54
|
+
measureMeshBytes: () => measureMeshBytes,
|
|
55
|
+
openPointCloudFile: () => openPointCloudFile,
|
|
56
|
+
openTileConversionArchive: () => openTileConversionArchive,
|
|
57
|
+
transformPointCloudSourceTile: () => transformPointCloudSourceTile
|
|
38
58
|
});
|
|
39
59
|
module.exports = __toCommonJS(adapters_exports);
|
|
40
60
|
|
|
@@ -365,29 +385,22 @@ var import_d_tiles = require("@loaders.gl/3d-tiles");
|
|
|
365
385
|
var import_schema_utils2 = require("@loaders.gl/schema-utils");
|
|
366
386
|
var import_core2 = require("@loaders.gl/tile-converter/v5/core");
|
|
367
387
|
function encodePointCloudTile(pointBatch, options = {}) {
|
|
388
|
+
return encodePointCloudTileWithMetadata(pointBatch, options).pnts;
|
|
389
|
+
}
|
|
390
|
+
function encodePointCloudTileWithMetadata(pointBatch, options = {}) {
|
|
368
391
|
const mesh = "attributes" in pointBatch ? pointBatch : (0, import_schema_utils2.convertTableToMesh)(pointBatch);
|
|
369
392
|
validatePointAttributes(mesh.attributes);
|
|
370
393
|
const positionAttribute = mesh.attributes.POSITION;
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"Point cloud batches must contain a three-component POSITION attribute"
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
const positions = getPositionValues(positionAttribute, options.rtcCenter);
|
|
394
|
+
validatePointCloudPosition(positionAttribute);
|
|
395
|
+
const placement = encodePointPositions(positionAttribute, options);
|
|
396
|
+
const { positions } = placement;
|
|
378
397
|
const pointCount = positions.length / 3;
|
|
379
|
-
if (pointCount === 0 || !Number.isInteger(pointCount)) {
|
|
380
|
-
throw new import_core2.TileConversionError(
|
|
381
|
-
"POINT_CLOUD_POSITION_COUNT_INVALID",
|
|
382
|
-
"Point cloud POSITION must contain one or more xyz triples"
|
|
383
|
-
);
|
|
384
|
-
}
|
|
385
398
|
const colors = getColorAttribute(mesh.attributes.COLOR_0 || mesh.attributes.COLOR, pointCount);
|
|
386
399
|
const constantRGBA = colors ? null : getConstantRgba(options.constantRGBA);
|
|
387
400
|
const normals = getNormalAttribute(mesh.attributes.NORMAL, pointCount);
|
|
388
401
|
const batchIds = getBatchIds(mesh.attributes.BATCH_ID, pointCount);
|
|
389
402
|
const batchTableJson = validateBatchTable(options.batchTableJson, batchIds);
|
|
390
|
-
|
|
403
|
+
const pnts = import_d_tiles.Tile3DWriter.encodeSync(
|
|
391
404
|
{
|
|
392
405
|
type: import_d_tiles.TILE3D_TYPE.POINT_CLOUD,
|
|
393
406
|
featureTableJson: options.rtcCenter || constantRGBA ? {
|
|
@@ -399,6 +412,13 @@ function encodePointCloudTile(pointBatch, options = {}) {
|
|
|
399
412
|
},
|
|
400
413
|
{}
|
|
401
414
|
);
|
|
415
|
+
return {
|
|
416
|
+
pnts,
|
|
417
|
+
pointCount,
|
|
418
|
+
rtcCenter: placement.rtcCenter,
|
|
419
|
+
localBoundingBox: placement.localBoundingBox,
|
|
420
|
+
maximumPositionError: placement.maximumPositionError
|
|
421
|
+
};
|
|
402
422
|
}
|
|
403
423
|
function validatePointAttributes(attributes) {
|
|
404
424
|
const supportedAttributes = /* @__PURE__ */ new Set([
|
|
@@ -429,23 +449,88 @@ function getConstantRgba(constantRGBA) {
|
|
|
429
449
|
}
|
|
430
450
|
return [constantRGBA[0], constantRGBA[1], constantRGBA[2], constantRGBA[3]];
|
|
431
451
|
}
|
|
432
|
-
function
|
|
433
|
-
if (
|
|
452
|
+
function validatePointCloudPosition(attribute) {
|
|
453
|
+
if (!attribute || attribute.size !== 3 || attribute.byteOffset || attribute.byteStride || attribute.normalized || attribute.componentType) {
|
|
454
|
+
throw new import_core2.TileConversionError(
|
|
455
|
+
"POINT_CLOUD_POSITION_REQUIRED",
|
|
456
|
+
"Point cloud batches must contain packed, non-normalized three-component POSITION values"
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
if (!attribute.value.length || attribute.value.length % 3) {
|
|
460
|
+
throw new import_core2.TileConversionError(
|
|
461
|
+
"POINT_CLOUD_POSITION_COUNT_INVALID",
|
|
462
|
+
"Point cloud POSITION must contain one or more xyz triples"
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
const transform = attribute.transform;
|
|
466
|
+
if (transform && (transform.type !== "quantization" || !Number.isInteger(transform.bits) || transform.bits < 1 || transform.bits > 32 || transform.origin.length !== 3 || transform.origin.some((value) => !Number.isFinite(value)) || !Number.isFinite(transform.range) || transform.range < 0)) {
|
|
467
|
+
throw new import_core2.TileConversionError(
|
|
468
|
+
"POINT_CLOUD_POSITION_TRANSFORM_INVALID",
|
|
469
|
+
"Position quantization requires finite xyz origin, nonnegative range and 1\u201332 bits"
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function decodePointCloudPositions(attribute) {
|
|
474
|
+
validatePointCloudPosition(attribute);
|
|
475
|
+
const transform = attribute.transform;
|
|
476
|
+
return Float64Array.from(
|
|
477
|
+
attribute.value,
|
|
478
|
+
(value, index) => transform ? transform.origin[index % 3] + value / (2 ** transform.bits - 1) * transform.range : value
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
function encodePointPositions(attribute, options) {
|
|
482
|
+
const { rtcCenter = [0, 0, 0], maxPositionError = Infinity } = options;
|
|
483
|
+
if (rtcCenter.length !== 3 || rtcCenter.some((value) => !Number.isFinite(value))) {
|
|
434
484
|
throw new import_core2.TileConversionError(
|
|
435
485
|
"POINT_CLOUD_RTC_CENTER_INVALID",
|
|
436
486
|
"Point cloud RTC_CENTER must contain three finite values"
|
|
437
487
|
);
|
|
438
488
|
}
|
|
489
|
+
if (options.maxPositionError !== void 0 && (!Number.isFinite(maxPositionError) || maxPositionError < 0)) {
|
|
490
|
+
throw new import_core2.TileConversionError(
|
|
491
|
+
"POINT_CLOUD_PRECISION_INVALID",
|
|
492
|
+
"maxPositionError must be finite and nonnegative"
|
|
493
|
+
);
|
|
494
|
+
}
|
|
439
495
|
const values = attribute.value;
|
|
440
|
-
const
|
|
496
|
+
const positions = new Float32Array(values.length);
|
|
497
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
498
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
499
|
+
let maximumPositionError = 0;
|
|
441
500
|
const transform = attribute.transform;
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
501
|
+
const errors = [0, 0, 0];
|
|
502
|
+
for (let point = 0; point < values.length; point += 3) {
|
|
503
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
504
|
+
const value = values[point + axis];
|
|
505
|
+
const absoluteValue = transform ? transform.origin[axis] + value / (2 ** transform.bits - 1) * transform.range : value;
|
|
506
|
+
const relativeValue = absoluteValue - rtcCenter[axis];
|
|
507
|
+
positions[point + axis] = relativeValue;
|
|
508
|
+
const encodedValue = positions[point + axis];
|
|
509
|
+
const reconstructed = encodedValue + rtcCenter[axis];
|
|
510
|
+
if (!Number.isFinite(absoluteValue) || !Number.isFinite(relativeValue) || !Number.isFinite(encodedValue) || !Number.isFinite(reconstructed)) {
|
|
511
|
+
throw new import_core2.TileConversionError(
|
|
512
|
+
"POINT_CLOUD_POSITION_INVALID",
|
|
513
|
+
"Point positions and their float32 reconstruction must be finite"
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
errors[axis] = reconstructed - absoluteValue;
|
|
517
|
+
minimum[axis] = Math.min(minimum[axis], encodedValue);
|
|
518
|
+
maximum[axis] = Math.max(maximum[axis], encodedValue);
|
|
519
|
+
}
|
|
520
|
+
maximumPositionError = Math.max(maximumPositionError, Math.hypot(...errors));
|
|
521
|
+
}
|
|
522
|
+
if (!Number.isFinite(maximumPositionError) || maximumPositionError > maxPositionError) {
|
|
523
|
+
throw new import_core2.TileConversionError(
|
|
524
|
+
"POINT_CLOUD_PRECISION_EXCEEDED",
|
|
525
|
+
"Float32 position reconstruction exceeds maxPositionError; select a closer RTC center"
|
|
526
|
+
);
|
|
447
527
|
}
|
|
448
|
-
return
|
|
528
|
+
return {
|
|
529
|
+
positions,
|
|
530
|
+
rtcCenter: [...rtcCenter],
|
|
531
|
+
localBoundingBox: [minimum, maximum],
|
|
532
|
+
maximumPositionError
|
|
533
|
+
};
|
|
449
534
|
}
|
|
450
535
|
function getColorAttribute(attribute, pointCount) {
|
|
451
536
|
if (!attribute) {
|
|
@@ -558,10 +643,191 @@ function validateBatchTable(batchTableJson, batchIds) {
|
|
|
558
643
|
return normalizedBatchTable;
|
|
559
644
|
}
|
|
560
645
|
|
|
646
|
+
// src/v5/point-cloud-spatial.ts
|
|
647
|
+
var import_core3 = require("@math.gl/core");
|
|
648
|
+
var import_tiles = require("@loaders.gl/tiles");
|
|
649
|
+
var import_schema_utils3 = require("@loaders.gl/schema-utils");
|
|
650
|
+
var import_core4 = require("@loaders.gl/tile-converter/v5/core");
|
|
651
|
+
async function transformPointCloudSourceTile(sourceTile, options) {
|
|
652
|
+
options.signal?.throwIfAborted();
|
|
653
|
+
const { content } = sourceTile;
|
|
654
|
+
if (!content) return sourceTile;
|
|
655
|
+
const reference = options.spatialContext.spatialReference;
|
|
656
|
+
const contentReference = content.spatialReference;
|
|
657
|
+
const retainTransformedEcef = contentReference?.status === "transformed" && (contentReference.targetCrs || contentReference.sourceCrs) === "EPSG:4978" && reference.status === "native" && reference.sourceCrs === "EPSG:4978" && reference.verticalUnitScale === 1 && (reference.units?.every((unit) => unit === "meter") ?? true);
|
|
658
|
+
const sourceReference = retainTransformedEcef ? (0, import_tiles.createTilesetSpatialReference)({
|
|
659
|
+
sourceCrs: "EPSG:4978",
|
|
660
|
+
coordinateFrame: "geocentric",
|
|
661
|
+
axisOrder: "xyz",
|
|
662
|
+
units: reference.units,
|
|
663
|
+
coordinateEpoch: contentReference.coordinateEpoch,
|
|
664
|
+
heightReference: contentReference.targetHeightReference === "native" ? contentReference.heightReference : contentReference.targetHeightReference
|
|
665
|
+
}) : contentReference;
|
|
666
|
+
if (sourceReference && (sourceReference.status === "transformed" || sourceReference.status === "unresolved" || JSON.stringify(sourceReference.sourceCrs) !== JSON.stringify(reference.sourceCrs) || sourceReference.heightReference !== reference.heightReference || sourceReference.verticalUnitScale !== reference.verticalUnitScale || sourceReference.coordinateEpoch !== reference.coordinateEpoch || JSON.stringify(sourceReference.units) !== JSON.stringify(reference.units) || sourceReference.elevationMode !== reference.elevationMode || sourceReference.elevationOffset !== reference.elevationOffset || sourceReference.elevationUnitScale !== reference.elevationUnitScale)) {
|
|
667
|
+
throw new import_core4.TileConversionError(
|
|
668
|
+
"POINT_CLOUD_SPATIAL_REFERENCE_MISMATCH",
|
|
669
|
+
"Point content and context must agree on the input CRS, units, height reference and epoch"
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
const sourceFrame = reference.coordinateFrame === "unknown" && reference.sourceCrs ? (0, import_tiles.getSpatialCoordinateFrame)(reference.sourceCrs) : reference.coordinateFrame;
|
|
673
|
+
const geographic = sourceFrame === "geographic";
|
|
674
|
+
if (!["xy", "xyz", "unknown"].includes(reference.axisOrder) || (geographic ? !["lnglat", "lnglat-offsets"].includes(content.coordinateSystem) : content.coordinateSystem !== "cartesian" || !["projected", "geocentric", "cartesian", "local"].includes(sourceFrame))) {
|
|
675
|
+
throw new import_core4.TileConversionError(
|
|
676
|
+
"POINT_CLOUD_SOURCE_FRAME_UNSUPPORTED",
|
|
677
|
+
"Spatial preparation requires explicit Cartesian or longitude/latitude positions in source xyz order"
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
if (content.cartographicOrigin.length !== 3 || content.cartographicOrigin.some((value) => !Number.isFinite(value)) || content.coordinateSystem !== "lnglat-offsets" && content.cartographicOrigin.some((value) => value !== 0)) {
|
|
681
|
+
throw new import_core4.TileConversionError(
|
|
682
|
+
"POINT_CLOUD_SOURCE_ORIGIN_INVALID",
|
|
683
|
+
"Only longitude/latitude offsets use a nonzero cartographic origin"
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
if (content.modelMatrix && (content.modelMatrix.length !== 16 || Array.from(content.modelMatrix).some((value) => !Number.isFinite(value)))) {
|
|
687
|
+
throw new import_core4.TileConversionError(
|
|
688
|
+
"POINT_CLOUD_SOURCE_PLACEMENT_UNSUPPORTED",
|
|
689
|
+
"Point placement must contain sixteen finite matrix values"
|
|
690
|
+
);
|
|
691
|
+
}
|
|
692
|
+
const matrix = new import_core3.Matrix4(content.modelMatrix ? Array.from(content.modelMatrix) : void 0);
|
|
693
|
+
const determinant = matrix.determinant();
|
|
694
|
+
if (matrix[3] !== 0 || matrix[7] !== 0 || matrix[11] !== 0 || matrix[15] !== 1 || !Number.isFinite(determinant) || determinant === 0 || geographic && !matrix.equals(new import_core3.Matrix4())) {
|
|
695
|
+
throw new import_core4.TileConversionError(
|
|
696
|
+
"POINT_CLOUD_SOURCE_PLACEMENT_UNSUPPORTED",
|
|
697
|
+
"Cartesian placement must be finite, affine and invertible; geographic placement must be identity"
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const mesh = (0, import_schema_utils3.convertTableToMesh)(content.data);
|
|
701
|
+
const positionAttribute = mesh.attributes.POSITION;
|
|
702
|
+
const declaredTransform = mesh.schema?.fields.find((field) => field.name === "POSITION")?.metadata?.["loaders.gl.transform"];
|
|
703
|
+
if (declaredTransform && !positionAttribute?.transform) {
|
|
704
|
+
throw new import_core4.TileConversionError(
|
|
705
|
+
"POINT_CLOUD_POSITION_TRANSFORM_INVALID",
|
|
706
|
+
"Arrow position transform metadata is unsupported or invalid"
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
const sourcePositions = decodePointCloudPositions(positionAttribute);
|
|
710
|
+
if (sourcePositions.some((value) => !Number.isFinite(value))) {
|
|
711
|
+
throw new import_core4.TileConversionError(
|
|
712
|
+
"POINT_CLOUD_POSITION_INVALID",
|
|
713
|
+
"Decoded point positions must be finite"
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
const translationOnly = Array.from(matrix).every(
|
|
717
|
+
(value, index) => index >= 12 && index <= 14 || value === (index % 5 === 0 ? 1 : 0)
|
|
718
|
+
);
|
|
719
|
+
for (let index = 0; index < sourcePositions.length; index += 3) {
|
|
720
|
+
if (geographic || translationOnly) {
|
|
721
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
722
|
+
sourcePositions[index + axis] += geographic ? content.cartographicOrigin[axis] : matrix[12 + axis];
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
sourcePositions.set(
|
|
726
|
+
matrix.transformAsPoint(Array.from(sourcePositions.subarray(index, index + 3))),
|
|
727
|
+
index
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
const normalAttribute = mesh.attributes.NORMAL;
|
|
732
|
+
let normals;
|
|
733
|
+
if (normalAttribute) {
|
|
734
|
+
if (!options.normalReferenceFrame || normalAttribute.size !== 3 || normalAttribute.value.length !== sourcePositions.length || normalAttribute.transform || normalAttribute.normalized || normalAttribute.byteOffset || normalAttribute.byteStride || normalAttribute.componentType || !translationOnly) {
|
|
735
|
+
throw new import_core4.TileConversionError(
|
|
736
|
+
"POINT_CLOUD_NORMAL_FRAME_REQUIRED",
|
|
737
|
+
"Spatial point normals require a declared vector frame and packed xyz values"
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
normals = new Float32Array(normalAttribute.value.length);
|
|
741
|
+
for (let index = 0; index < normals.length; index += 3) {
|
|
742
|
+
const normal = Array.from(normalAttribute.value.slice(index, index + 3));
|
|
743
|
+
const length = Math.hypot(...normal);
|
|
744
|
+
if (!Number.isFinite(length) || length === 0) {
|
|
745
|
+
throw new import_core4.TileConversionError(
|
|
746
|
+
"POINT_CLOUD_NORMAL_INVALID",
|
|
747
|
+
"Spatial point normals must be finite and nonzero"
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
normals.set(
|
|
751
|
+
normal.map((value) => value / length),
|
|
752
|
+
index
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
const transformed = await options.spatialContext.transformGeometryAsync(
|
|
757
|
+
sourcePositions,
|
|
758
|
+
normals,
|
|
759
|
+
options.normalReferenceFrame
|
|
760
|
+
);
|
|
761
|
+
options.signal?.throwIfAborted();
|
|
762
|
+
if (transformed.positions.length !== sourcePositions.length) {
|
|
763
|
+
throw new import_core4.TileConversionError(
|
|
764
|
+
"POINT_CLOUD_POSITION_INVALID",
|
|
765
|
+
"Spatial conversion must return one xyz triple per point"
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
769
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
770
|
+
for (let index = 0; index < transformed.positions.length; index++) {
|
|
771
|
+
const value = transformed.positions[index];
|
|
772
|
+
if (!Number.isFinite(value)) {
|
|
773
|
+
throw new import_core4.TileConversionError(
|
|
774
|
+
"POINT_CLOUD_POSITION_INVALID",
|
|
775
|
+
"Transformed point positions must be finite"
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
const axis = index % 3;
|
|
779
|
+
minimum[axis] = Math.min(minimum[axis], value);
|
|
780
|
+
maximum[axis] = Math.max(maximum[axis], value);
|
|
781
|
+
}
|
|
782
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
783
|
+
const targetFrame = reference.status === "native" ? sourceFrame : (0, import_tiles.getSpatialCoordinateFrame)(reference.targetCrs || reference.sourceCrs);
|
|
784
|
+
const targetIsGeographic = targetFrame === "geographic";
|
|
785
|
+
const attributes = {
|
|
786
|
+
...mesh.attributes,
|
|
787
|
+
POSITION: { value: transformed.positions, size: 3 },
|
|
788
|
+
...transformed.normals ? { NORMAL: { value: transformed.normals, size: 3 } } : {}
|
|
789
|
+
};
|
|
790
|
+
return {
|
|
791
|
+
header: sourceTile.header,
|
|
792
|
+
content: {
|
|
793
|
+
...content,
|
|
794
|
+
data: (0, import_schema_utils3.makeMeshArrowTable)(attributes, {
|
|
795
|
+
topology: "point-list",
|
|
796
|
+
boundingBox: [minimum, maximum],
|
|
797
|
+
schema: mesh.schema ? {
|
|
798
|
+
...mesh.schema,
|
|
799
|
+
metadata: {
|
|
800
|
+
...mesh.schema.metadata,
|
|
801
|
+
topology: "point-list",
|
|
802
|
+
boundingBox: JSON.stringify([minimum, maximum])
|
|
803
|
+
},
|
|
804
|
+
fields: mesh.schema.fields.map(
|
|
805
|
+
(field) => field.name === "POSITION" || transformed.normals && field.name === "NORMAL" ? { ...field, metadata: {} } : field
|
|
806
|
+
)
|
|
807
|
+
} : void 0
|
|
808
|
+
}),
|
|
809
|
+
coordinateSystem: targetIsGeographic ? "lnglat" : "cartesian",
|
|
810
|
+
cartographicOrigin: [0, 0, 0],
|
|
811
|
+
modelMatrix: void 0,
|
|
812
|
+
spatialReference: reference,
|
|
813
|
+
spatialBoundingVolume: {
|
|
814
|
+
cartographicBounds: [minimum, maximum],
|
|
815
|
+
coordinateFrame: targetIsGeographic ? "geographic" : "cartesian",
|
|
816
|
+
center,
|
|
817
|
+
radius: Math.hypot(...maximum.map((value, axis) => value - center[axis]))
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
|
|
561
823
|
// src/v5/point-cloud-source-encoder.ts
|
|
562
|
-
var
|
|
824
|
+
var import_core5 = require("@loaders.gl/tile-converter/v5/core");
|
|
563
825
|
async function* encodePointCloudSource(source, options = {}) {
|
|
564
|
-
for await (const
|
|
826
|
+
for await (const inputTile of (0, import_core5.traversePointCloudSource)(source, options)) {
|
|
827
|
+
const sourceTile = options.spatialContext ? await transformPointCloudSourceTile(inputTile, {
|
|
828
|
+
...options,
|
|
829
|
+
spatialContext: options.spatialContext
|
|
830
|
+
}) : inputTile;
|
|
565
831
|
const encodingOptions = sourceTile.content ? options.getTileEncodingOptions?.(sourceTile) : void 0;
|
|
566
832
|
const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
|
|
567
833
|
if (encodedTile) {
|
|
@@ -574,14 +840,20 @@ function encodePointCloudSourceTile(sourceTile, options = {}) {
|
|
|
574
840
|
if (!content) {
|
|
575
841
|
return null;
|
|
576
842
|
}
|
|
843
|
+
const encoded = encodePointCloudTileWithMetadata(content.data, {
|
|
844
|
+
...options,
|
|
845
|
+
constantRGBA: options.constantRGBA ?? content.constantRGBA
|
|
846
|
+
});
|
|
847
|
+
if (content.pointCount !== encoded.pointCount) {
|
|
848
|
+
throw new import_core5.TileConversionError(
|
|
849
|
+
"POINT_CLOUD_SOURCE_COUNT_INVALID",
|
|
850
|
+
"Source pointCount must match the encoded Arrow point rows"
|
|
851
|
+
);
|
|
852
|
+
}
|
|
577
853
|
return {
|
|
578
854
|
id: header.id,
|
|
579
855
|
header,
|
|
580
|
-
|
|
581
|
-
...options,
|
|
582
|
-
constantRGBA: options.constantRGBA ?? content.constantRGBA
|
|
583
|
-
}),
|
|
584
|
-
pointCount: content.pointCount,
|
|
856
|
+
...encoded,
|
|
585
857
|
coordinateSystem: content.coordinateSystem,
|
|
586
858
|
cartographicOrigin: [...content.cartographicOrigin],
|
|
587
859
|
modelMatrix: content.modelMatrix ? Array.from(content.modelMatrix) : void 0,
|
|
@@ -591,20 +863,25 @@ function encodePointCloudSourceTile(sourceTile, options = {}) {
|
|
|
591
863
|
}
|
|
592
864
|
|
|
593
865
|
// src/v5/point-cloud-conversion.ts
|
|
594
|
-
var
|
|
595
|
-
var
|
|
866
|
+
var import_core6 = require("@loaders.gl/tile-converter/v5/core");
|
|
867
|
+
var import_core7 = require("@loaders.gl/tile-converter/v5/core");
|
|
596
868
|
function convertPointCloudSource(source, options) {
|
|
597
|
-
return (0,
|
|
869
|
+
return (0, import_core6.convertTileset)({
|
|
598
870
|
source: {
|
|
599
871
|
async inspect() {
|
|
600
872
|
if (!source.isReady) {
|
|
601
873
|
await source.initialize();
|
|
602
874
|
}
|
|
603
875
|
},
|
|
604
|
-
read: (_inspection, signal) => (0,
|
|
876
|
+
read: (_inspection, signal) => (0, import_core7.traversePointCloudSource)(source, { maxDepth: options.maxDepth, signal })
|
|
605
877
|
},
|
|
606
878
|
codec: {
|
|
607
|
-
async *convert(
|
|
879
|
+
async *convert(inputTile, _inspection, signal) {
|
|
880
|
+
const sourceTile = options.spatialContext ? await transformPointCloudSourceTile(inputTile, {
|
|
881
|
+
...options,
|
|
882
|
+
spatialContext: options.spatialContext,
|
|
883
|
+
signal
|
|
884
|
+
}) : inputTile;
|
|
608
885
|
const encodingOptions = sourceTile.content ? options.getTileEncodingOptions?.(sourceTile) : void 0;
|
|
609
886
|
const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
|
|
610
887
|
if (encodedTile) {
|
|
@@ -625,9 +902,257 @@ function convertPointCloudSource(source, options) {
|
|
|
625
902
|
// src/v5/mesh.ts
|
|
626
903
|
var import_gltf = require("@loaders.gl/gltf");
|
|
627
904
|
var import_images = require("@loaders.gl/images");
|
|
628
|
-
|
|
905
|
+
|
|
906
|
+
// src/v5/mesh-features.ts
|
|
907
|
+
var import_schema_utils4 = require("@loaders.gl/schema-utils");
|
|
908
|
+
var import_core8 = require("@loaders.gl/tile-converter/v5/core");
|
|
909
|
+
function prepareMeshFeatureGeometry(mesh, features) {
|
|
910
|
+
if (!features) return mesh;
|
|
911
|
+
const count = features.batches.reduce((total, batch) => total + batch.data.numRows, 0);
|
|
912
|
+
const indices = mesh.indices?.value;
|
|
913
|
+
const vertexCount = indices?.length ?? mesh.attributes.POSITION.value.length / 3;
|
|
914
|
+
const associations = features.triangleFeatureIndices;
|
|
915
|
+
if (!(associations instanceof Uint32Array) || associations.length !== vertexCount / 3 || associations.some((row) => row >= count) || new Set(associations).size !== count) {
|
|
916
|
+
throw new import_core8.TileConversionError(
|
|
917
|
+
"MESH_FEATURE_ASSOCIATION_INVALID",
|
|
918
|
+
"Every feature row must own a triangle and every triangle must reference a valid row"
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
if (count > 2 ** 24)
|
|
922
|
+
throw new import_core8.TileConversionError(
|
|
923
|
+
"MESH_FEATURE_COUNT_UNSUPPORTED",
|
|
924
|
+
"Feature row indices must be exactly representable in a glTF vertex attribute"
|
|
925
|
+
);
|
|
926
|
+
const attributes = Object.fromEntries(
|
|
927
|
+
Object.entries(mesh.attributes).map(([name, attribute]) => {
|
|
928
|
+
const values = attribute.value;
|
|
929
|
+
const Constructor = values.constructor;
|
|
930
|
+
const output = new Constructor(vertexCount * attribute.size);
|
|
931
|
+
for (let vertex = 0; vertex < vertexCount; vertex++) {
|
|
932
|
+
const sourceVertex = indices?.[vertex] ?? vertex;
|
|
933
|
+
for (let component = 0; component < attribute.size; component++)
|
|
934
|
+
output[vertex * attribute.size + component] = Number(
|
|
935
|
+
values[sourceVertex * attribute.size + component]
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
return [name, { ...attribute, value: output }];
|
|
939
|
+
})
|
|
940
|
+
);
|
|
941
|
+
const featureIndices = count <= 65536 ? new Uint16Array(vertexCount) : new Float32Array(vertexCount);
|
|
942
|
+
for (let vertex = 0; vertex < vertexCount; vertex++)
|
|
943
|
+
featureIndices[vertex] = associations[Math.floor(vertex / 3)];
|
|
944
|
+
attributes._FEATURE_ID_0 = { value: featureIndices, size: 1 };
|
|
945
|
+
return { ...mesh, indices: void 0, attributes };
|
|
946
|
+
}
|
|
947
|
+
function encodeMeshFeatures(scenegraph, meshIndex, features) {
|
|
948
|
+
if (!features) return;
|
|
949
|
+
const { batches, featureIdField } = features;
|
|
950
|
+
const schema = batches[0]?.schema;
|
|
951
|
+
const count = batches.reduce((total, batch) => total + batch.data.numRows, 0);
|
|
952
|
+
const metadataClass = schema?.metadata?.["loaders.gl:feature-class"] || "features";
|
|
953
|
+
if (!schema || !count || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(metadataClass) || !schema.fields.some((field) => field.name === featureIdField && field.nullable !== true) || new Set(schema.fields.map((field) => field.name)).size !== schema.fields.length) {
|
|
954
|
+
throw new import_core8.TileConversionError(
|
|
955
|
+
"MESH_FEATURE_SCHEMA_INVALID",
|
|
956
|
+
"Features require a single named class, unique fields, and a non-null stable identifier"
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
for (const batch of batches) {
|
|
960
|
+
const actual = (0, import_schema_utils4.convertArrowToSchema)(batch.data.schema).fields;
|
|
961
|
+
if (JSON.stringify(batch.schema) !== JSON.stringify(schema) || batch.length !== batch.data.numRows || JSON.stringify(actual.map((field) => [field.name, field.type])) !== JSON.stringify(schema.fields.map((field) => [field.name, field.type]))) {
|
|
962
|
+
throw new import_core8.TileConversionError(
|
|
963
|
+
"MESH_FEATURE_SCHEMA_INVALID",
|
|
964
|
+
"Declared feature schemas and Arrow column types must agree across batches"
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
const properties = /* @__PURE__ */ Object.create(null);
|
|
969
|
+
const columns = /* @__PURE__ */ Object.create(null);
|
|
970
|
+
for (const field of schema.fields) {
|
|
971
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field.name))
|
|
972
|
+
throw new import_core8.TileConversionError(
|
|
973
|
+
"MESH_FEATURE_SCHEMA_INVALID",
|
|
974
|
+
"Property names must be metadata identifiers"
|
|
975
|
+
);
|
|
976
|
+
const values = batches.flatMap((batch) => {
|
|
977
|
+
const column = batch.data.getChild(field.name);
|
|
978
|
+
if (!column)
|
|
979
|
+
throw new import_core8.TileConversionError(
|
|
980
|
+
"MESH_FEATURE_SCHEMA_INVALID",
|
|
981
|
+
`Missing Arrow column ${field.name}`
|
|
982
|
+
);
|
|
983
|
+
return Array.from({ length: column.length }, (_, index) => column.get(index));
|
|
984
|
+
});
|
|
985
|
+
if (field.name === featureIdField && (values.some(
|
|
986
|
+
(value) => !(typeof value === "string" || typeof value === "bigint" || Number.isSafeInteger(value))
|
|
987
|
+
) || new Set(values).size !== count))
|
|
988
|
+
throw new import_core8.TileConversionError(
|
|
989
|
+
"MESH_FEATURE_IDS_INVALID",
|
|
990
|
+
"Stable identifiers must be unique strings or exact integers"
|
|
991
|
+
);
|
|
992
|
+
const encoded = encodeFeatureColumn(field, values);
|
|
993
|
+
properties[field.name] = encoded.property;
|
|
994
|
+
columns[field.name] = {
|
|
995
|
+
values: addMetadataBuffer(scenegraph, encoded.data),
|
|
996
|
+
...encoded.offsets ? {
|
|
997
|
+
stringOffsets: addMetadataBuffer(scenegraph, encoded.offsets),
|
|
998
|
+
stringOffsetType: "UINT32"
|
|
999
|
+
} : {}
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
scenegraph.json.extensions ||= {};
|
|
1003
|
+
scenegraph.json.extensions.EXT_structural_metadata = {
|
|
1004
|
+
schema: { id: "loaders_gl_features", classes: { [metadataClass]: { properties } } },
|
|
1005
|
+
propertyTables: [{ class: metadataClass, count, properties: columns }]
|
|
1006
|
+
};
|
|
1007
|
+
const primitive = scenegraph.json.meshes[meshIndex].primitives[0];
|
|
1008
|
+
primitive.extensions ||= {};
|
|
1009
|
+
primitive.extensions.EXT_mesh_features = {
|
|
1010
|
+
featureIds: [{ featureCount: count, attribute: 0, propertyTable: 0 }]
|
|
1011
|
+
};
|
|
1012
|
+
scenegraph.registerUsedExtension("EXT_structural_metadata");
|
|
1013
|
+
scenegraph.registerUsedExtension("EXT_mesh_features");
|
|
1014
|
+
}
|
|
1015
|
+
function addMetadataBuffer(scenegraph, data) {
|
|
1016
|
+
if (scenegraph.byteLength % 8) scenegraph.addBufferView(new Uint8Array(4));
|
|
1017
|
+
return scenegraph.addBufferView(data);
|
|
1018
|
+
}
|
|
1019
|
+
function encodeFeatureColumn(field, values) {
|
|
1020
|
+
const type = field.type;
|
|
1021
|
+
const hasNull = values.includes(null);
|
|
1022
|
+
if (hasNull && field.nullable !== true)
|
|
1023
|
+
throw new import_core8.TileConversionError(
|
|
1024
|
+
"MESH_FEATURE_VALUE_INVALID",
|
|
1025
|
+
`Required field ${field.name} contains null`
|
|
1026
|
+
);
|
|
1027
|
+
const present = values.filter((value) => value !== null);
|
|
1028
|
+
let noData;
|
|
1029
|
+
if (type === "utf8") {
|
|
1030
|
+
if (present.some(
|
|
1031
|
+
(value) => typeof value !== "string" || new TextDecoder().decode(new TextEncoder().encode(value)) !== value
|
|
1032
|
+
))
|
|
1033
|
+
throw new import_core8.TileConversionError("MESH_FEATURE_VALUE_INVALID", "Strings require valid Unicode");
|
|
1034
|
+
if (hasNull) {
|
|
1035
|
+
noData = "";
|
|
1036
|
+
const strings = new Set(present);
|
|
1037
|
+
while (strings.has(noData)) noData += "_";
|
|
1038
|
+
}
|
|
1039
|
+
const encoded = values.map(
|
|
1040
|
+
(value) => new TextEncoder().encode(value === null ? noData : value)
|
|
1041
|
+
);
|
|
1042
|
+
const offsets = new Uint32Array(values.length + 1);
|
|
1043
|
+
for (let index = 0; index < values.length; index++)
|
|
1044
|
+
offsets[index + 1] = offsets[index] + encoded[index].length;
|
|
1045
|
+
const data2 = new Uint8Array(offsets[values.length]);
|
|
1046
|
+
encoded.forEach((bytes, index) => data2.set(bytes, offsets[index]));
|
|
1047
|
+
return {
|
|
1048
|
+
property: { type: "STRING", required: field.nullable !== true, ...hasNull ? { noData } : {} },
|
|
1049
|
+
data: data2,
|
|
1050
|
+
offsets
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
const widths = {
|
|
1054
|
+
int8: 1,
|
|
1055
|
+
uint8: 1,
|
|
1056
|
+
int16: 2,
|
|
1057
|
+
uint16: 2,
|
|
1058
|
+
int32: 4,
|
|
1059
|
+
uint32: 4,
|
|
1060
|
+
int64: 8,
|
|
1061
|
+
uint64: 8,
|
|
1062
|
+
float32: 4,
|
|
1063
|
+
float64: 8
|
|
1064
|
+
};
|
|
1065
|
+
const width = widths[String(type)];
|
|
1066
|
+
if (!width)
|
|
1067
|
+
throw new import_core8.TileConversionError(
|
|
1068
|
+
"MESH_FEATURE_TYPE_UNSUPPORTED",
|
|
1069
|
+
`No qualified GLB property mapping for ${String(type)}`
|
|
1070
|
+
);
|
|
1071
|
+
const isInteger64 = type === "int64" || type === "uint64";
|
|
1072
|
+
const isFloat = type === "float32" || type === "float64";
|
|
1073
|
+
const minimum = isFloat ? -Infinity : String(type).startsWith("uint") ? 0n : -(1n << BigInt(width * 8 - 1));
|
|
1074
|
+
const maximum = isFloat ? Infinity : (1n << BigInt(width * 8 - (String(type).startsWith("uint") ? 0 : 1))) - 1n;
|
|
1075
|
+
if (present.some(
|
|
1076
|
+
(value) => isInteger64 ? typeof value !== "bigint" || value < minimum || value > maximum : typeof value !== "number" || !Number.isFinite(value) || !isFloat && (!Number.isSafeInteger(value) || BigInt(value) < minimum || BigInt(value) > maximum)
|
|
1077
|
+
))
|
|
1078
|
+
throw new import_core8.TileConversionError(
|
|
1079
|
+
"MESH_FEATURE_VALUE_INVALID",
|
|
1080
|
+
`Values do not fit ${String(type)}`
|
|
1081
|
+
);
|
|
1082
|
+
if (hasNull) {
|
|
1083
|
+
const used = new Set(present.map(String));
|
|
1084
|
+
noData = 0;
|
|
1085
|
+
while (used.has(String(noData))) noData++;
|
|
1086
|
+
if (noData > maximum && minimum < 0) {
|
|
1087
|
+
noData = -1;
|
|
1088
|
+
while (used.has(String(noData))) noData--;
|
|
1089
|
+
}
|
|
1090
|
+
if (noData > maximum || noData < minimum)
|
|
1091
|
+
throw new import_core8.TileConversionError(
|
|
1092
|
+
"MESH_FEATURE_NULL_UNREPRESENTABLE",
|
|
1093
|
+
"No unused value remains for null without losing a real value"
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
const data = new Uint8Array(values.length * width);
|
|
1097
|
+
const view = new DataView(data.buffer);
|
|
1098
|
+
values.forEach((value, index) => {
|
|
1099
|
+
const selected = value === null ? isInteger64 ? BigInt(noData) : noData : value;
|
|
1100
|
+
const offset = index * width;
|
|
1101
|
+
switch (type) {
|
|
1102
|
+
case "int8":
|
|
1103
|
+
view.setInt8(offset, selected);
|
|
1104
|
+
break;
|
|
1105
|
+
case "uint8":
|
|
1106
|
+
view.setUint8(offset, selected);
|
|
1107
|
+
break;
|
|
1108
|
+
case "int16":
|
|
1109
|
+
view.setInt16(offset, selected, true);
|
|
1110
|
+
break;
|
|
1111
|
+
case "uint16":
|
|
1112
|
+
view.setUint16(offset, selected, true);
|
|
1113
|
+
break;
|
|
1114
|
+
case "int32":
|
|
1115
|
+
view.setInt32(offset, selected, true);
|
|
1116
|
+
break;
|
|
1117
|
+
case "uint32":
|
|
1118
|
+
view.setUint32(offset, selected, true);
|
|
1119
|
+
break;
|
|
1120
|
+
case "int64":
|
|
1121
|
+
view.setBigInt64(offset, selected, true);
|
|
1122
|
+
break;
|
|
1123
|
+
case "uint64":
|
|
1124
|
+
view.setBigUint64(offset, selected, true);
|
|
1125
|
+
break;
|
|
1126
|
+
case "float32":
|
|
1127
|
+
view.setFloat32(offset, selected, true);
|
|
1128
|
+
break;
|
|
1129
|
+
case "float64":
|
|
1130
|
+
view.setFloat64(offset, selected, true);
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
if (isFloat && !Number.isFinite(
|
|
1134
|
+
type === "float32" ? view.getFloat32(offset, true) : view.getFloat64(offset, true)
|
|
1135
|
+
))
|
|
1136
|
+
throw new import_core8.TileConversionError(
|
|
1137
|
+
"MESH_FEATURE_VALUE_INVALID",
|
|
1138
|
+
"Floating point property overflow"
|
|
1139
|
+
);
|
|
1140
|
+
});
|
|
1141
|
+
return {
|
|
1142
|
+
property: {
|
|
1143
|
+
type: "SCALAR",
|
|
1144
|
+
componentType: String(type).toUpperCase(),
|
|
1145
|
+
required: field.nullable !== true,
|
|
1146
|
+
...hasNull ? { noData } : {}
|
|
1147
|
+
},
|
|
1148
|
+
data
|
|
1149
|
+
};
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// src/v5/mesh.ts
|
|
1153
|
+
var import_core9 = require("@loaders.gl/tile-converter/v5/core");
|
|
629
1154
|
function encodeMeshTile(mesh, options = {}) {
|
|
630
|
-
const geometry = validateMeshGeometry(mesh);
|
|
1155
|
+
const geometry = prepareMeshFeatureGeometry(validateMeshGeometry(mesh), options.features);
|
|
631
1156
|
const { scenegraph, materialIndex } = createMeshTileScenegraph(options, geometry);
|
|
632
1157
|
const meshIndex = scenegraph.addMesh({
|
|
633
1158
|
attributes: geometry.attributes,
|
|
@@ -635,6 +1160,7 @@ function encodeMeshTile(mesh, options = {}) {
|
|
|
635
1160
|
material: materialIndex,
|
|
636
1161
|
mode: 4
|
|
637
1162
|
});
|
|
1163
|
+
encodeMeshFeatures(scenegraph, meshIndex, options.features);
|
|
638
1164
|
return finalizeMeshTileScenegraph(scenegraph, meshIndex);
|
|
639
1165
|
}
|
|
640
1166
|
function createMeshTileScenegraph(options, geometry) {
|
|
@@ -651,14 +1177,14 @@ function finalizeMeshTileScenegraph(scenegraph, meshIndex) {
|
|
|
651
1177
|
}
|
|
652
1178
|
function validateMeshGeometry(mesh, allowFloat64Positions = false) {
|
|
653
1179
|
if (mesh.topology !== "triangle-list" || mesh.mode !== 4) {
|
|
654
|
-
throw new
|
|
1180
|
+
throw new import_core9.TileConversionError(
|
|
655
1181
|
"MESH_TOPOLOGY_UNSUPPORTED",
|
|
656
1182
|
"Only triangle-list mode 4 is supported"
|
|
657
1183
|
);
|
|
658
1184
|
}
|
|
659
1185
|
for (const name of Object.keys(mesh.attributes)) {
|
|
660
1186
|
if (name !== "POSITION" && name !== "NORMAL" && name !== "COLOR_0" && name !== "TEXCOORD_0") {
|
|
661
|
-
throw new
|
|
1187
|
+
throw new import_core9.TileConversionError(
|
|
662
1188
|
"MESH_ATTRIBUTE_UNSUPPORTED",
|
|
663
1189
|
`Mesh attribute ${name} is unsupported`
|
|
664
1190
|
);
|
|
@@ -671,14 +1197,14 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
|
|
|
671
1197
|
if (mesh.attributes.NORMAL) {
|
|
672
1198
|
const normals = getFloatAttribute(mesh.attributes.NORMAL, "NORMAL");
|
|
673
1199
|
if (normals.length !== positions.length) {
|
|
674
|
-
throw new
|
|
1200
|
+
throw new import_core9.TileConversionError(
|
|
675
1201
|
"MESH_NORMAL_COUNT_MISMATCH",
|
|
676
1202
|
"NORMAL count must match POSITION count"
|
|
677
1203
|
);
|
|
678
1204
|
}
|
|
679
1205
|
for (let index = 0; index < normals.length; index += 3) {
|
|
680
1206
|
if (Math.abs(Math.hypot(normals[index], normals[index + 1], normals[index + 2]) - 1) > 1e-4) {
|
|
681
|
-
throw new
|
|
1207
|
+
throw new import_core9.TileConversionError(
|
|
682
1208
|
"MESH_NORMAL_INVALID",
|
|
683
1209
|
"NORMAL vectors must have unit length"
|
|
684
1210
|
);
|
|
@@ -690,7 +1216,7 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
|
|
|
690
1216
|
const colors = mesh.attributes.COLOR_0;
|
|
691
1217
|
const normalized = isNormalizedUnsignedAttribute(colors);
|
|
692
1218
|
if (!colors || !(colors.value instanceof Float32Array || normalized) || colors.size !== 3 && colors.size !== 4 || colors.value.length / colors.size !== positions.length / 3 || colors.value instanceof Float32Array && colors.value.some((value) => !Number.isFinite(value) || value < 0 || value > 1)) {
|
|
693
|
-
throw new
|
|
1219
|
+
throw new import_core9.TileConversionError(
|
|
694
1220
|
"MESH_COLOR_INVALID",
|
|
695
1221
|
"COLOR_0 requires packed linear RGB/RGBA: Float32 in [0, 1] or normalized Uint8/Uint16, one per vertex"
|
|
696
1222
|
);
|
|
@@ -706,7 +1232,7 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
|
|
|
706
1232
|
const textureCoordinates = mesh.attributes.TEXCOORD_0;
|
|
707
1233
|
const normalized = isNormalizedUnsignedAttribute(textureCoordinates);
|
|
708
1234
|
if (!textureCoordinates || !(textureCoordinates.value instanceof Float32Array || normalized) || textureCoordinates.size !== 2 || textureCoordinates.value.length / 2 !== positions.length / 3 || textureCoordinates.value.some((value) => !Number.isFinite(value))) {
|
|
709
|
-
throw new
|
|
1235
|
+
throw new import_core9.TileConversionError(
|
|
710
1236
|
"MESH_TEXCOORD_INVALID",
|
|
711
1237
|
"TEXCOORD_0 requires packed finite Float32 or normalized Uint8/Uint16 UV pairs, one per vertex"
|
|
712
1238
|
);
|
|
@@ -718,9 +1244,9 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
|
|
|
718
1244
|
...normalized ? { normalized: true } : {}
|
|
719
1245
|
};
|
|
720
1246
|
}
|
|
721
|
-
const indices =
|
|
1247
|
+
const indices = validateMeshIndices(mesh.indices, positions.length / 3);
|
|
722
1248
|
if (!indices && positions.length % 9 !== 0) {
|
|
723
|
-
throw new
|
|
1249
|
+
throw new import_core9.TileConversionError(
|
|
724
1250
|
"MESH_TRIANGLE_COUNT_INVALID",
|
|
725
1251
|
"Nonindexed meshes require complete triangles"
|
|
726
1252
|
);
|
|
@@ -734,36 +1260,36 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
|
|
|
734
1260
|
}
|
|
735
1261
|
function getFloatAttribute(attribute, name, allowFloat64 = false) {
|
|
736
1262
|
if (!attribute || !(attribute.value instanceof Float32Array || allowFloat64 && attribute.value instanceof Float64Array) || attribute.size !== 3 || attribute.value.length === 0 || attribute.value.length % 3 !== 0) {
|
|
737
|
-
throw new
|
|
1263
|
+
throw new import_core9.TileConversionError(
|
|
738
1264
|
"MESH_ATTRIBUTE_INVALID",
|
|
739
1265
|
`${name} must contain packed ${allowFloat64 ? "Float32 or Float64" : "Float32"} xyz triples`
|
|
740
1266
|
);
|
|
741
1267
|
}
|
|
742
1268
|
validateAttributeLayout(attribute, name);
|
|
743
1269
|
if (attribute.value.some((value) => !Number.isFinite(value))) {
|
|
744
|
-
throw new
|
|
1270
|
+
throw new import_core9.TileConversionError("MESH_ATTRIBUTE_NONFINITE", `${name} must contain finite values`);
|
|
745
1271
|
}
|
|
746
1272
|
return attribute.value;
|
|
747
1273
|
}
|
|
748
|
-
function
|
|
1274
|
+
function validateMeshIndices(attribute, vertexCount, primitiveMode = 4) {
|
|
749
1275
|
if (!attribute) return void 0;
|
|
750
1276
|
validateAttributeLayout(attribute, "indices");
|
|
751
1277
|
const values = attribute.value;
|
|
752
1278
|
if (attribute.size !== 1 || !(values instanceof Uint8Array || values instanceof Uint16Array || values instanceof Uint32Array)) {
|
|
753
|
-
throw new
|
|
1279
|
+
throw new import_core9.TileConversionError(
|
|
754
1280
|
"MESH_INDICES_INVALID",
|
|
755
1281
|
"Indices must contain packed unsigned integer scalars"
|
|
756
1282
|
);
|
|
757
1283
|
}
|
|
758
|
-
if (values.length === 0 || values.length % 3 !== 0) {
|
|
759
|
-
throw new
|
|
1284
|
+
if (primitiveMode === 5 ? values.length < 3 : values.length === 0 || values.length % 3 !== 0) {
|
|
1285
|
+
throw new import_core9.TileConversionError(
|
|
760
1286
|
"MESH_TRIANGLE_COUNT_INVALID",
|
|
761
|
-
"Indices must contain complete triangles"
|
|
1287
|
+
"Indices must contain complete triangles or at least three strip vertices"
|
|
762
1288
|
);
|
|
763
1289
|
}
|
|
764
1290
|
const restartIndex = 2 ** (values.BYTES_PER_ELEMENT * 8) - 1;
|
|
765
1291
|
if (values.some((value) => value >= vertexCount || value === restartIndex)) {
|
|
766
|
-
throw new
|
|
1292
|
+
throw new import_core9.TileConversionError(
|
|
767
1293
|
"MESH_INDEX_OUT_OF_RANGE",
|
|
768
1294
|
"Indices must reference existing vertices and exclude primitive-restart values"
|
|
769
1295
|
);
|
|
@@ -775,7 +1301,7 @@ function isNormalizedUnsignedAttribute(attribute) {
|
|
|
775
1301
|
}
|
|
776
1302
|
function validateAttributeLayout(attribute, name, allowNormalized = false) {
|
|
777
1303
|
if ((attribute.byteOffset ?? 0) !== 0 || (attribute.byteStride ?? 0) !== 0 || attribute.normalized && !allowNormalized || attribute.transform || attribute.componentType) {
|
|
778
|
-
throw new
|
|
1304
|
+
throw new import_core9.TileConversionError(
|
|
779
1305
|
"MESH_ATTRIBUTE_LAYOUT_UNSUPPORTED",
|
|
780
1306
|
`${name} must use packed contiguous storage with supported normalization`
|
|
781
1307
|
);
|
|
@@ -783,7 +1309,7 @@ function validateAttributeLayout(attribute, name, allowNormalized = false) {
|
|
|
783
1309
|
}
|
|
784
1310
|
function validateMeshMaterial(material, scenegraph, hasTextureCoordinates) {
|
|
785
1311
|
if (!material || typeof material !== "object" || Array.isArray(material)) {
|
|
786
|
-
throw new
|
|
1312
|
+
throw new import_core9.TileConversionError("MESH_MATERIAL_INVALID", "Material must be an object");
|
|
787
1313
|
}
|
|
788
1314
|
const allowedProperties = [
|
|
789
1315
|
"baseColorFactor",
|
|
@@ -796,7 +1322,7 @@ function validateMeshMaterial(material, scenegraph, hasTextureCoordinates) {
|
|
|
796
1322
|
];
|
|
797
1323
|
for (const name of Object.keys(material)) {
|
|
798
1324
|
if (!allowedProperties.includes(name)) {
|
|
799
|
-
throw new
|
|
1325
|
+
throw new import_core9.TileConversionError(
|
|
800
1326
|
"MESH_MATERIAL_UNSUPPORTED",
|
|
801
1327
|
`Material property ${name} is unsupported`
|
|
802
1328
|
);
|
|
@@ -808,7 +1334,7 @@ function validateMeshMaterial(material, scenegraph, hasTextureCoordinates) {
|
|
|
808
1334
|
)) || [metallicFactor, roughnessFactor].some(
|
|
809
1335
|
(value) => value !== void 0 && (!Number.isFinite(value) || value < 0 || value > 1)
|
|
810
1336
|
) || alphaMode !== void 0 && !["OPAQUE", "MASK", "BLEND"].includes(alphaMode) || alphaCutoff !== void 0 && (alphaMode !== "MASK" || !Number.isFinite(alphaCutoff) || alphaCutoff < 0) || doubleSided !== void 0 && typeof doubleSided !== "boolean") {
|
|
811
|
-
throw new
|
|
1337
|
+
throw new import_core9.TileConversionError(
|
|
812
1338
|
"MESH_MATERIAL_INVALID",
|
|
813
1339
|
"Material values must satisfy the selected glTF profile"
|
|
814
1340
|
);
|
|
@@ -829,13 +1355,13 @@ function encodeBaseColorTexture(texture, scenegraph, hasTextureCoordinates) {
|
|
|
829
1355
|
if (!texture || Object.keys(texture).some(
|
|
830
1356
|
(name) => name !== "data" && name !== "mimeType" && name !== "sampler" && name !== "transform"
|
|
831
1357
|
) || !(texture.data instanceof Uint8Array) || !["image/png", "image/jpeg"].includes(texture.mimeType)) {
|
|
832
|
-
throw new
|
|
1358
|
+
throw new import_core9.TileConversionError(
|
|
833
1359
|
"MESH_TEXTURE_INVALID",
|
|
834
1360
|
"Base-color texture must supply Uint8Array data, image/png or image/jpeg mimeType, and optional sampling/transform controls"
|
|
835
1361
|
);
|
|
836
1362
|
}
|
|
837
1363
|
if (!hasTextureCoordinates) {
|
|
838
|
-
throw new
|
|
1364
|
+
throw new import_core9.TileConversionError(
|
|
839
1365
|
"MESH_TEXCOORD_REQUIRED",
|
|
840
1366
|
"Base-color texture requires TEXCOORD_0"
|
|
841
1367
|
);
|
|
@@ -847,7 +1373,7 @@ function encodeBaseColorTexture(texture, scenegraph, hasTextureCoordinates) {
|
|
|
847
1373
|
} catch {
|
|
848
1374
|
}
|
|
849
1375
|
if (!metadata || metadata.mimeType !== texture.mimeType || metadata.width <= 0 || metadata.height <= 0) {
|
|
850
|
-
throw new
|
|
1376
|
+
throw new import_core9.TileConversionError(
|
|
851
1377
|
"MESH_TEXTURE_INVALID",
|
|
852
1378
|
"Image header must match mimeType and declare positive dimensions"
|
|
853
1379
|
);
|
|
@@ -865,7 +1391,7 @@ function encodeBaseColorTexture(texture, scenegraph, hasTextureCoordinates) {
|
|
|
865
1391
|
}
|
|
866
1392
|
function validateMeshSampler(sampler) {
|
|
867
1393
|
if (!sampler || typeof sampler !== "object" || Array.isArray(sampler)) {
|
|
868
|
-
throw new
|
|
1394
|
+
throw new import_core9.TileConversionError("MESH_SAMPLER_INVALID", "Sampler must be an object");
|
|
869
1395
|
}
|
|
870
1396
|
const allowedValues = {
|
|
871
1397
|
wrapS: [33071, 33648, 10497],
|
|
@@ -875,7 +1401,7 @@ function validateMeshSampler(sampler) {
|
|
|
875
1401
|
};
|
|
876
1402
|
for (const name in sampler) {
|
|
877
1403
|
if (!Object.hasOwn(allowedValues, name)) {
|
|
878
|
-
throw new
|
|
1404
|
+
throw new import_core9.TileConversionError(
|
|
879
1405
|
"MESH_SAMPLER_UNSUPPORTED",
|
|
880
1406
|
`Sampler property ${name} is unsupported`
|
|
881
1407
|
);
|
|
@@ -890,7 +1416,7 @@ function validateMeshSampler(sampler) {
|
|
|
890
1416
|
for (const name of Object.keys(allowedValues)) {
|
|
891
1417
|
const property = name;
|
|
892
1418
|
if (selectedSampler[property] !== void 0 && !allowedValues[property].includes(selectedSampler[property])) {
|
|
893
|
-
throw new
|
|
1419
|
+
throw new import_core9.TileConversionError(
|
|
894
1420
|
"MESH_SAMPLER_INVALID",
|
|
895
1421
|
`Sampler ${name} must use a glTF wrapping/filtering value`
|
|
896
1422
|
);
|
|
@@ -900,14 +1426,14 @@ function validateMeshSampler(sampler) {
|
|
|
900
1426
|
}
|
|
901
1427
|
function validateMeshTextureTransform(transform) {
|
|
902
1428
|
if (!transform || typeof transform !== "object" || Array.isArray(transform)) {
|
|
903
|
-
throw new
|
|
1429
|
+
throw new import_core9.TileConversionError(
|
|
904
1430
|
"MESH_TEXTURE_TRANSFORM_INVALID",
|
|
905
1431
|
"Texture transform must be an object"
|
|
906
1432
|
);
|
|
907
1433
|
}
|
|
908
1434
|
for (const name in transform) {
|
|
909
1435
|
if (name !== "offset" && name !== "rotation" && name !== "scale") {
|
|
910
|
-
throw new
|
|
1436
|
+
throw new import_core9.TileConversionError(
|
|
911
1437
|
"MESH_TEXTURE_TRANSFORM_UNSUPPORTED",
|
|
912
1438
|
`Texture transform property ${name} is unsupported`
|
|
913
1439
|
);
|
|
@@ -919,7 +1445,7 @@ function validateMeshTextureTransform(transform) {
|
|
|
919
1445
|
if ([offsetValues, scaleValues].some(
|
|
920
1446
|
(value) => value !== void 0 && (!Array.isArray(value) || value.length !== 2 || value.some((component) => !Number.isFinite(component)))
|
|
921
1447
|
) || rotation !== void 0 && !Number.isFinite(rotation)) {
|
|
922
|
-
throw new
|
|
1448
|
+
throw new import_core9.TileConversionError(
|
|
923
1449
|
"MESH_TEXTURE_TRANSFORM_INVALID",
|
|
924
1450
|
"Texture transform requires finite UV pairs and a finite rotation"
|
|
925
1451
|
);
|
|
@@ -932,15 +1458,15 @@ function validateMeshTextureTransform(transform) {
|
|
|
932
1458
|
}
|
|
933
1459
|
|
|
934
1460
|
// src/v5/mesh-conversion.ts
|
|
935
|
-
var
|
|
936
|
-
var
|
|
1461
|
+
var import_tiles2 = require("@loaders.gl/tiles");
|
|
1462
|
+
var import_core11 = require("@loaders.gl/tile-converter/v5/core");
|
|
937
1463
|
|
|
938
1464
|
// src/v5/mesh-draco.ts
|
|
939
1465
|
var import_draco = require("@loaders.gl/draco");
|
|
940
|
-
var
|
|
1466
|
+
var import_core10 = require("@loaders.gl/tile-converter/v5/core");
|
|
941
1467
|
var DRACO_EXTENSION = "KHR_draco_mesh_compression";
|
|
942
1468
|
async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
|
|
943
|
-
geometry = validateMeshGeometry(geometry);
|
|
1469
|
+
geometry = prepareMeshFeatureGeometry(validateMeshGeometry(geometry), options.features);
|
|
944
1470
|
if (!geometry.indices) {
|
|
945
1471
|
geometry.indices = {
|
|
946
1472
|
size: 1,
|
|
@@ -962,7 +1488,7 @@ async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
|
|
|
962
1488
|
const decoded = await loader.parse(encoded.data, loaderOptions);
|
|
963
1489
|
const positions = decoded.attributes.POSITION.value;
|
|
964
1490
|
if (!(positions instanceof Float32Array) || positions.length !== encoded.report.pointCount * 3 || encoded.report.pointCount === 0 || !decoded.indices || decoded.indices.value.length !== encoded.report.faceCount * 3 || encoded.report.faceCount * 3 !== (geometry.indices?.value.length ?? geometry.attributes.POSITION.value.length / 3)) {
|
|
965
|
-
throw new
|
|
1491
|
+
throw new import_core10.TileConversionError(
|
|
966
1492
|
"MESH_DRACO_ENCODING_INVALID",
|
|
967
1493
|
"Draco must preserve the triangle count and emit finite positions"
|
|
968
1494
|
);
|
|
@@ -974,12 +1500,18 @@ async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
|
|
|
974
1500
|
}
|
|
975
1501
|
for (let index = 0; index < positions.length; index += 3) {
|
|
976
1502
|
if (!positionKeys.has(getPositionKey(positions, index))) {
|
|
977
|
-
throw new
|
|
1503
|
+
throw new import_core10.TileConversionError(
|
|
978
1504
|
"MESH_DRACO_POSITION_CHANGED",
|
|
979
1505
|
"Lossless Draco must preserve the encoded Float32 positions"
|
|
980
1506
|
);
|
|
981
1507
|
}
|
|
982
1508
|
}
|
|
1509
|
+
if (JSON.stringify(getMeshTriangles(geometry)) !== JSON.stringify(getMeshTriangles(decoded))) {
|
|
1510
|
+
throw new import_core10.TileConversionError(
|
|
1511
|
+
"MESH_DRACO_ATTRIBUTE_CHANGED",
|
|
1512
|
+
"Lossless Draco must preserve oriented triangles and every attribute, including feature ownership"
|
|
1513
|
+
);
|
|
1514
|
+
}
|
|
983
1515
|
const bufferView = scenegraph.addBufferView(encoded.data);
|
|
984
1516
|
const attributes = {};
|
|
985
1517
|
const attributeIdentifiers = {};
|
|
@@ -987,12 +1519,12 @@ async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
|
|
|
987
1519
|
for (const [name, attribute] of Object.entries(geometry.attributes)) {
|
|
988
1520
|
const decodedAttribute = decoded.attributes[name];
|
|
989
1521
|
if (!decodedAttribute || decodedAttribute.value.length !== encoded.report.pointCount * attribute.size) {
|
|
990
|
-
throw new
|
|
1522
|
+
throw new import_core10.TileConversionError("MESH_DRACO_ENCODING_INVALID", `Draco must preserve ${name}`);
|
|
991
1523
|
}
|
|
992
1524
|
const accessor = {
|
|
993
|
-
componentType: attribute.value instanceof Uint8Array ? 5121 : attribute.value instanceof Uint16Array ? 5123 : 5126,
|
|
1525
|
+
componentType: attribute.value instanceof Uint8Array ? 5121 : attribute.value instanceof Uint16Array ? 5123 : attribute.value instanceof Uint32Array ? 5125 : 5126,
|
|
994
1526
|
count: encoded.report.pointCount,
|
|
995
|
-
type: attribute.size === 2 ? "VEC2" : attribute.size === 3 ? "VEC3" : "VEC4",
|
|
1527
|
+
type: attribute.size === 1 ? "SCALAR" : attribute.size === 2 ? "VEC2" : attribute.size === 3 ? "VEC3" : "VEC4",
|
|
996
1528
|
...attribute.normalized ? { normalized: true } : {}
|
|
997
1529
|
};
|
|
998
1530
|
if (name === "POSITION") {
|
|
@@ -1027,30 +1559,51 @@ async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
|
|
|
1027
1559
|
]
|
|
1028
1560
|
}
|
|
1029
1561
|
];
|
|
1562
|
+
encodeMeshFeatures(scenegraph, 0, options.features);
|
|
1030
1563
|
scenegraph.registerRequiredExtension(DRACO_EXTENSION);
|
|
1031
1564
|
return { glb: finalizeMeshTileScenegraph(scenegraph, 0), positions };
|
|
1032
1565
|
}
|
|
1033
1566
|
function getPositionKey(positions, index) {
|
|
1034
1567
|
return `${positions[index]},${positions[index + 1]},${positions[index + 2]}`;
|
|
1035
1568
|
}
|
|
1569
|
+
function getMeshTriangles(mesh) {
|
|
1570
|
+
const names = Object.keys(mesh.attributes).sort();
|
|
1571
|
+
const indices = mesh.indices.value;
|
|
1572
|
+
const triangles = [];
|
|
1573
|
+
for (let index = 0; index < indices.length; index += 3) {
|
|
1574
|
+
const vertices = [0, 1, 2].map(
|
|
1575
|
+
(corner) => names.map((name) => {
|
|
1576
|
+
const attribute = mesh.attributes[name];
|
|
1577
|
+
return Array.from(
|
|
1578
|
+
{ length: attribute.size },
|
|
1579
|
+
(_, component) => attribute.value[indices[index + corner] * attribute.size + component]
|
|
1580
|
+
).join(",");
|
|
1581
|
+
}).join("|")
|
|
1582
|
+
);
|
|
1583
|
+
triangles.push(
|
|
1584
|
+
[0, 1, 2].map((corner) => [...vertices.slice(corner), ...vertices.slice(0, corner)].join(";")).sort()[0]
|
|
1585
|
+
);
|
|
1586
|
+
}
|
|
1587
|
+
return triangles.sort();
|
|
1588
|
+
}
|
|
1036
1589
|
|
|
1037
1590
|
// src/v5/mesh-conversion.ts
|
|
1038
1591
|
function createMeshConversionCodec(options) {
|
|
1039
1592
|
const { spatialContext, maxPositionError, draco = true, dracoLibraryOptions } = options;
|
|
1040
1593
|
if (typeof draco !== "boolean") {
|
|
1041
|
-
throw new
|
|
1594
|
+
throw new import_core11.TileConversionError("MESH_DRACO_OPTIONS_INVALID", "draco must be a boolean");
|
|
1042
1595
|
}
|
|
1043
1596
|
if (!Number.isFinite(maxPositionError) || maxPositionError < 0) {
|
|
1044
|
-
throw new
|
|
1597
|
+
throw new import_core11.TileConversionError(
|
|
1045
1598
|
"MESH_PRECISION_LIMIT_INVALID",
|
|
1046
1599
|
"maxPositionError must be finite and nonnegative"
|
|
1047
1600
|
);
|
|
1048
1601
|
}
|
|
1049
1602
|
const spatialReference = spatialContext.spatialReference;
|
|
1050
1603
|
const outputCrs = spatialReference.targetCrs || spatialReference.sourceCrs;
|
|
1051
|
-
const outputFrame = spatialReference.status === "native" ? spatialReference.coordinateFrame : outputCrs ? (0,
|
|
1604
|
+
const outputFrame = spatialReference.status === "native" ? spatialReference.coordinateFrame : outputCrs ? (0, import_tiles2.getSpatialCoordinateFrame)(outputCrs) : "unknown";
|
|
1052
1605
|
if (!["native", "transformed"].includes(spatialReference.status) || !["projected", "geocentric", "local", "cartesian"].includes(outputFrame)) {
|
|
1053
|
-
throw new
|
|
1606
|
+
throw new import_core11.TileConversionError(
|
|
1054
1607
|
"MESH_OUTPUT_FRAME_UNSUPPORTED",
|
|
1055
1608
|
"Mesh output requires a resolved Cartesian coordinate frame"
|
|
1056
1609
|
);
|
|
@@ -1059,28 +1612,41 @@ function createMeshConversionCodec(options) {
|
|
|
1059
1612
|
/** Prepares and encodes one geometry while honoring cancellation. */
|
|
1060
1613
|
async *convert(resource, _inspection, signal) {
|
|
1061
1614
|
signal?.throwIfAborted();
|
|
1062
|
-
if ("features" in resource && resource.features) {
|
|
1063
|
-
throw new import_core8.TileConversionError(
|
|
1064
|
-
"MESH_FEATURE_OUTPUT_UNSUPPORTED",
|
|
1065
|
-
"The GLB codec requires an explicit feature metadata writer; use the I3S codec for supported feature-bearing meshes"
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
1615
|
const geometry = validateMeshGeometry(resource.mesh, true);
|
|
1069
1616
|
const sourcePositions = geometry.attributes.POSITION.value;
|
|
1070
1617
|
const origin = [...resource.origin];
|
|
1071
1618
|
if (origin.length !== 3 || origin.some((value) => !Number.isFinite(value))) {
|
|
1072
|
-
throw new
|
|
1619
|
+
throw new import_core11.TileConversionError(
|
|
1073
1620
|
"MESH_ORIGIN_INVALID",
|
|
1074
1621
|
"Target origin must contain three finite coordinates"
|
|
1075
1622
|
);
|
|
1076
1623
|
}
|
|
1077
|
-
const
|
|
1624
|
+
const transformed = "transformGeometryAsync" in spatialContext ? await spatialContext.transformGeometryAsync(
|
|
1625
|
+
sourcePositions,
|
|
1626
|
+
geometry.attributes.NORMAL?.value,
|
|
1627
|
+
resource.normalReferenceFrame ?? options.normalReferenceFrame
|
|
1628
|
+
) : {
|
|
1629
|
+
positions: spatialContext.transformPositions(sourcePositions),
|
|
1630
|
+
normals: geometry.attributes.NORMAL ? spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions) : void 0
|
|
1631
|
+
};
|
|
1632
|
+
signal?.throwIfAborted();
|
|
1633
|
+
const targetPositions = transformed.positions;
|
|
1078
1634
|
if (targetPositions.length !== sourcePositions.length || targetPositions.some((value) => !Number.isFinite(value))) {
|
|
1079
|
-
throw new
|
|
1635
|
+
throw new import_core11.TileConversionError(
|
|
1080
1636
|
"MESH_TRANSFORM_INVALID",
|
|
1081
1637
|
"Spatial transformation must return one finite xyz position per vertex"
|
|
1082
1638
|
);
|
|
1083
1639
|
}
|
|
1640
|
+
if (options.autoOrigin) {
|
|
1641
|
+
const minimum2 = [Infinity, Infinity, Infinity];
|
|
1642
|
+
const maximum2 = [-Infinity, -Infinity, -Infinity];
|
|
1643
|
+
for (let index = 0; index < targetPositions.length; index++) {
|
|
1644
|
+
const axis = index % 3;
|
|
1645
|
+
minimum2[axis] = Math.min(minimum2[axis], targetPositions[index]);
|
|
1646
|
+
maximum2[axis] = Math.max(maximum2[axis], targetPositions[index]);
|
|
1647
|
+
}
|
|
1648
|
+
for (let axis = 0; axis < 3; axis++) origin[axis] = minimum2[axis] / 2 + maximum2[axis] / 2;
|
|
1649
|
+
}
|
|
1084
1650
|
const localPositions = new Float32Array(targetPositions.length);
|
|
1085
1651
|
const minimum = [Infinity, Infinity, Infinity];
|
|
1086
1652
|
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
@@ -1097,7 +1663,7 @@ function createMeshConversionCodec(options) {
|
|
|
1097
1663
|
localPositions[index + 2] + origin[2] - targetPositions[index + 2]
|
|
1098
1664
|
);
|
|
1099
1665
|
if (!Number.isFinite(positionError) || positionError > maxPositionError) {
|
|
1100
|
-
throw new
|
|
1666
|
+
throw new import_core11.TileConversionError(
|
|
1101
1667
|
"MESH_POSITION_PRECISION_EXCEEDED",
|
|
1102
1668
|
"Float32 mesh positions exceed the configured reconstruction error budget"
|
|
1103
1669
|
);
|
|
@@ -1107,11 +1673,11 @@ function createMeshConversionCodec(options) {
|
|
|
1107
1673
|
geometry.attributes.POSITION = { value: localPositions, size: 3 };
|
|
1108
1674
|
if (geometry.attributes.NORMAL) {
|
|
1109
1675
|
geometry.attributes.NORMAL = {
|
|
1110
|
-
value:
|
|
1676
|
+
value: transformed.normals,
|
|
1111
1677
|
size: 3
|
|
1112
1678
|
};
|
|
1113
1679
|
}
|
|
1114
|
-
const appearance = { material: resource.material };
|
|
1680
|
+
const appearance = { material: resource.material, features: resource.features };
|
|
1115
1681
|
const encoded = draco ? await encodeDracoMeshTile(geometry, appearance, dracoLibraryOptions) : { glb: encodeMeshTile(geometry, appearance), positions: localPositions };
|
|
1116
1682
|
signal?.throwIfAborted();
|
|
1117
1683
|
for (let index = 0; index < encoded.positions.length; index++) {
|
|
@@ -1124,6 +1690,8 @@ function createMeshConversionCodec(options) {
|
|
|
1124
1690
|
}
|
|
1125
1691
|
yield {
|
|
1126
1692
|
id: resource.id,
|
|
1693
|
+
contentResourceId: resource.contentResourceId,
|
|
1694
|
+
contentPrimitive: resource.contentPrimitive,
|
|
1127
1695
|
glb: encoded.glb,
|
|
1128
1696
|
origin,
|
|
1129
1697
|
boundingBox: [minimum, maximum],
|
|
@@ -1147,17 +1715,17 @@ function createMeshConversionCodec(options) {
|
|
|
1147
1715
|
}
|
|
1148
1716
|
|
|
1149
1717
|
// src/v5/mesh-tileset-sink.ts
|
|
1150
|
-
var
|
|
1151
|
-
var
|
|
1718
|
+
var import_core12 = require("@loaders.gl/tile-converter/v5/core");
|
|
1719
|
+
var import_core13 = require("@loaders.gl/tile-converter/v5/core");
|
|
1152
1720
|
function createSingleMeshTilesetSink(options) {
|
|
1153
1721
|
const { geometricError } = options;
|
|
1154
1722
|
if (!Number.isFinite(geometricError) || geometricError < 0) {
|
|
1155
|
-
throw new
|
|
1723
|
+
throw new import_core12.TileConversionError(
|
|
1156
1724
|
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1157
1725
|
"geometricError must be finite and nonnegative, in meters"
|
|
1158
1726
|
);
|
|
1159
1727
|
}
|
|
1160
|
-
const memorySink = (0,
|
|
1728
|
+
const memorySink = (0, import_core13.createBoundedMemoryTileConversionSink)(options);
|
|
1161
1729
|
let state = "empty";
|
|
1162
1730
|
let completed = false;
|
|
1163
1731
|
return {
|
|
@@ -1167,7 +1735,7 @@ function createSingleMeshTilesetSink(options) {
|
|
|
1167
1735
|
async write(resource, signal) {
|
|
1168
1736
|
signal?.throwIfAborted();
|
|
1169
1737
|
if (state !== "empty") {
|
|
1170
|
-
throw new
|
|
1738
|
+
throw new import_core12.TileConversionError(
|
|
1171
1739
|
"SINGLE_MESH_SINK_UNAVAILABLE",
|
|
1172
1740
|
"The sink accepts exactly one mesh before finalization or abort"
|
|
1173
1741
|
);
|
|
@@ -1186,7 +1754,7 @@ function createSingleMeshTilesetSink(options) {
|
|
|
1186
1754
|
},
|
|
1187
1755
|
async finalize(report) {
|
|
1188
1756
|
if (state !== "written") {
|
|
1189
|
-
throw new
|
|
1757
|
+
throw new import_core12.TileConversionError(
|
|
1190
1758
|
"SINGLE_MESH_SINK_INCOMPLETE",
|
|
1191
1759
|
"Finalization requires exactly one successfully written mesh"
|
|
1192
1760
|
);
|
|
@@ -1207,13 +1775,13 @@ function createMeshTileset(resource, geometricError) {
|
|
|
1207
1775
|
const outputCrs = reference.targetCrs || reference.sourceCrs;
|
|
1208
1776
|
const heightReference = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
|
|
1209
1777
|
if (!["native", "transformed"].includes(reference.status) || outputCrs !== "EPSG:4978" || heightReference !== "ellipsoidal" || reference.status === "native" && (reference.coordinateFrame !== "geocentric" || reference.axisOrder !== "xyz" || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter"))) {
|
|
1210
|
-
throw new
|
|
1778
|
+
throw new import_core12.TileConversionError(
|
|
1211
1779
|
"MESH_TILESET_FRAME_UNSUPPORTED",
|
|
1212
1780
|
"Single-mesh tilesets require resolved EPSG:4978 xyz coordinates in meters with ellipsoidal heights"
|
|
1213
1781
|
);
|
|
1214
1782
|
}
|
|
1215
1783
|
if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
|
|
1216
|
-
throw new
|
|
1784
|
+
throw new import_core12.TileConversionError(
|
|
1217
1785
|
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1218
1786
|
"geometricError must include the measured position reconstruction error"
|
|
1219
1787
|
);
|
|
@@ -1221,7 +1789,7 @@ function createMeshTileset(resource, geometricError) {
|
|
|
1221
1789
|
const origin = [...resource.origin];
|
|
1222
1790
|
const [minimum, maximum] = resource.localBoundingBox.map((bound) => [...bound]);
|
|
1223
1791
|
if (origin.length !== 3 || minimum.length !== 3 || maximum.length !== 3 || [...origin, ...minimum, ...maximum].some((value) => !Number.isFinite(value)) || minimum.some((value, axis) => value > maximum[axis])) {
|
|
1224
|
-
throw new
|
|
1792
|
+
throw new import_core12.TileConversionError(
|
|
1225
1793
|
"MESH_TILESET_BOUNDS_INVALID",
|
|
1226
1794
|
"Origin and local bounds must be finite xyz coordinates with ordered bounds"
|
|
1227
1795
|
);
|
|
@@ -1260,12 +1828,12 @@ function createMeshTileset(resource, geometricError) {
|
|
|
1260
1828
|
function createMeshTilesetSink(options) {
|
|
1261
1829
|
const { geometricError, maxMeshes } = options;
|
|
1262
1830
|
if (!Number.isFinite(geometricError) || geometricError < 0 || !Number.isSafeInteger(maxMeshes) || maxMeshes < 1) {
|
|
1263
|
-
throw new
|
|
1831
|
+
throw new import_core12.TileConversionError(
|
|
1264
1832
|
"MESH_TILESET_OPTIONS_INVALID",
|
|
1265
1833
|
"A leaf collection requires finite nonnegative geometricError and positive maxMeshes"
|
|
1266
1834
|
);
|
|
1267
1835
|
}
|
|
1268
|
-
const memory = (0,
|
|
1836
|
+
const memory = (0, import_core13.createBoundedMemoryTileConversionSink)(options);
|
|
1269
1837
|
const children = [];
|
|
1270
1838
|
const identifiers = /* @__PURE__ */ new Set();
|
|
1271
1839
|
const minimum = [Infinity, Infinity, Infinity];
|
|
@@ -1279,12 +1847,12 @@ function createMeshTilesetSink(options) {
|
|
|
1279
1847
|
async write(resource, signal) {
|
|
1280
1848
|
signal?.throwIfAborted();
|
|
1281
1849
|
if (state !== "open")
|
|
1282
|
-
throw new
|
|
1850
|
+
throw new import_core12.TileConversionError(
|
|
1283
1851
|
"MESH_TILESET_SINK_UNAVAILABLE",
|
|
1284
1852
|
"The mesh sink is busy or closed"
|
|
1285
1853
|
);
|
|
1286
1854
|
if (children.length >= maxMeshes || identifiers.has(resource.id))
|
|
1287
|
-
throw new
|
|
1855
|
+
throw new import_core12.TileConversionError(
|
|
1288
1856
|
"MESH_TILESET_PLACEMENT_INVALID",
|
|
1289
1857
|
"Mesh count exceeds maxMeshes or a placement identifier is repeated"
|
|
1290
1858
|
);
|
|
@@ -1296,7 +1864,7 @@ function createMeshTilesetSink(options) {
|
|
|
1296
1864
|
(value, axis) => value + resource.origin[axis]
|
|
1297
1865
|
);
|
|
1298
1866
|
if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
|
|
1299
|
-
throw new
|
|
1867
|
+
throw new import_core12.TileConversionError(
|
|
1300
1868
|
"MESH_TILESET_BOUNDS_INVALID",
|
|
1301
1869
|
"Absolute bounds must be finite"
|
|
1302
1870
|
);
|
|
@@ -1318,7 +1886,7 @@ function createMeshTilesetSink(options) {
|
|
|
1318
1886
|
/** Writes the enclosing root only when every leaf has completed. */
|
|
1319
1887
|
async finalize(report) {
|
|
1320
1888
|
if (state !== "open" || !children.length)
|
|
1321
|
-
throw new
|
|
1889
|
+
throw new import_core12.TileConversionError(
|
|
1322
1890
|
"MESH_TILESET_SINK_INCOMPLETE",
|
|
1323
1891
|
"Finalization requires written mesh leaves"
|
|
1324
1892
|
);
|
|
@@ -1327,7 +1895,7 @@ function createMeshTilesetSink(options) {
|
|
|
1327
1895
|
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
1328
1896
|
const rootGeometricError = Math.max(geometricError, 2 * Math.hypot(...halfSize));
|
|
1329
1897
|
if (!Number.isFinite(rootGeometricError))
|
|
1330
|
-
throw new
|
|
1898
|
+
throw new import_core12.TileConversionError(
|
|
1331
1899
|
"MESH_TILESET_BOUNDS_INVALID",
|
|
1332
1900
|
"Root bounds must have a finite diagonal"
|
|
1333
1901
|
);
|
|
@@ -1379,7 +1947,7 @@ function createMeshTilesetSink(options) {
|
|
|
1379
1947
|
// src/v5/conversion-archive.ts
|
|
1380
1948
|
var import_d_tiles_archive_writer = require("@loaders.gl/3d-tiles/3d-tiles-archive-writer");
|
|
1381
1949
|
var import_i3s_slpk_writer = require("@loaders.gl/i3s/i3s-slpk-writer");
|
|
1382
|
-
var
|
|
1950
|
+
var import_core14 = require("@loaders.gl/tile-converter/v5/core");
|
|
1383
1951
|
async function createTileConversionArchive(files, options) {
|
|
1384
1952
|
const { resources, format, maxArchiveBytes, signal } = prepareArchiveInput(files, options);
|
|
1385
1953
|
const writer = format === "3tz" ? import_d_tiles_archive_writer.Tiles3DArchiveWriter : import_i3s_slpk_writer.SLPKWriter;
|
|
@@ -1394,17 +1962,17 @@ async function* encodeTileConversionArchiveInBatches(files, options) {
|
|
|
1394
1962
|
function prepareArchiveInput(files, options) {
|
|
1395
1963
|
const { format, maxArchiveBytes, signal } = options;
|
|
1396
1964
|
if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
|
|
1397
|
-
throw new
|
|
1965
|
+
throw new import_core14.TileConversionError(
|
|
1398
1966
|
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
1399
1967
|
"Archive byte limit must be a non-negative safe integer"
|
|
1400
1968
|
);
|
|
1401
1969
|
}
|
|
1402
1970
|
if (format !== "3tz" && format !== "slpk") {
|
|
1403
|
-
throw new
|
|
1971
|
+
throw new import_core14.TileConversionError("INVALID_ARCHIVE_FORMAT", "Expected 3tz or slpk archive format");
|
|
1404
1972
|
}
|
|
1405
1973
|
const resources = Object.fromEntries(files.map((file) => [file.resourceId, file.blob]));
|
|
1406
1974
|
if (Object.keys(resources).length !== files.length) {
|
|
1407
|
-
throw new
|
|
1975
|
+
throw new import_core14.TileConversionError(
|
|
1408
1976
|
"DUPLICATE_ARCHIVE_RESOURCE",
|
|
1409
1977
|
"Archive resource identifiers must be unique"
|
|
1410
1978
|
);
|
|
@@ -1414,17 +1982,17 @@ function prepareArchiveInput(files, options) {
|
|
|
1414
1982
|
|
|
1415
1983
|
// src/v5/mesh-tileset-archive.ts
|
|
1416
1984
|
var import_d_tiles_archive_writer2 = require("@loaders.gl/3d-tiles/3d-tiles-archive-writer");
|
|
1417
|
-
var
|
|
1985
|
+
var import_core15 = require("@loaders.gl/tile-converter/v5/core");
|
|
1418
1986
|
async function createSingleMeshTilesetArchive(files, options) {
|
|
1419
1987
|
const { maxArchiveBytes, signal } = options;
|
|
1420
1988
|
if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
|
|
1421
|
-
throw new
|
|
1989
|
+
throw new import_core15.TileConversionError(
|
|
1422
1990
|
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
1423
1991
|
"Archive byte limit must be a non-negative safe integer"
|
|
1424
1992
|
);
|
|
1425
1993
|
}
|
|
1426
1994
|
if (files.length !== 2 || !files.some((file) => file.resourceId === "mesh.glb") || !files.some((file) => file.resourceId === "tileset.json")) {
|
|
1427
|
-
throw new
|
|
1995
|
+
throw new import_core15.TileConversionError(
|
|
1428
1996
|
"INVALID_SINGLE_MESH_ARCHIVE",
|
|
1429
1997
|
"Expected mesh.glb and tileset.json from a finalized single-mesh sink"
|
|
1430
1998
|
);
|
|
@@ -1435,12 +2003,13 @@ async function createSingleMeshTilesetArchive(files, options) {
|
|
|
1435
2003
|
}
|
|
1436
2004
|
|
|
1437
2005
|
// src/v5/mesh-source.ts
|
|
1438
|
-
var
|
|
1439
|
-
var
|
|
1440
|
-
var
|
|
2006
|
+
var import_core19 = require("@math.gl/core");
|
|
2007
|
+
var import_core20 = require("@loaders.gl/tile-converter/v5/core");
|
|
2008
|
+
var import_core21 = require("@loaders.gl/tile-converter/v5/core");
|
|
1441
2009
|
|
|
1442
2010
|
// src/v5/mesh-source-texture.ts
|
|
1443
|
-
var
|
|
2011
|
+
var import_images2 = require("@loaders.gl/images");
|
|
2012
|
+
var import_core16 = require("@loaders.gl/tile-converter/v5/core");
|
|
1444
2013
|
function mapMeshSourceTexture(textureInfo) {
|
|
1445
2014
|
const texture = textureInfo?.texture;
|
|
1446
2015
|
const image = texture?.source;
|
|
@@ -1467,7 +2036,7 @@ function mapMeshSourceTexture(textureInfo) {
|
|
|
1467
2036
|
"extras"
|
|
1468
2037
|
].includes(name)
|
|
1469
2038
|
))) {
|
|
1470
|
-
throw new
|
|
2039
|
+
throw new import_core16.TileConversionError(
|
|
1471
2040
|
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
1472
2041
|
"Base-color textures require a PNG/JPEG bufferView or base64 data URI, TEXCOORD_0, glTF sampling and an optional KHR_texture_transform on TEXCOORD_0"
|
|
1473
2042
|
);
|
|
@@ -1496,7 +2065,7 @@ function mapMeshSourceImage(image) {
|
|
|
1496
2065
|
}
|
|
1497
2066
|
const match = typeof image.uri === "string" ? /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/]+={0,2})$/.exec(image.uri) : null;
|
|
1498
2067
|
if (!match || image.mimeType !== void 0 && image.mimeType !== match[1]) {
|
|
1499
|
-
throw new
|
|
2068
|
+
throw new import_core16.TileConversionError(
|
|
1500
2069
|
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
1501
2070
|
"Inline images require a PNG/JPEG base64 data URI and matching optional mimeType"
|
|
1502
2071
|
);
|
|
@@ -1505,91 +2074,233 @@ function mapMeshSourceImage(image) {
|
|
|
1505
2074
|
try {
|
|
1506
2075
|
encodedBytes = atob(match[2]);
|
|
1507
2076
|
} catch {
|
|
1508
|
-
throw new
|
|
2077
|
+
throw new import_core16.TileConversionError("MESH_SOURCE_TEXTURE_UNSUPPORTED", "Invalid inline image base64");
|
|
1509
2078
|
}
|
|
1510
2079
|
return {
|
|
1511
2080
|
data: Uint8Array.from(encodedBytes, (character) => character.charCodeAt(0)),
|
|
1512
2081
|
mimeType: match[1]
|
|
1513
2082
|
};
|
|
1514
2083
|
}
|
|
2084
|
+
async function resolveMeshSourceTexture(textureInfo, readExternalResource, contentUri, signal) {
|
|
2085
|
+
const image = textureInfo?.texture?.source;
|
|
2086
|
+
if (!image || image.uri === void 0 || image.uri.startsWith("data:"))
|
|
2087
|
+
return mapMeshSourceTexture(textureInfo);
|
|
2088
|
+
if (!readExternalResource || image.bufferView !== void 0)
|
|
2089
|
+
throw new import_core16.TileConversionError(
|
|
2090
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
2091
|
+
"External images require an explicit bounded resource reader"
|
|
2092
|
+
);
|
|
2093
|
+
signal?.throwIfAborted();
|
|
2094
|
+
const data = await readExternalResource(image.uri, contentUri, signal);
|
|
2095
|
+
signal?.throwIfAborted();
|
|
2096
|
+
let metadata;
|
|
2097
|
+
try {
|
|
2098
|
+
metadata = (0, import_images2.getBinaryImageMetadata)(new DataView(data.buffer, data.byteOffset, data.byteLength));
|
|
2099
|
+
} catch {
|
|
2100
|
+
}
|
|
2101
|
+
if (!metadata || !["image/png", "image/jpeg"].includes(metadata.mimeType) || image.mimeType !== void 0 && image.mimeType !== metadata.mimeType)
|
|
2102
|
+
throw new import_core16.TileConversionError(
|
|
2103
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
2104
|
+
"External images require matching PNG/JPEG headers"
|
|
2105
|
+
);
|
|
2106
|
+
const resolved = {
|
|
2107
|
+
...textureInfo,
|
|
2108
|
+
texture: {
|
|
2109
|
+
...textureInfo.texture,
|
|
2110
|
+
source: { ...image, uri: void 0, mimeType: metadata.mimeType, bufferView: { data } }
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
return mapMeshSourceTexture(resolved);
|
|
2114
|
+
}
|
|
1515
2115
|
|
|
1516
|
-
// src/v5/mesh-source-
|
|
1517
|
-
var
|
|
1518
|
-
|
|
2116
|
+
// src/v5/mesh-source-schema.ts
|
|
2117
|
+
var import_core17 = require("@loaders.gl/tile-converter/v5/core");
|
|
2118
|
+
var SCALAR_TYPES = {
|
|
2119
|
+
INT8: "int8",
|
|
2120
|
+
UINT8: "uint8",
|
|
2121
|
+
INT16: "int16",
|
|
2122
|
+
UINT16: "uint16",
|
|
2123
|
+
INT32: "int32",
|
|
2124
|
+
UINT32: "uint32",
|
|
2125
|
+
INT64: "int64",
|
|
2126
|
+
UINT64: "uint64",
|
|
2127
|
+
FLOAT32: "float32",
|
|
2128
|
+
FLOAT64: "float64"
|
|
2129
|
+
};
|
|
2130
|
+
function createMeshSourceFeatureOptions(gltf, options) {
|
|
2131
|
+
const metadata = gltf.extensions?.EXT_structural_metadata;
|
|
2132
|
+
const table = metadata?.propertyTables?.[0];
|
|
2133
|
+
const properties = metadata?.schema?.classes?.[options.metadataClass]?.properties;
|
|
2134
|
+
if (!options.metadataClass || metadata?.propertyTables?.length !== 1 || table?.class !== options.metadataClass || !properties || !Number.isSafeInteger(table.count) || table.count < 1 || table.count > 4294967295 || metadata.propertyTextures?.length || metadata.propertyAttributes?.length) {
|
|
2135
|
+
throw new import_core17.TileConversionError(
|
|
2136
|
+
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
2137
|
+
"Schema mapping requires one inline class, one matching property table and a positive uint32 row count"
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
if (Object.keys(table.properties || {}).some((name) => !Object.hasOwn(properties, name))) {
|
|
2141
|
+
throw new import_core17.TileConversionError(
|
|
2142
|
+
"MESH_FEATURE_PROPERTY_UNMAPPED",
|
|
2143
|
+
"Property table columns must be declared in the selected class"
|
|
2144
|
+
);
|
|
2145
|
+
}
|
|
2146
|
+
const fields = Object.entries(properties).map(([name, property]) => {
|
|
2147
|
+
const declaration = property;
|
|
2148
|
+
const column = table.properties?.[name];
|
|
2149
|
+
if (declaration.array || !["SCALAR", "STRING"].includes(declaration.type) || declaration.normalized || declaration.offset !== void 0 || declaration.scale !== void 0 || column?.offset !== void 0 || column?.scale !== void 0 || declaration.required && (declaration.noData !== void 0 || declaration.default !== void 0)) {
|
|
2150
|
+
throw new import_core17.TileConversionError(
|
|
2151
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2152
|
+
`Property ${name} requires an explicit separately qualified mapping`
|
|
2153
|
+
);
|
|
2154
|
+
}
|
|
2155
|
+
const type = declaration.type === "STRING" ? "utf8" : Object.hasOwn(SCALAR_TYPES, declaration.componentType || "") ? SCALAR_TYPES[declaration.componentType] : void 0;
|
|
2156
|
+
if (!type) {
|
|
2157
|
+
throw new import_core17.TileConversionError(
|
|
2158
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2159
|
+
`Property ${name} has no supported declared scalar width`
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
return {
|
|
2163
|
+
name,
|
|
2164
|
+
type,
|
|
2165
|
+
nullable: declaration.required !== true,
|
|
2166
|
+
metadata: { "loaders.gl:structural-property": JSON.stringify(declaration) }
|
|
2167
|
+
};
|
|
2168
|
+
});
|
|
2169
|
+
const identifierName = options.featureIdField || "feature_id";
|
|
2170
|
+
const sourceIdentifier = options.sourceFeatureIdProperty;
|
|
2171
|
+
const sourceField = sourceIdentifier ? fields.find((field) => field.name === sourceIdentifier) : void 0;
|
|
2172
|
+
if (sourceIdentifier && !sourceField || fields.some((field) => field.name === identifierName && field !== sourceField) || sourceField && !["utf8", "int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64"].includes(
|
|
2173
|
+
String(sourceField.type)
|
|
2174
|
+
)) {
|
|
2175
|
+
throw new import_core17.TileConversionError(
|
|
2176
|
+
"MESH_FEATURE_ID_PROPERTY_REQUIRED",
|
|
2177
|
+
"Select a declared string/integer identifier and a distinct output identifier name"
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
return {
|
|
2181
|
+
...options,
|
|
2182
|
+
schema: {
|
|
2183
|
+
fields: [
|
|
2184
|
+
{
|
|
2185
|
+
...sourceField,
|
|
2186
|
+
name: identifierName,
|
|
2187
|
+
type: sourceField?.type || "uint32",
|
|
2188
|
+
nullable: false
|
|
2189
|
+
},
|
|
2190
|
+
...fields.filter((field) => field !== sourceField)
|
|
2191
|
+
],
|
|
2192
|
+
metadata: { "loaders.gl:source-schema": JSON.stringify(metadata.schema) }
|
|
2193
|
+
}
|
|
2194
|
+
};
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
// src/v5/mesh-source-features.ts
|
|
2198
|
+
var import_core18 = require("@loaders.gl/tile-converter/v5/core");
|
|
2199
|
+
function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
1519
2200
|
const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
|
|
1520
2201
|
const metadata = gltf.extensions?.EXT_structural_metadata;
|
|
1521
2202
|
const legacy = primitive.attributes._BATCHID;
|
|
1522
2203
|
if (!declarations && !legacy && !metadata && !Object.keys(payload.batchTableJson || {}).length && !payload.header?.batchLength) {
|
|
1523
2204
|
if (Object.keys(primitive.attributes).some((name) => /^_FEATURE_ID_\d+$/.test(name)))
|
|
1524
|
-
throw new
|
|
2205
|
+
throw new import_core18.TileConversionError(
|
|
1525
2206
|
"MESH_FEATURE_MAPPING_REQUIRED",
|
|
1526
2207
|
"Feature attributes require an EXT_mesh_features declaration"
|
|
1527
2208
|
);
|
|
1528
2209
|
return void 0;
|
|
1529
2210
|
}
|
|
1530
2211
|
if (!options)
|
|
1531
|
-
throw new
|
|
2212
|
+
throw new import_core18.TileConversionError(
|
|
1532
2213
|
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
1533
|
-
"Feature extraction requires
|
|
2214
|
+
"Feature extraction requires a class selection and a declared Arrow or inline source schema"
|
|
1534
2215
|
);
|
|
1535
2216
|
if (!options.metadataClass)
|
|
1536
|
-
throw new
|
|
2217
|
+
throw new import_core18.TileConversionError(
|
|
1537
2218
|
"MESH_FEATURE_CLASS_REQUIRED",
|
|
1538
2219
|
"Feature metadata class must be explicit"
|
|
1539
2220
|
);
|
|
2221
|
+
const mapping = "schema" in options ? options : createMeshSourceFeatureOptions(gltf, options);
|
|
1540
2222
|
let count;
|
|
1541
2223
|
let attribute = legacy;
|
|
1542
2224
|
let properties;
|
|
1543
2225
|
if (declarations) {
|
|
1544
2226
|
if (legacy || declarations.length !== 1 || metadata?.propertyTables?.length !== 1 || metadata.propertyTextures?.length || metadata.propertyAttributes?.length)
|
|
1545
|
-
throw new
|
|
2227
|
+
throw new import_core18.TileConversionError(
|
|
1546
2228
|
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1547
2229
|
"Only one attribute-backed feature set and one property table are supported"
|
|
1548
2230
|
);
|
|
1549
2231
|
const declaration = declarations[0];
|
|
1550
2232
|
if (!Number.isInteger(declaration.attribute) || declaration.attribute < 0 || declaration.propertyTable !== 0 || declaration.texture || declaration.implicit || declaration.nullFeatureId !== void 0)
|
|
1551
|
-
throw new
|
|
2233
|
+
throw new import_core18.TileConversionError(
|
|
1552
2234
|
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1553
2235
|
"Features require an attribute and property table 0 without null/texture/implicit IDs"
|
|
1554
2236
|
);
|
|
1555
2237
|
attribute = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
|
|
1556
2238
|
const table = metadata.propertyTables[0];
|
|
1557
2239
|
if (table.class !== options.metadataClass || table.count !== declaration.featureCount)
|
|
1558
|
-
throw new
|
|
2240
|
+
throw new import_core18.TileConversionError(
|
|
1559
2241
|
"MESH_FEATURE_CLASS_MISMATCH",
|
|
1560
2242
|
"Feature class and declared table counts must agree"
|
|
1561
2243
|
);
|
|
1562
2244
|
count = table.count;
|
|
2245
|
+
if (!Number.isSafeInteger(count) || count <= 0 || count > mesh.attributes.POSITION.value.length / 3) {
|
|
2246
|
+
throw new import_core18.TileConversionError(
|
|
2247
|
+
"MESH_FEATURE_IDS_INVALID",
|
|
2248
|
+
"Feature row count exceeds the available mesh vertices"
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
1563
2251
|
properties = {};
|
|
1564
2252
|
const classProperties = metadata.schema?.classes?.[table.class]?.properties;
|
|
1565
2253
|
if (!classProperties)
|
|
1566
|
-
throw new
|
|
2254
|
+
throw new import_core18.TileConversionError(
|
|
1567
2255
|
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
1568
2256
|
"A decoded inline structural metadata class is required"
|
|
1569
2257
|
);
|
|
1570
2258
|
if (Object.keys(table.properties || {}).some((name) => !Object.hasOwn(classProperties, name)))
|
|
1571
|
-
throw new
|
|
2259
|
+
throw new import_core18.TileConversionError(
|
|
1572
2260
|
"MESH_FEATURE_PROPERTY_UNMAPPED",
|
|
1573
2261
|
"Property table columns must be declared in the metadata class"
|
|
1574
2262
|
);
|
|
1575
2263
|
for (const [name, property] of Object.entries(classProperties)) {
|
|
1576
2264
|
const column = table.properties?.[name];
|
|
1577
|
-
if (!["SCALAR", "STRING"].includes(property.type) || property.array || property.noData !== void 0 || ["INT64", "UINT64"].includes(property.componentType || "") && (property.normalized || property.offset !== void 0 || property.scale !== void 0 || column?.offset !== void 0 || column?.scale !== void 0))
|
|
1578
|
-
throw new
|
|
2265
|
+
if (!["SCALAR", "STRING"].includes(property.type) || property.array || property.noData !== void 0 && (property.normalized || property.offset !== void 0 || property.scale !== void 0 || column?.offset !== void 0 || column?.scale !== void 0) || ["INT64", "UINT64"].includes(property.componentType || "") && (property.normalized || property.offset !== void 0 || property.scale !== void 0 || column?.offset !== void 0 || column?.scale !== void 0))
|
|
2266
|
+
throw new import_core18.TileConversionError(
|
|
1579
2267
|
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
1580
2268
|
`Property ${name} requires a separately qualified semantic mapping`
|
|
1581
2269
|
);
|
|
1582
2270
|
const data = column?.data;
|
|
1583
|
-
if (data === void 0 && property.
|
|
1584
|
-
throw new
|
|
2271
|
+
if (column !== void 0 && (data === void 0 || data === null) || data === void 0 && property.required === true)
|
|
2272
|
+
throw new import_core18.TileConversionError(
|
|
1585
2273
|
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
1586
2274
|
`Property ${name} has not been decoded`
|
|
1587
2275
|
);
|
|
1588
|
-
|
|
2276
|
+
const integer64 = ["INT64", "UINT64"].includes(property.componentType || "");
|
|
2277
|
+
if (integer64 && property.default !== void 0 && (typeof property.default !== "number" || !Number.isSafeInteger(property.default))) {
|
|
2278
|
+
throw new import_core18.TileConversionError(
|
|
2279
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2280
|
+
`Property ${name} requires an exact 64-bit default`
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
const defaultValue = integer64 && property.default !== void 0 ? BigInt(property.default) : property.default ?? null;
|
|
2284
|
+
const decoded = data ?? Array.from({ length: count }, () => defaultValue);
|
|
2285
|
+
if (property.noData === void 0 || data === void 0) {
|
|
2286
|
+
properties[name] = decoded;
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
const sentinel = property.noData;
|
|
2290
|
+
if ((property.type === "STRING" ? typeof sentinel !== "string" : typeof sentinel !== "number" || !Number.isFinite(sentinel) || integer64 && !Number.isSafeInteger(sentinel)) || !decoded || !Array.isArray(decoded) && !ArrayBuffer.isView(decoded) || decoded.length !== count)
|
|
2291
|
+
throw new import_core18.TileConversionError(
|
|
2292
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2293
|
+
`Property ${name} requires a valid decoded noData mapping`
|
|
2294
|
+
);
|
|
2295
|
+
const comparable = integer64 ? BigInt(sentinel) : sentinel;
|
|
2296
|
+
properties[name] = Array.from(
|
|
2297
|
+
decoded,
|
|
2298
|
+
(value) => value === comparable ? defaultValue : value
|
|
2299
|
+
);
|
|
1589
2300
|
}
|
|
1590
2301
|
} else {
|
|
1591
2302
|
if (metadata)
|
|
1592
|
-
throw new
|
|
2303
|
+
throw new import_core18.TileConversionError(
|
|
1593
2304
|
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1594
2305
|
"Structural metadata requires an attribute-backed feature declaration"
|
|
1595
2306
|
);
|
|
@@ -1600,7 +2311,7 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
|
1600
2311
|
if (!Number.isSafeInteger(count) || count <= 0 || !attribute || attribute.components !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(attribute.value).some(
|
|
1601
2312
|
(value) => !Number.isSafeInteger(value) || value < 0 || value >= count
|
|
1602
2313
|
))
|
|
1603
|
-
throw new
|
|
2314
|
+
throw new import_core18.TileConversionError(
|
|
1604
2315
|
"MESH_FEATURE_IDS_INVALID",
|
|
1605
2316
|
"Feature IDs must be one valid property-table row index per vertex"
|
|
1606
2317
|
);
|
|
@@ -1608,20 +2319,20 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
|
1608
2319
|
if (Object.keys(primitive.attributes).some(
|
|
1609
2320
|
(name) => (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) && name !== selectedAttribute
|
|
1610
2321
|
))
|
|
1611
|
-
throw new
|
|
2322
|
+
throw new import_core18.TileConversionError(
|
|
1612
2323
|
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1613
2324
|
"Additional feature ID sets require separate mappings"
|
|
1614
2325
|
);
|
|
1615
2326
|
for (const [name, values] of Object.entries(properties)) {
|
|
1616
2327
|
if (!values || !Array.isArray(values) && !ArrayBuffer.isView(values) || values.length !== count)
|
|
1617
|
-
throw new
|
|
2328
|
+
throw new import_core18.TileConversionError(
|
|
1618
2329
|
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
1619
2330
|
`Property ${name} must be a decoded column matching the table count`
|
|
1620
2331
|
);
|
|
1621
2332
|
}
|
|
1622
2333
|
const sourceIdentifier = options.sourceFeatureIdProperty;
|
|
1623
2334
|
if (sourceIdentifier && !properties[sourceIdentifier])
|
|
1624
|
-
throw new
|
|
2335
|
+
throw new import_core18.TileConversionError(
|
|
1625
2336
|
"MESH_FEATURE_ID_PROPERTY_REQUIRED",
|
|
1626
2337
|
"Stable source identifier property is missing"
|
|
1627
2338
|
);
|
|
@@ -1633,18 +2344,19 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
|
1633
2344
|
Object.entries(properties).filter(([name]) => name !== sourceIdentifier).map(([name, values]) => [name, values[index]])
|
|
1634
2345
|
)
|
|
1635
2346
|
})),
|
|
1636
|
-
|
|
2347
|
+
mapping
|
|
1637
2348
|
);
|
|
1638
2349
|
const indices = mesh.indices?.value;
|
|
1639
2350
|
const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
|
|
1640
2351
|
for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
|
|
1641
|
-
const
|
|
1642
|
-
(corner) =>
|
|
2352
|
+
const corners = [0, 1, 2].map(
|
|
2353
|
+
(corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
|
|
1643
2354
|
);
|
|
1644
|
-
|
|
1645
|
-
|
|
2355
|
+
const rows = corners.map((corner) => attribute.value[corner]);
|
|
2356
|
+
if (rows.some((row) => row !== rows[0]) && new Set(corners).size === 3)
|
|
2357
|
+
throw new import_core18.TileConversionError(
|
|
1646
2358
|
"MESH_FEATURE_TRIANGLE_MIXED",
|
|
1647
|
-
"All vertices of a triangle must reference the same feature row"
|
|
2359
|
+
"All vertices of a nondegenerate triangle must reference the same feature row"
|
|
1648
2360
|
);
|
|
1649
2361
|
triangleFeatureIndices[triangle] = Number(rows[0]);
|
|
1650
2362
|
}
|
|
@@ -1658,20 +2370,20 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
|
1658
2370
|
|
|
1659
2371
|
// src/v5/mesh-source.ts
|
|
1660
2372
|
function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
1661
|
-
const source = (0,
|
|
2373
|
+
const source = (0, import_core21.createTilesetConversionSource)(tileset, options);
|
|
1662
2374
|
return {
|
|
1663
2375
|
/** Validates the source frame and supported dataset metadata before traversal. */
|
|
1664
2376
|
async inspect(signal) {
|
|
1665
2377
|
const metadata = await source.inspect(signal);
|
|
1666
2378
|
const reference = metadata.spatialReference;
|
|
1667
2379
|
if (metadata.type !== "TILES3D" || !reference || reference.status !== "native" || reference.sourceCrs !== "EPSG:4978" || reference.heightReference !== "ellipsoidal" || reference.axisOrder !== "xyz" || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter")) {
|
|
1668
|
-
throw new
|
|
2380
|
+
throw new import_core20.TileConversionError(
|
|
1669
2381
|
"MESH_SOURCE_FRAME_UNSUPPORTED",
|
|
1670
2382
|
"Mesh extraction requires native EPSG:4978 xyz coordinates in meters and ellipsoidal heights"
|
|
1671
2383
|
);
|
|
1672
2384
|
}
|
|
1673
2385
|
if (metadata.tileset.metadata || metadata.tileset.groups?.length) {
|
|
1674
|
-
throw new
|
|
2386
|
+
throw new import_core20.TileConversionError(
|
|
1675
2387
|
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1676
2388
|
"Tileset/group metadata requires a separate mapping"
|
|
1677
2389
|
);
|
|
@@ -1683,25 +2395,25 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1683
2395
|
for await (const item of source.read(inspection, signal)) {
|
|
1684
2396
|
const placementReference = item.tile.header._spatialReference || tileset.spatialReference;
|
|
1685
2397
|
if (placementReference && (placementReference.status !== "native" || placementReference.sourceCrs !== "EPSG:4978"))
|
|
1686
|
-
throw new
|
|
2398
|
+
throw new import_core20.TileConversionError(
|
|
1687
2399
|
"MESH_SOURCE_FRAME_UNSUPPORTED",
|
|
1688
2400
|
"Nested mesh placements require native ECEF coordinates"
|
|
1689
2401
|
);
|
|
1690
2402
|
if (item.tile.header.metadata || item.tile.header.implicitMetadata)
|
|
1691
|
-
throw new
|
|
2403
|
+
throw new import_core20.TileConversionError(
|
|
1692
2404
|
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1693
2405
|
"Tile metadata requires a separate mapping"
|
|
1694
2406
|
);
|
|
1695
2407
|
for (const content of item.contents) {
|
|
1696
2408
|
signal?.throwIfAborted();
|
|
1697
2409
|
if (content.metadata || content.group !== void 0)
|
|
1698
|
-
throw new
|
|
2410
|
+
throw new import_core20.TileConversionError(
|
|
1699
2411
|
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1700
2412
|
"Content metadata requires a separate mapping"
|
|
1701
2413
|
);
|
|
1702
2414
|
const payload = content.payload;
|
|
1703
2415
|
if (!payload?.gltf || !["glTF", "gltf", "b3dm"].includes(payload.type || content.type || "")) {
|
|
1704
|
-
throw new
|
|
2416
|
+
throw new import_core20.TileConversionError(
|
|
1705
2417
|
"MESH_SOURCE_CONTENT_UNSUPPORTED",
|
|
1706
2418
|
"Mesh extraction requires decoded GLB/glTF or B3DM content"
|
|
1707
2419
|
);
|
|
@@ -1710,40 +2422,47 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1710
2422
|
if (gltf.animations?.length || gltf.skins?.length || Object.keys(gltf.extensions || {}).some(
|
|
1711
2423
|
(name) => !["EXT_structural_metadata", "CESIUM_RTC"].includes(name)
|
|
1712
2424
|
)) {
|
|
1713
|
-
throw new
|
|
2425
|
+
throw new import_core20.TileConversionError(
|
|
1714
2426
|
"MESH_SOURCE_SCENE_UNSUPPORTED",
|
|
1715
2427
|
"Animated/skinned scenes and unmapped scene extensions are unsupported"
|
|
1716
2428
|
);
|
|
1717
2429
|
}
|
|
1718
2430
|
const axis = payload.gltfUpAxis || "Y";
|
|
1719
2431
|
if (!["X", "Y", "Z"].includes(axis))
|
|
1720
|
-
throw new
|
|
1721
|
-
const placement = new
|
|
2432
|
+
throw new import_core20.TileConversionError("MESH_SOURCE_AXIS_UNSUPPORTED", "Unknown glTF up axis");
|
|
2433
|
+
const placement = new import_core19.Matrix4(item.tile.computedTransform);
|
|
1722
2434
|
const rtc = payload.rtcCenter || gltf.extensions?.CESIUM_RTC?.center;
|
|
1723
2435
|
if (rtc) {
|
|
1724
2436
|
if (rtc.length !== 3 || rtc.some((value) => !Number.isFinite(value)))
|
|
1725
|
-
throw new
|
|
2437
|
+
throw new import_core20.TileConversionError(
|
|
1726
2438
|
"MESH_SOURCE_TRANSFORM_INVALID",
|
|
1727
2439
|
"RTC center must contain finite xyz coordinates"
|
|
1728
2440
|
);
|
|
1729
2441
|
placement.translate(rtc);
|
|
1730
2442
|
}
|
|
1731
2443
|
if (axis === "Y")
|
|
1732
|
-
placement.multiplyRight(new
|
|
2444
|
+
placement.multiplyRight(new import_core19.Matrix4([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]));
|
|
1733
2445
|
if (axis === "X")
|
|
1734
|
-
placement.multiplyRight(new
|
|
2446
|
+
placement.multiplyRight(new import_core19.Matrix4([0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]));
|
|
1735
2447
|
const roots = gltf.scene?.nodes || (gltf.scenes?.length === 1 ? gltf.scenes[0].nodes : void 0);
|
|
1736
2448
|
if (!roots?.length)
|
|
1737
|
-
throw new
|
|
2449
|
+
throw new import_core20.TileConversionError(
|
|
1738
2450
|
"MESH_SOURCE_SCENE_UNSUPPORTED",
|
|
1739
2451
|
"A default scene or exactly one scene is required"
|
|
1740
2452
|
);
|
|
1741
2453
|
let primitiveIndex = 0;
|
|
1742
|
-
|
|
1743
|
-
|
|
2454
|
+
const primitives = traverseMeshNodes(roots, placement);
|
|
2455
|
+
let current = primitives.next();
|
|
2456
|
+
while (!current.done) {
|
|
2457
|
+
const selected = current.value;
|
|
2458
|
+
const next = primitives.next();
|
|
2459
|
+
const resource = await extractPrimitive(
|
|
1744
2460
|
selected.primitive,
|
|
1745
2461
|
selected.transform,
|
|
1746
|
-
`${item.tile.id}/${content.index}/${primitiveIndex++}
|
|
2462
|
+
`${item.tile.id}/${content.index}/${primitiveIndex++}`,
|
|
2463
|
+
options.readExternalResource,
|
|
2464
|
+
content.uri,
|
|
2465
|
+
signal
|
|
1747
2466
|
);
|
|
1748
2467
|
const features = extractMeshFeatures(
|
|
1749
2468
|
gltf,
|
|
@@ -1752,10 +2471,15 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1752
2471
|
resource.mesh,
|
|
1753
2472
|
options.features
|
|
1754
2473
|
);
|
|
1755
|
-
yield {
|
|
2474
|
+
yield {
|
|
2475
|
+
...resource,
|
|
2476
|
+
features,
|
|
2477
|
+
contentPrimitive: { index: primitiveIndex - 1, last: Boolean(next.done) }
|
|
2478
|
+
};
|
|
2479
|
+
current = next;
|
|
1756
2480
|
}
|
|
1757
2481
|
if (!primitiveIndex)
|
|
1758
|
-
throw new
|
|
2482
|
+
throw new import_core20.TileConversionError(
|
|
1759
2483
|
"MESH_SOURCE_CONTENT_EMPTY",
|
|
1760
2484
|
"Declared content contains no supported mesh primitive"
|
|
1761
2485
|
);
|
|
@@ -1767,16 +2491,16 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1767
2491
|
function* traverseMeshNodes(nodes, parent, ancestors = /* @__PURE__ */ new Set()) {
|
|
1768
2492
|
for (const node of nodes) {
|
|
1769
2493
|
if (ancestors.has(node) || node.skin || node.weights || Object.keys(node.extensions || {}).length)
|
|
1770
|
-
throw new
|
|
2494
|
+
throw new import_core20.TileConversionError(
|
|
1771
2495
|
"MESH_SOURCE_NODE_UNSUPPORTED",
|
|
1772
2496
|
"Cyclic, skinned, morphed, or extended nodes are unsupported"
|
|
1773
2497
|
);
|
|
1774
|
-
const transform = new
|
|
1775
|
-
node.matrix ? new
|
|
2498
|
+
const transform = new import_core19.Matrix4(parent).multiplyRight(
|
|
2499
|
+
node.matrix ? new import_core19.Matrix4(node.matrix) : new import_core19.Matrix4().translate(node.translation || [0, 0, 0]).multiplyRight(new import_core19.Matrix4().fromQuaternion(node.rotation || [0, 0, 0, 1])).scale(node.scale || [1, 1, 1])
|
|
1776
2500
|
);
|
|
1777
2501
|
if (node.mesh) {
|
|
1778
2502
|
if (node.mesh.weights || Object.keys(node.mesh.extensions || {}).length)
|
|
1779
|
-
throw new
|
|
2503
|
+
throw new import_core20.TileConversionError(
|
|
1780
2504
|
"MESH_SOURCE_NODE_UNSUPPORTED",
|
|
1781
2505
|
"Morph weights and mesh extensions are unsupported"
|
|
1782
2506
|
);
|
|
@@ -1785,14 +2509,14 @@ function* traverseMeshNodes(nodes, parent, ancestors = /* @__PURE__ */ new Set()
|
|
|
1785
2509
|
yield* traverseMeshNodes(node.children || [], transform, /* @__PURE__ */ new Set([...ancestors, node]));
|
|
1786
2510
|
}
|
|
1787
2511
|
}
|
|
1788
|
-
function extractPrimitive(primitive, transform, id) {
|
|
2512
|
+
async function extractPrimitive(primitive, transform, id, readExternalResource, contentUri, signal) {
|
|
1789
2513
|
if (primitive.targets?.length || Object.keys(primitive.extensions || {}).some((name) => name !== "EXT_mesh_features"))
|
|
1790
|
-
throw new
|
|
2514
|
+
throw new import_core20.TileConversionError(
|
|
1791
2515
|
"MESH_SOURCE_PRIMITIVE_UNSUPPORTED",
|
|
1792
2516
|
"Morph targets and unmapped primitive extensions are unsupported"
|
|
1793
2517
|
);
|
|
1794
2518
|
if (Array.from(transform).some((value) => !Number.isFinite(value)) || transform[3] !== 0 || transform[7] !== 0 || transform[11] !== 0 || transform[15] !== 1 || transform.determinant() <= 0)
|
|
1795
|
-
throw new
|
|
2519
|
+
throw new import_core20.TileConversionError(
|
|
1796
2520
|
"MESH_SOURCE_TRANSFORM_INVALID",
|
|
1797
2521
|
"Mesh placement requires a finite invertible affine transform with positive determinant"
|
|
1798
2522
|
);
|
|
@@ -1800,7 +2524,7 @@ function extractPrimitive(primitive, transform, id) {
|
|
|
1800
2524
|
for (const [name, accessor] of Object.entries(primitive.attributes)) {
|
|
1801
2525
|
if (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) continue;
|
|
1802
2526
|
if (!["POSITION", "NORMAL", "COLOR_0", "TEXCOORD_0"].includes(name))
|
|
1803
|
-
throw new
|
|
2527
|
+
throw new import_core20.TileConversionError(
|
|
1804
2528
|
"MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
1805
2529
|
`Unsupported mesh attribute ${name}`
|
|
1806
2530
|
);
|
|
@@ -1813,9 +2537,9 @@ function extractPrimitive(primitive, transform, id) {
|
|
|
1813
2537
|
const mesh = validateMeshGeometry(
|
|
1814
2538
|
{
|
|
1815
2539
|
topology: "triangle-list",
|
|
1816
|
-
mode: primitive.mode ?? 4,
|
|
2540
|
+
mode: primitive.mode === 5 ? 4 : primitive.mode ?? 4,
|
|
1817
2541
|
attributes,
|
|
1818
|
-
indices: primitive.
|
|
2542
|
+
indices: createTriangleIndices(primitive, (attributes.POSITION?.value.length ?? 0) / 3)
|
|
1819
2543
|
},
|
|
1820
2544
|
true
|
|
1821
2545
|
);
|
|
@@ -1836,7 +2560,7 @@ function extractPrimitive(primitive, transform, id) {
|
|
|
1836
2560
|
mesh.attributes.POSITION = { value: positions, size: 3 };
|
|
1837
2561
|
if (mesh.attributes.NORMAL) {
|
|
1838
2562
|
const values = mesh.attributes.NORMAL.value;
|
|
1839
|
-
const normalMatrix = new
|
|
2563
|
+
const normalMatrix = new import_core19.Matrix4(transform).invert().transpose();
|
|
1840
2564
|
const normals = new Float32Array(values.length);
|
|
1841
2565
|
for (let index = 0; index < values.length; index += 3) {
|
|
1842
2566
|
const normal = normalMatrix.transformAsVector(Array.from(values.subarray(index, index + 3)));
|
|
@@ -1852,14 +2576,19 @@ function extractPrimitive(primitive, transform, id) {
|
|
|
1852
2576
|
id,
|
|
1853
2577
|
mesh,
|
|
1854
2578
|
origin: minimum.map((value, axis) => value / 2 + maximum[axis] / 2),
|
|
1855
|
-
material:
|
|
2579
|
+
material: await mapMeshSourceMaterial(
|
|
2580
|
+
primitive.material,
|
|
2581
|
+
readExternalResource,
|
|
2582
|
+
contentUri,
|
|
2583
|
+
signal
|
|
2584
|
+
)
|
|
1856
2585
|
};
|
|
1857
2586
|
}
|
|
1858
|
-
function
|
|
2587
|
+
async function mapMeshSourceMaterial(material, readExternalResource, contentUri, signal) {
|
|
1859
2588
|
if (!material) return void 0;
|
|
1860
2589
|
const pbr = material.pbrMetallicRoughness;
|
|
1861
|
-
if (Object.
|
|
1862
|
-
(name) => ![
|
|
2590
|
+
if (Object.entries(material).some(
|
|
2591
|
+
([name, value]) => value !== void 0 && ![
|
|
1863
2592
|
"id",
|
|
1864
2593
|
"name",
|
|
1865
2594
|
"pbrMetallicRoughness",
|
|
@@ -1869,16 +2598,23 @@ function mapMaterial(material) {
|
|
|
1869
2598
|
"emissiveFactor",
|
|
1870
2599
|
"extras"
|
|
1871
2600
|
].includes(name)
|
|
1872
|
-
) || Object.
|
|
1873
|
-
(name) => !["baseColorFactor", "baseColorTexture", "metallicFactor", "roughnessFactor"].includes(name)
|
|
2601
|
+
) || Object.entries(pbr || {}).some(
|
|
2602
|
+
([name, value]) => value !== void 0 && !["baseColorFactor", "baseColorTexture", "metallicFactor", "roughnessFactor"].includes(name)
|
|
1874
2603
|
) || material.emissiveFactor?.some((value) => value !== 0))
|
|
1875
|
-
throw new
|
|
2604
|
+
throw new import_core20.TileConversionError(
|
|
1876
2605
|
"MESH_SOURCE_MATERIAL_UNSUPPORTED",
|
|
1877
2606
|
"Only metallic-roughness factors and one embedded base-color image are supported"
|
|
1878
2607
|
);
|
|
1879
2608
|
return {
|
|
1880
2609
|
baseColorFactor: pbr?.baseColorFactor,
|
|
1881
|
-
..."baseColorTexture" in (pbr || {}) ? {
|
|
2610
|
+
..."baseColorTexture" in (pbr || {}) ? {
|
|
2611
|
+
baseColorTexture: await resolveMeshSourceTexture(
|
|
2612
|
+
pbr.baseColorTexture,
|
|
2613
|
+
readExternalResource,
|
|
2614
|
+
contentUri,
|
|
2615
|
+
signal
|
|
2616
|
+
)
|
|
2617
|
+
} : {},
|
|
1882
2618
|
metallicFactor: pbr?.metallicFactor,
|
|
1883
2619
|
roughnessFactor: pbr?.roughnessFactor,
|
|
1884
2620
|
alphaMode: material.alphaMode,
|
|
@@ -1886,53 +2622,107 @@ function mapMaterial(material) {
|
|
|
1886
2622
|
doubleSided: material.doubleSided
|
|
1887
2623
|
};
|
|
1888
2624
|
}
|
|
2625
|
+
function createTriangleIndices(primitive, vertexCount) {
|
|
2626
|
+
const attribute = primitive.indices ? {
|
|
2627
|
+
value: primitive.indices.value,
|
|
2628
|
+
size: primitive.indices.components,
|
|
2629
|
+
normalized: primitive.indices.normalized
|
|
2630
|
+
} : void 0;
|
|
2631
|
+
if (primitive.mode !== 5) return attribute;
|
|
2632
|
+
const sourceIndices = validateMeshIndices(attribute, vertexCount, 5);
|
|
2633
|
+
const indexCount = sourceIndices?.length ?? vertexCount;
|
|
2634
|
+
if (!Number.isSafeInteger(indexCount) || indexCount < 3) {
|
|
2635
|
+
throw new import_core20.TileConversionError(
|
|
2636
|
+
"MESH_TRIANGLE_COUNT_INVALID",
|
|
2637
|
+
"Triangle strips require at least three vertices"
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
const IndexArray = vertexCount < 65536 ? Uint16Array : Uint32Array;
|
|
2641
|
+
const indices = new IndexArray((indexCount - 2) * 3);
|
|
2642
|
+
for (let triangle = 0; triangle < indexCount - 2; triangle++) {
|
|
2643
|
+
const second = triangle + 1 + triangle % 2;
|
|
2644
|
+
const third = triangle + 2 - triangle % 2;
|
|
2645
|
+
indices[triangle * 3] = sourceIndices?.[triangle] ?? triangle;
|
|
2646
|
+
indices[triangle * 3 + 1] = sourceIndices?.[second] ?? second;
|
|
2647
|
+
indices[triangle * 3 + 2] = sourceIndices?.[third] ?? third;
|
|
2648
|
+
}
|
|
2649
|
+
return { value: indices, size: 1 };
|
|
2650
|
+
}
|
|
1889
2651
|
|
|
1890
2652
|
// src/v5/i3s-mesh-conversion.ts
|
|
1891
2653
|
var import_i3s = require("@loaders.gl/i3s");
|
|
1892
|
-
var
|
|
1893
|
-
var
|
|
2654
|
+
var import_core22 = require("@loaders.gl/tile-converter/v5/core");
|
|
2655
|
+
var import_core23 = require("@loaders.gl/tile-converter/v5/core");
|
|
1894
2656
|
function createI3SMeshConversionCodec(options) {
|
|
2657
|
+
if (options.integratedMeshPolicy !== void 0 && options.integratedMeshPolicy !== "as-3d-object") {
|
|
2658
|
+
throw new import_core22.TileConversionError(
|
|
2659
|
+
"I3S_LAYER_TYPE_POLICY_INVALID",
|
|
2660
|
+
"Unknown IntegratedMesh conversion policy"
|
|
2661
|
+
);
|
|
2662
|
+
}
|
|
1895
2663
|
const reference = options.spatialContext.spatialReference;
|
|
1896
2664
|
const height = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
|
|
1897
2665
|
if (!["native", "transformed"].includes(reference.status) || (reference.targetCrs || reference.sourceCrs) !== "EPSG:4978" || height !== "ellipsoidal" || reference.status === "native" && (!["xyz", "unknown"].includes(reference.axisOrder) || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter")))
|
|
1898
|
-
throw new
|
|
2666
|
+
throw new import_core22.TileConversionError(
|
|
1899
2667
|
"I3S_MESH_FRAME_UNSUPPORTED",
|
|
1900
2668
|
"I3S mesh authoring requires resolved ECEF coordinates and ellipsoidal heights"
|
|
1901
2669
|
);
|
|
2670
|
+
let objectIdOffset = 0;
|
|
1902
2671
|
return {
|
|
1903
2672
|
/** Places a source mesh in ECEF and authors one layer without modifying source arrays. */
|
|
1904
2673
|
async *convert(resource, _inspection, signal) {
|
|
1905
2674
|
signal?.throwIfAborted();
|
|
2675
|
+
if (resource.sourceLayerType === "IntegratedMesh" && options.integratedMeshPolicy !== "as-3d-object") {
|
|
2676
|
+
throw new import_core22.TileConversionError(
|
|
2677
|
+
"I3S_LAYER_TYPE_CHANGE_REQUIRED",
|
|
2678
|
+
"IntegratedMesh to 3DObject conversion requires integratedMeshPolicy=as-3d-object"
|
|
2679
|
+
);
|
|
2680
|
+
}
|
|
1906
2681
|
const mesh = validateMeshGeometry(resource.mesh, true);
|
|
1907
2682
|
const positions = mesh.attributes.POSITION.value;
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
2683
|
+
const context = options.spatialContext;
|
|
2684
|
+
const transformed = "transformGeometryAsync" in context ? await context.transformGeometryAsync(
|
|
2685
|
+
positions,
|
|
2686
|
+
mesh.attributes.NORMAL?.value,
|
|
2687
|
+
resource.normalReferenceFrame ?? options.normalReferenceFrame
|
|
2688
|
+
) : {
|
|
2689
|
+
positions: context.transformPositions(positions),
|
|
2690
|
+
normals: mesh.attributes.NORMAL ? context.transformNormals(mesh.attributes.NORMAL.value, positions) : void 0
|
|
1911
2691
|
};
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
size: 3
|
|
1916
|
-
};
|
|
2692
|
+
signal?.throwIfAborted();
|
|
2693
|
+
mesh.attributes.POSITION = { value: transformed.positions, size: 3 };
|
|
2694
|
+
if (mesh.attributes.NORMAL) mesh.attributes.NORMAL = { value: transformed.normals, size: 3 };
|
|
1917
2695
|
let layer;
|
|
1918
2696
|
try {
|
|
1919
|
-
|
|
2697
|
+
await (0, import_i3s.preloadI3SMeshCompression)();
|
|
2698
|
+
const writerOptions = {
|
|
1920
2699
|
...options,
|
|
1921
2700
|
material: resource.material,
|
|
1922
|
-
features: resource.features
|
|
1923
|
-
|
|
2701
|
+
features: resource.features,
|
|
2702
|
+
objectIdOffset
|
|
2703
|
+
};
|
|
2704
|
+
layer = options.draco === false ? (0, import_i3s.encodeI3SMeshLayer)(mesh, writerOptions) : await (0, import_i3s.encodeI3SMeshLayerWithDraco)(mesh, writerOptions, options.dracoLibraryOptions);
|
|
1924
2705
|
} catch (error) {
|
|
1925
|
-
throw new
|
|
2706
|
+
throw new import_core22.TileConversionError(
|
|
1926
2707
|
"I3S_MESH_PROFILE_UNSUPPORTED",
|
|
1927
2708
|
error instanceof Error ? error.message : String(error)
|
|
1928
2709
|
);
|
|
1929
2710
|
}
|
|
1930
2711
|
signal?.throwIfAborted();
|
|
1931
|
-
|
|
2712
|
+
objectIdOffset += resource.features?.batches.reduce((total, batch) => total + batch.data.numRows, 0) ?? 1;
|
|
2713
|
+
yield { id: resource.id, ...layer, sourceLayerType: resource.sourceLayerType };
|
|
1932
2714
|
},
|
|
1933
2715
|
/** Reports measured precision and explicitly authorized integer representation changes. */
|
|
1934
2716
|
async validateOutput(resource) {
|
|
1935
2717
|
return [
|
|
2718
|
+
...resource.sourceLayerType === "IntegratedMesh" ? [
|
|
2719
|
+
{
|
|
2720
|
+
code: "I3S_LAYER_TYPE_CHANGED",
|
|
2721
|
+
message: "Explicitly authored IntegratedMesh geometry as I3S 1.7 3DObject; integrated surface layer semantics and source LOD are not preserved",
|
|
2722
|
+
severity: "warning",
|
|
2723
|
+
resourceId: resource.id
|
|
2724
|
+
}
|
|
2725
|
+
] : [],
|
|
1936
2726
|
...resource.maximumPositionError > 0 ? [
|
|
1937
2727
|
{
|
|
1938
2728
|
code: "I3S_MESH_POSITION_ROUNDING",
|
|
@@ -1952,7 +2742,7 @@ function createI3SMeshConversionCodec(options) {
|
|
|
1952
2742
|
};
|
|
1953
2743
|
}
|
|
1954
2744
|
function createSingleMeshI3SSink(options) {
|
|
1955
|
-
const memory = (0,
|
|
2745
|
+
const memory = (0, import_core23.createBoundedMemoryTileConversionSink)(options);
|
|
1956
2746
|
let state = "empty";
|
|
1957
2747
|
let completed = false;
|
|
1958
2748
|
return {
|
|
@@ -1962,19 +2752,26 @@ function createSingleMeshI3SSink(options) {
|
|
|
1962
2752
|
async write(resource, signal) {
|
|
1963
2753
|
signal?.throwIfAborted();
|
|
1964
2754
|
if (state !== "empty")
|
|
1965
|
-
throw new
|
|
2755
|
+
throw new import_core22.TileConversionError(
|
|
1966
2756
|
"SINGLE_I3S_MESH_UNAVAILABLE",
|
|
1967
2757
|
"The initial I3S sink accepts exactly one mesh layer"
|
|
1968
2758
|
);
|
|
1969
2759
|
state = "writing";
|
|
1970
2760
|
for (const [resourceId, bytes] of Object.entries(resource.files))
|
|
1971
|
-
await memory.write(
|
|
2761
|
+
await memory.write(
|
|
2762
|
+
{
|
|
2763
|
+
resourceId,
|
|
2764
|
+
parts: [bytes],
|
|
2765
|
+
contentType: resourceId.endsWith(".png") ? "image/png" : resourceId.endsWith(".jpg") ? "image/jpeg" : "application/gzip"
|
|
2766
|
+
},
|
|
2767
|
+
signal
|
|
2768
|
+
);
|
|
1972
2769
|
state = "written";
|
|
1973
2770
|
},
|
|
1974
2771
|
/** Commits a complete single-layer result. */
|
|
1975
2772
|
async finalize(report) {
|
|
1976
2773
|
if (state !== "written")
|
|
1977
|
-
throw new
|
|
2774
|
+
throw new import_core22.TileConversionError(
|
|
1978
2775
|
"SINGLE_I3S_MESH_INCOMPLETE",
|
|
1979
2776
|
"Finalization requires exactly one successfully written layer"
|
|
1980
2777
|
);
|
|
@@ -1990,3 +2787,2184 @@ function createSingleMeshI3SSink(options) {
|
|
|
1990
2787
|
}
|
|
1991
2788
|
};
|
|
1992
2789
|
}
|
|
2790
|
+
|
|
2791
|
+
// src/v5/i3s-mesh-sink.ts
|
|
2792
|
+
var import_geospatial = require("@math.gl/geospatial");
|
|
2793
|
+
var import_compression = require("@loaders.gl/compression");
|
|
2794
|
+
var import_core24 = require("@loaders.gl/tile-converter/v5/core");
|
|
2795
|
+
function createI3SMeshSink(options) {
|
|
2796
|
+
const { maxMeshes, maxResourceBytes } = options;
|
|
2797
|
+
if (!Number.isInteger(maxMeshes) || maxMeshes < 1 || maxMeshes > 64 || !Number.isSafeInteger(maxResourceBytes) || maxResourceBytes < 1)
|
|
2798
|
+
throw new import_core24.TileConversionError(
|
|
2799
|
+
"I3S_COLLECTION_OPTIONS_INVALID",
|
|
2800
|
+
"I3S collections require 1\u201364 meshes and a positive resource byte limit"
|
|
2801
|
+
);
|
|
2802
|
+
const memory = (0, import_core24.createBoundedMemoryTileConversionSink)(options);
|
|
2803
|
+
const compressor = new import_compression.GZipCompressor({ useNative: false });
|
|
2804
|
+
const decompressor = new import_compression.GZipDecompressor({ useNative: false });
|
|
2805
|
+
const leaves = [];
|
|
2806
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
2807
|
+
const objectIdRanges = [];
|
|
2808
|
+
let layer;
|
|
2809
|
+
let schemaKey;
|
|
2810
|
+
let state = "open";
|
|
2811
|
+
let completed = false;
|
|
2812
|
+
async function readBytes(bytes) {
|
|
2813
|
+
const output = await decompressor.decompress(bytes);
|
|
2814
|
+
if (output.byteLength > maxResourceBytes)
|
|
2815
|
+
throw new import_core24.TileConversionError(
|
|
2816
|
+
"I3S_COLLECTION_RESOURCE_TOO_LARGE",
|
|
2817
|
+
"I3S metadata exceeds maxResourceBytes"
|
|
2818
|
+
);
|
|
2819
|
+
return output;
|
|
2820
|
+
}
|
|
2821
|
+
async function readJson(resource, name) {
|
|
2822
|
+
return JSON.parse(new TextDecoder().decode(await readBytes(resource.files[name])));
|
|
2823
|
+
}
|
|
2824
|
+
async function writeJson(resourceId, value) {
|
|
2825
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
2826
|
+
if (bytes.byteLength > maxResourceBytes)
|
|
2827
|
+
throw new import_core24.TileConversionError(
|
|
2828
|
+
"I3S_COLLECTION_RESOURCE_TOO_LARGE",
|
|
2829
|
+
"I3S index exceeds maxResourceBytes"
|
|
2830
|
+
);
|
|
2831
|
+
await memory.write({
|
|
2832
|
+
resourceId,
|
|
2833
|
+
parts: [await compressor.compress(bytes.buffer)],
|
|
2834
|
+
contentType: "application/gzip"
|
|
2835
|
+
});
|
|
2836
|
+
}
|
|
2837
|
+
return {
|
|
2838
|
+
/** Exposes the whole collection atomically. */
|
|
2839
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
2840
|
+
/** Retains one leaf's binary resources and validates its shared schema and IDs. */
|
|
2841
|
+
async write(resource, signal) {
|
|
2842
|
+
signal?.throwIfAborted();
|
|
2843
|
+
if (state !== "open" || identifiers.has(resource.id) || leaves.length >= maxMeshes)
|
|
2844
|
+
throw new import_core24.TileConversionError(
|
|
2845
|
+
"I3S_COLLECTION_UNAVAILABLE",
|
|
2846
|
+
"I3S collection is busy, closed, full, or repeats a placement"
|
|
2847
|
+
);
|
|
2848
|
+
state = "writing";
|
|
2849
|
+
const authored = await readJson(resource, "3dSceneLayer.json.gz");
|
|
2850
|
+
const key = JSON.stringify([
|
|
2851
|
+
authored.fields,
|
|
2852
|
+
authored.attributeStorageInfo,
|
|
2853
|
+
authored.description,
|
|
2854
|
+
authored.store.defaultGeometrySchema
|
|
2855
|
+
]);
|
|
2856
|
+
if (schemaKey !== void 0 && key !== schemaKey)
|
|
2857
|
+
throw new import_core24.TileConversionError(
|
|
2858
|
+
"I3S_COLLECTION_SCHEMA_MISMATCH",
|
|
2859
|
+
"Mesh nodes must share feature fields and the legacy geometry layout"
|
|
2860
|
+
);
|
|
2861
|
+
const objectIds = new DataView(
|
|
2862
|
+
await readBytes(resource.files["nodes/1/attributes/f_0/0.bin.gz"])
|
|
2863
|
+
);
|
|
2864
|
+
const count = objectIds.getUint32(0, true);
|
|
2865
|
+
const first = objectIds.getUint32(4, true);
|
|
2866
|
+
if (objectIds.byteLength !== 4 + count * 4 || !count || objectIdRanges.some(([start, end]) => first < end && first + count > start) || Array.from({ length: count }, (_, index2) => objectIds.getUint32(4 + index2 * 4, true)).some(
|
|
2867
|
+
(value, index2) => value !== first + index2
|
|
2868
|
+
))
|
|
2869
|
+
throw new import_core24.TileConversionError(
|
|
2870
|
+
"I3S_COLLECTION_IDS_INVALID",
|
|
2871
|
+
"Generated object IDs must be contiguous and disjoint across nodes"
|
|
2872
|
+
);
|
|
2873
|
+
const index = leaves.length + 1;
|
|
2874
|
+
const node = (await readJson(resource, "nodepages/0.json.gz")).nodes[1];
|
|
2875
|
+
const legacy = await readJson(
|
|
2876
|
+
resource,
|
|
2877
|
+
"nodes/1/3dNodeIndexDocument.json.gz"
|
|
2878
|
+
);
|
|
2879
|
+
if (node.obb.quaternion.join(",") !== "0,0,0,1" || [...node.obb.center, ...node.obb.halfSize].some((value) => !Number.isFinite(value)) || node.obb.halfSize.some((value) => value < 0))
|
|
2880
|
+
throw new import_core24.TileConversionError(
|
|
2881
|
+
"I3S_COLLECTION_BOUNDS_INVALID",
|
|
2882
|
+
"Expected finite axis-aligned bounds from the I3S mesh writer"
|
|
2883
|
+
);
|
|
2884
|
+
if (!layer) {
|
|
2885
|
+
layer = structuredClone(authored);
|
|
2886
|
+
layer.materialDefinitions = [];
|
|
2887
|
+
layer.geometryDefinitions = [];
|
|
2888
|
+
layer.textureSetDefinitions = [];
|
|
2889
|
+
schemaKey = key;
|
|
2890
|
+
}
|
|
2891
|
+
const material = structuredClone(authored.materialDefinitions[0]);
|
|
2892
|
+
const texture = material.pbrMetallicRoughness?.baseColorTexture;
|
|
2893
|
+
if (texture) {
|
|
2894
|
+
texture.textureSetDefinitionId = layer.textureSetDefinitions.length;
|
|
2895
|
+
layer.textureSetDefinitions.push(authored.textureSetDefinitions[0]);
|
|
2896
|
+
node.mesh.material.resource = index;
|
|
2897
|
+
}
|
|
2898
|
+
node.index = index;
|
|
2899
|
+
node.parentIndex = 0;
|
|
2900
|
+
node.mesh.material.definition = layer.materialDefinitions.length;
|
|
2901
|
+
node.mesh.geometry.definition = layer.geometryDefinitions.length;
|
|
2902
|
+
node.mesh.geometry.resource = index;
|
|
2903
|
+
node.mesh.attribute.resource = index;
|
|
2904
|
+
layer.materialDefinitions.push(material);
|
|
2905
|
+
layer.geometryDefinitions.push(authored.geometryDefinitions[0]);
|
|
2906
|
+
layer.store.resourcePattern = [
|
|
2907
|
+
.../* @__PURE__ */ new Set([...layer.store.resourcePattern, ...authored.store.resourcePattern])
|
|
2908
|
+
];
|
|
2909
|
+
layer.store.extent = layer.store.extent.map(
|
|
2910
|
+
(value, axis) => axis < 2 ? Math.min(value, authored.store.extent[axis]) : Math.max(value, authored.store.extent[axis])
|
|
2911
|
+
);
|
|
2912
|
+
for (const [name, bytes] of Object.entries(resource.files)) {
|
|
2913
|
+
if (name.startsWith("nodes/1/") && !name.endsWith("3dNodeIndexDocument.json.gz"))
|
|
2914
|
+
await memory.write(
|
|
2915
|
+
{
|
|
2916
|
+
resourceId: name.replace("nodes/1/", `nodes/${index}/`),
|
|
2917
|
+
parts: [bytes],
|
|
2918
|
+
contentType: name.endsWith(".gz") ? "application/gzip" : name.endsWith(".png") ? "image/png" : "image/jpeg"
|
|
2919
|
+
},
|
|
2920
|
+
signal
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
leaves.push({ node, legacy: { ...legacy, id: String(index) } });
|
|
2924
|
+
identifiers.add(resource.id);
|
|
2925
|
+
objectIdRanges.push([first, first + count]);
|
|
2926
|
+
state = "open";
|
|
2927
|
+
},
|
|
2928
|
+
/** Rebuilds both indices and layer bounds only after every leaf has completed. */
|
|
2929
|
+
async finalize(report) {
|
|
2930
|
+
if (state !== "open" || !layer || !leaves.length)
|
|
2931
|
+
throw new import_core24.TileConversionError(
|
|
2932
|
+
"I3S_COLLECTION_INCOMPLETE",
|
|
2933
|
+
"Finalization requires successfully written mesh nodes"
|
|
2934
|
+
);
|
|
2935
|
+
state = "writing";
|
|
2936
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
2937
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
2938
|
+
for (const { node } of leaves) {
|
|
2939
|
+
const center2 = import_geospatial.Ellipsoid.WGS84.cartographicToCartesian(node.obb.center);
|
|
2940
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2941
|
+
minimum[axis] = Math.min(minimum[axis], center2[axis] - node.obb.halfSize[axis]);
|
|
2942
|
+
maximum[axis] = Math.max(maximum[axis], center2[axis] + node.obb.halfSize[axis]);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
2946
|
+
const geographicCenter = Array.from(import_geospatial.Ellipsoid.WGS84.cartesianToCartographic(center));
|
|
2947
|
+
const reconstructedCenter = import_geospatial.Ellipsoid.WGS84.cartographicToCartesian(geographicCenter);
|
|
2948
|
+
const obb = {
|
|
2949
|
+
center: geographicCenter,
|
|
2950
|
+
halfSize: minimum.map(
|
|
2951
|
+
(value, axis) => maximum[axis] / 2 - value / 2 + Math.abs(center[axis] - reconstructedCenter[axis]) + 1e-7
|
|
2952
|
+
),
|
|
2953
|
+
quaternion: [0, 0, 0, 1]
|
|
2954
|
+
};
|
|
2955
|
+
if ([...obb.center, ...obb.halfSize].some((value) => !Number.isFinite(value)))
|
|
2956
|
+
throw new import_core24.TileConversionError(
|
|
2957
|
+
"I3S_COLLECTION_BOUNDS_INVALID",
|
|
2958
|
+
"Collection bounds cannot be represented"
|
|
2959
|
+
);
|
|
2960
|
+
const mbs = [...obb.center, Math.hypot(...obb.halfSize)];
|
|
2961
|
+
const children = leaves.map(({ node }) => ({
|
|
2962
|
+
id: String(node.index),
|
|
2963
|
+
href: `../${node.index}`,
|
|
2964
|
+
obb: node.obb,
|
|
2965
|
+
mbs: [...node.obb.center, Math.hypot(...node.obb.halfSize)]
|
|
2966
|
+
}));
|
|
2967
|
+
await writeJson("nodes/root/3dNodeIndexDocument.json.gz", {
|
|
2968
|
+
id: "root",
|
|
2969
|
+
level: 0,
|
|
2970
|
+
version: "1",
|
|
2971
|
+
obb,
|
|
2972
|
+
mbs,
|
|
2973
|
+
lodSelection: [{ metricType: "maxScreenThresholdSQ", maxError: 0 }],
|
|
2974
|
+
children
|
|
2975
|
+
});
|
|
2976
|
+
for (const { node, legacy } of leaves)
|
|
2977
|
+
await writeJson(`nodes/${node.index}/3dNodeIndexDocument.json.gz`, {
|
|
2978
|
+
...legacy,
|
|
2979
|
+
parentNode: { id: "root", href: "../root", obb, mbs }
|
|
2980
|
+
});
|
|
2981
|
+
const nodes = [
|
|
2982
|
+
{ index: 0, obb, lodThreshold: 0, children: leaves.map(({ node }) => node.index) },
|
|
2983
|
+
...leaves.map(({ node }) => node)
|
|
2984
|
+
];
|
|
2985
|
+
for (let page = 0; page * 64 < nodes.length; page++)
|
|
2986
|
+
await writeJson(`nodepages/${page}.json.gz`, {
|
|
2987
|
+
nodes: nodes.slice(page * 64, (page + 1) * 64)
|
|
2988
|
+
});
|
|
2989
|
+
if (!layer.textureSetDefinitions?.length) delete layer.textureSetDefinitions;
|
|
2990
|
+
await writeJson("3dSceneLayer.json.gz", layer);
|
|
2991
|
+
await writeJson("metadata.json.gz", {
|
|
2992
|
+
folderPattern: "BASIC",
|
|
2993
|
+
archiveCompressionType: "STORE",
|
|
2994
|
+
resourceCompressionType: "GZIP",
|
|
2995
|
+
I3SVersion: "1.7",
|
|
2996
|
+
nodeCount: nodes.length
|
|
2997
|
+
});
|
|
2998
|
+
await memory.finalize(report);
|
|
2999
|
+
state = "closed";
|
|
3000
|
+
completed = true;
|
|
3001
|
+
},
|
|
3002
|
+
/** Discards binary resources and index metadata after any failure. */
|
|
3003
|
+
async abort(error) {
|
|
3004
|
+
completed = false;
|
|
3005
|
+
state = "closed";
|
|
3006
|
+
leaves.length = 0;
|
|
3007
|
+
identifiers.clear();
|
|
3008
|
+
objectIdRanges.length = 0;
|
|
3009
|
+
layer = void 0;
|
|
3010
|
+
await memory.abort(error);
|
|
3011
|
+
}
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
// src/v5/conversion-api.ts
|
|
3016
|
+
var TileConversionError18 = class extends Error {
|
|
3017
|
+
/** Stable machine-readable failure code. */
|
|
3018
|
+
code;
|
|
3019
|
+
/** Diagnostics associated with this failure. */
|
|
3020
|
+
diagnostics;
|
|
3021
|
+
/** Creates a typed conversion failure. */
|
|
3022
|
+
constructor(code, message, diagnostics = []) {
|
|
3023
|
+
super(message);
|
|
3024
|
+
this.name = "TileConversionError";
|
|
3025
|
+
this.code = code;
|
|
3026
|
+
this.diagnostics = diagnostics;
|
|
3027
|
+
}
|
|
3028
|
+
};
|
|
3029
|
+
|
|
3030
|
+
// src/v5/resource-fetcher.ts
|
|
3031
|
+
function createTileConversionResourceFetcher(options) {
|
|
3032
|
+
let consumed = options.initialInputBytes ?? 0;
|
|
3033
|
+
if (!Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(consumed) || consumed < 0 || consumed > options.maxInputBytes)
|
|
3034
|
+
throw new TileConversionError18(
|
|
3035
|
+
"INVALID_INPUT_MEMORY_LIMIT",
|
|
3036
|
+
"Resource fetch requires a positive aggregate byte budget and valid initial bytes"
|
|
3037
|
+
);
|
|
3038
|
+
const fetcher = options.fetcher ?? fetch;
|
|
3039
|
+
return async (input, requestOptions) => {
|
|
3040
|
+
options.signal?.throwIfAborted();
|
|
3041
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
3042
|
+
if (!["http:", "https:", "blob:", "data:"].includes(url.protocol))
|
|
3043
|
+
throw new TileConversionError18(
|
|
3044
|
+
"EXTERNAL_RESOURCE_UNSUPPORTED",
|
|
3045
|
+
"Conversion transport requires HTTP(S), Blob or embedded data resources"
|
|
3046
|
+
);
|
|
3047
|
+
const requestSignal = requestOptions?.signal ?? (input instanceof Request ? input.signal : void 0);
|
|
3048
|
+
const signal = options.signal && requestSignal ? AbortSignal.any([options.signal, requestSignal]) : options.signal ?? requestSignal;
|
|
3049
|
+
const response = await fetcher(input, { ...requestOptions, signal });
|
|
3050
|
+
if (!response.ok) {
|
|
3051
|
+
await response.body?.cancel();
|
|
3052
|
+
throw new TileConversionError18(
|
|
3053
|
+
"BROWSER_SOURCE_FETCH_FAILED",
|
|
3054
|
+
`Could not fetch conversion resource (${response.status})`
|
|
3055
|
+
);
|
|
3056
|
+
}
|
|
3057
|
+
const reader = response.body?.getReader();
|
|
3058
|
+
if (!reader)
|
|
3059
|
+
throw new TileConversionError18(
|
|
3060
|
+
"BROWSER_SOURCE_STREAM_UNAVAILABLE",
|
|
3061
|
+
"Conversion resources require readable response streams"
|
|
3062
|
+
);
|
|
3063
|
+
const cancelRead = () => {
|
|
3064
|
+
void reader.cancel(signal?.reason).catch(() => {
|
|
3065
|
+
});
|
|
3066
|
+
};
|
|
3067
|
+
signal?.addEventListener("abort", cancelRead, { once: true });
|
|
3068
|
+
const parts = [];
|
|
3069
|
+
let byteLength = 0;
|
|
3070
|
+
try {
|
|
3071
|
+
while (true) {
|
|
3072
|
+
signal?.throwIfAborted();
|
|
3073
|
+
const { done, value } = await reader.read();
|
|
3074
|
+
signal?.throwIfAborted();
|
|
3075
|
+
if (done) break;
|
|
3076
|
+
if (value.byteLength > options.maxInputBytes - consumed)
|
|
3077
|
+
throw new TileConversionError18(
|
|
3078
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
3079
|
+
"Selected contents and external dependencies exceed the aggregate input byte budget"
|
|
3080
|
+
);
|
|
3081
|
+
consumed += value.byteLength;
|
|
3082
|
+
byteLength += value.byteLength;
|
|
3083
|
+
parts.push(value);
|
|
3084
|
+
}
|
|
3085
|
+
} catch (error) {
|
|
3086
|
+
await reader.cancel(error);
|
|
3087
|
+
throw error;
|
|
3088
|
+
} finally {
|
|
3089
|
+
signal?.removeEventListener("abort", cancelRead);
|
|
3090
|
+
reader.releaseLock();
|
|
3091
|
+
}
|
|
3092
|
+
const bytes = new Uint8Array(byteLength);
|
|
3093
|
+
let offset = 0;
|
|
3094
|
+
for (const part of parts) {
|
|
3095
|
+
bytes.set(part, offset);
|
|
3096
|
+
offset += part.byteLength;
|
|
3097
|
+
}
|
|
3098
|
+
const boundedResponse = new Response(bytes, {
|
|
3099
|
+
status: response.status,
|
|
3100
|
+
statusText: response.statusText,
|
|
3101
|
+
headers: response.headers
|
|
3102
|
+
});
|
|
3103
|
+
Object.defineProperty(boundedResponse, "url", { value: response.url });
|
|
3104
|
+
return boundedResponse;
|
|
3105
|
+
};
|
|
3106
|
+
}
|
|
3107
|
+
|
|
3108
|
+
// src/v5/point-cloud-tileset-sink.ts
|
|
3109
|
+
var import_core25 = require("@loaders.gl/tile-converter/v5/core");
|
|
3110
|
+
function createPointCloudTilesetSink(options) {
|
|
3111
|
+
const { maxTiles, geometricError } = options;
|
|
3112
|
+
if (!Number.isSafeInteger(maxTiles) || maxTiles < 1 || !Number.isFinite(geometricError) || geometricError < 0) {
|
|
3113
|
+
throw new import_core25.TileConversionError(
|
|
3114
|
+
"POINT_CLOUD_TILESET_OPTIONS_INVALID",
|
|
3115
|
+
"Positive maxTiles and finite nonnegative geometricError are required"
|
|
3116
|
+
);
|
|
3117
|
+
}
|
|
3118
|
+
const memory = (0, import_core25.createBoundedMemoryTileConversionSink)(options);
|
|
3119
|
+
const children = [];
|
|
3120
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
3121
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
3122
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
3123
|
+
let state = "open";
|
|
3124
|
+
let completed = false;
|
|
3125
|
+
return {
|
|
3126
|
+
/** Exposes finalized files without exposing a partial package. */
|
|
3127
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
3128
|
+
/** Captures one validated placement and encoded PNTS resource. */
|
|
3129
|
+
async write(resource, signal) {
|
|
3130
|
+
signal?.throwIfAborted();
|
|
3131
|
+
if (state !== "open")
|
|
3132
|
+
throw new import_core25.TileConversionError(
|
|
3133
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3134
|
+
"Point sink is busy or closed"
|
|
3135
|
+
);
|
|
3136
|
+
if (!resource.id || identifiers.has(resource.id) || children.length >= maxTiles) {
|
|
3137
|
+
throw new import_core25.TileConversionError(
|
|
3138
|
+
"POINT_CLOUD_TILESET_PLACEMENT_INVALID",
|
|
3139
|
+
"Placements require unique nonempty identifiers within maxTiles"
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
const [lower, upper] = getPointTileBounds(resource, geometricError);
|
|
3143
|
+
const resourceId = `points/${children.length}.pnts`;
|
|
3144
|
+
state = "writing";
|
|
3145
|
+
await memory.write(
|
|
3146
|
+
{ resourceId, parts: [resource.pnts], contentType: "application/octet-stream" },
|
|
3147
|
+
signal
|
|
3148
|
+
);
|
|
3149
|
+
if (state !== "writing")
|
|
3150
|
+
throw new import_core25.TileConversionError(
|
|
3151
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3152
|
+
"Point sink was aborted during writing"
|
|
3153
|
+
);
|
|
3154
|
+
children.push({
|
|
3155
|
+
boundingVolume: { box: createPointBoundsBox(lower, upper) },
|
|
3156
|
+
geometricError,
|
|
3157
|
+
content: { uri: resourceId }
|
|
3158
|
+
});
|
|
3159
|
+
identifiers.add(resource.id);
|
|
3160
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
3161
|
+
minimum[axis] = Math.min(minimum[axis], lower[axis]);
|
|
3162
|
+
maximum[axis] = Math.max(maximum[axis], upper[axis]);
|
|
3163
|
+
}
|
|
3164
|
+
state = "open";
|
|
3165
|
+
},
|
|
3166
|
+
/** Serializes the enclosing root after all placements have completed. */
|
|
3167
|
+
async finalize(report) {
|
|
3168
|
+
if (state !== "open" || !children.length)
|
|
3169
|
+
throw new import_core25.TileConversionError(
|
|
3170
|
+
"POINT_CLOUD_TILESET_SINK_INCOMPLETE",
|
|
3171
|
+
"Finalization requires successfully written point tiles"
|
|
3172
|
+
);
|
|
3173
|
+
state = "writing";
|
|
3174
|
+
const rootError = Math.max(
|
|
3175
|
+
geometricError,
|
|
3176
|
+
Math.hypot(...minimum.map((value, axis) => maximum[axis] - value))
|
|
3177
|
+
);
|
|
3178
|
+
if (!Number.isFinite(rootError))
|
|
3179
|
+
throw new import_core25.TileConversionError(
|
|
3180
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3181
|
+
"Enclosing bounds must have a finite diagonal"
|
|
3182
|
+
);
|
|
3183
|
+
const json = JSON.stringify({
|
|
3184
|
+
asset: { version: "1.0" },
|
|
3185
|
+
geometricError: rootError,
|
|
3186
|
+
root: {
|
|
3187
|
+
boundingVolume: { box: createPointBoundsBox(minimum, maximum) },
|
|
3188
|
+
geometricError: rootError,
|
|
3189
|
+
refine: "ADD",
|
|
3190
|
+
children
|
|
3191
|
+
}
|
|
3192
|
+
});
|
|
3193
|
+
await memory.write({
|
|
3194
|
+
resourceId: "tileset.json",
|
|
3195
|
+
parts: [json],
|
|
3196
|
+
contentType: "application/json"
|
|
3197
|
+
});
|
|
3198
|
+
if (state !== "writing")
|
|
3199
|
+
throw new import_core25.TileConversionError(
|
|
3200
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3201
|
+
"Point sink was aborted during finalization"
|
|
3202
|
+
);
|
|
3203
|
+
await memory.finalize(report);
|
|
3204
|
+
if (state !== "writing")
|
|
3205
|
+
throw new import_core25.TileConversionError(
|
|
3206
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3207
|
+
"Point sink was aborted during finalization"
|
|
3208
|
+
);
|
|
3209
|
+
state = "closed";
|
|
3210
|
+
completed = true;
|
|
3211
|
+
},
|
|
3212
|
+
/** Clears resource and hierarchy storage on failure or cancellation. */
|
|
3213
|
+
async abort(error) {
|
|
3214
|
+
state = "closed";
|
|
3215
|
+
completed = false;
|
|
3216
|
+
children.length = 0;
|
|
3217
|
+
identifiers.clear();
|
|
3218
|
+
await memory.abort(error);
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
function getPointTileBounds(resource, geometricError) {
|
|
3223
|
+
const reference = resource.spatialReference;
|
|
3224
|
+
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
3225
|
+
if (!reference || !["native", "transformed"].includes(reference.status) || (reference.targetCrs || reference.sourceCrs) !== "EPSG:4978" || (reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference) !== "ellipsoidal" || resource.coordinateSystem !== "cartesian" || resource.cartographicOrigin.length !== 3 || resource.cartographicOrigin.some((value) => value !== 0) || resource.modelMatrix && (resource.modelMatrix.length !== 16 || resource.modelMatrix.some((value, index) => value !== identity[index])) || reference.status === "native" && (reference.coordinateFrame !== "geocentric" || reference.axisOrder !== "xyz" || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter"))) {
|
|
3226
|
+
throw new import_core25.TileConversionError(
|
|
3227
|
+
"POINT_CLOUD_TILESET_FRAME_UNSUPPORTED",
|
|
3228
|
+
"Point tiles require declared EPSG:4978 xyz meters, ellipsoidal heights and no additional source placement"
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
|
|
3232
|
+
throw new import_core25.TileConversionError(
|
|
3233
|
+
"POINT_CLOUD_GEOMETRIC_ERROR_INVALID",
|
|
3234
|
+
"geometricError must include measured position reconstruction error"
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
const [minimum, maximum] = resource.localBoundingBox;
|
|
3238
|
+
const center = resource.rtcCenter;
|
|
3239
|
+
if (center.length !== 3 || minimum.length !== 3 || maximum.length !== 3 || [...center, ...minimum, ...maximum].some((value) => !Number.isFinite(value)) || minimum.some((value, axis) => value > maximum[axis])) {
|
|
3240
|
+
throw new import_core25.TileConversionError(
|
|
3241
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3242
|
+
"RTC center and encoded xyz bounds must be finite and ordered"
|
|
3243
|
+
);
|
|
3244
|
+
}
|
|
3245
|
+
const lower = minimum.map((value, axis) => value + center[axis]);
|
|
3246
|
+
const upper = maximum.map((value, axis) => value + center[axis]);
|
|
3247
|
+
if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
|
|
3248
|
+
throw new import_core25.TileConversionError(
|
|
3249
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3250
|
+
"Reconstructed ECEF bounds must be finite"
|
|
3251
|
+
);
|
|
3252
|
+
return [lower, upper];
|
|
3253
|
+
}
|
|
3254
|
+
function createPointBoundsBox(minimum, maximum) {
|
|
3255
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
3256
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
3257
|
+
return [center[0], center[1], center[2], halfSize[0], 0, 0, 0, halfSize[1], 0, 0, 0, halfSize[2]];
|
|
3258
|
+
}
|
|
3259
|
+
|
|
3260
|
+
// src/v5/i3s-mesh-source.ts
|
|
3261
|
+
var import_core28 = require("@loaders.gl/tile-converter/v5/core");
|
|
3262
|
+
|
|
3263
|
+
// src/v5/i3s-mesh-source-material.ts
|
|
3264
|
+
var import_images3 = require("@loaders.gl/images");
|
|
3265
|
+
var import_core26 = require("@loaders.gl/tile-converter/v5/core");
|
|
3266
|
+
async function mapI3SMeshSourceMaterial(content, header, readExternalResource, contentUri, signal) {
|
|
3267
|
+
const material = Object.fromEntries(
|
|
3268
|
+
Object.entries(content.material || {}).filter(([, value]) => value !== void 0)
|
|
3269
|
+
);
|
|
3270
|
+
const metallicRoughness = material.pbrMetallicRoughness ? Object.fromEntries(
|
|
3271
|
+
Object.entries(material.pbrMetallicRoughness).filter(([, value]) => value !== void 0)
|
|
3272
|
+
) : {};
|
|
3273
|
+
material.pbrMetallicRoughness = metallicRoughness;
|
|
3274
|
+
if (material.cullFace !== void 0 && material.cullFace !== "back") {
|
|
3275
|
+
throw new import_core26.TileConversionError(
|
|
3276
|
+
"I3S_MESH_SOURCE_MATERIAL_UNSUPPORTED",
|
|
3277
|
+
"Only back-face culling is supported"
|
|
3278
|
+
);
|
|
3279
|
+
}
|
|
3280
|
+
delete material.cullFace;
|
|
3281
|
+
let textureInfo = metallicRoughness.baseColorTexture ? { ...metallicRoughness.baseColorTexture } : void 0;
|
|
3282
|
+
const resources = header.textureUrls?.length ? header.textureUrls : header.textureUrl ? [
|
|
3283
|
+
{
|
|
3284
|
+
textureSetDefinitionId: textureInfo?.textureSetDefinitionId ?? 0,
|
|
3285
|
+
textureUrl: header.textureUrl,
|
|
3286
|
+
textureFormat: header.textureFormat || "jpg"
|
|
3287
|
+
}
|
|
3288
|
+
] : [];
|
|
3289
|
+
if (!textureInfo && !header.materialDefinition && resources.length === 1) {
|
|
3290
|
+
textureInfo = { textureSetDefinitionId: resources[0].textureSetDefinitionId };
|
|
3291
|
+
}
|
|
3292
|
+
const encodedTextures = content.textures || {};
|
|
3293
|
+
if (!textureInfo && (resources.length || content.texture || Object.keys(encodedTextures).length)) {
|
|
3294
|
+
throw new import_core26.TileConversionError(
|
|
3295
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3296
|
+
"Texture resources require a base-color mapping"
|
|
3297
|
+
);
|
|
3298
|
+
}
|
|
3299
|
+
if (textureInfo) {
|
|
3300
|
+
const definitionId = textureInfo.textureSetDefinitionId;
|
|
3301
|
+
const wrappingModes = { none: 33071, repeat: 10497, mirror: 33648 };
|
|
3302
|
+
if (typeof definitionId !== "number" || !Number.isSafeInteger(definitionId) || definitionId < 0 || textureInfo.texCoord !== void 0 && textureInfo.texCoord !== 0 || Object.keys(textureInfo).some(
|
|
3303
|
+
(name) => !["textureSetDefinitionId", "texCoord", "wrapS", "wrapT", "texture"].includes(name)
|
|
3304
|
+
) || ["wrapS", "wrapT"].some(
|
|
3305
|
+
(name) => textureInfo[name] !== void 0 && (typeof textureInfo[name] !== "string" || !Object.hasOwn(wrappingModes, textureInfo[name]))
|
|
3306
|
+
) || resources.length > 1 || resources.some((resource) => resource.textureSetDefinitionId !== definitionId) || Object.keys(encodedTextures).some((key) => key !== String(definitionId))) {
|
|
3307
|
+
throw new import_core26.TileConversionError(
|
|
3308
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3309
|
+
"Only one base-color texture set on TEXCOORD_0 with qualified wrapping is supported"
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
let encoded = encodedTextures[String(definitionId)] || content.texture || textureInfo.texture?.source?.image;
|
|
3313
|
+
if (!encoded) {
|
|
3314
|
+
if (!resources.length || !readExternalResource) {
|
|
3315
|
+
throw new import_core26.TileConversionError(
|
|
3316
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3317
|
+
"Encoded texture bytes or an explicit bounded resource reader are required"
|
|
3318
|
+
);
|
|
3319
|
+
}
|
|
3320
|
+
signal?.throwIfAborted();
|
|
3321
|
+
encoded = await readExternalResource(resources[0].textureUrl, contentUri, signal);
|
|
3322
|
+
signal?.throwIfAborted();
|
|
3323
|
+
}
|
|
3324
|
+
const data = encoded instanceof ArrayBuffer ? new Uint8Array(encoded) : encoded;
|
|
3325
|
+
let image;
|
|
3326
|
+
if (data instanceof Uint8Array) {
|
|
3327
|
+
try {
|
|
3328
|
+
image = (0, import_images3.getBinaryImageMetadata)(new DataView(data.buffer, data.byteOffset, data.byteLength));
|
|
3329
|
+
} catch {
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
const declaredFormat = resources[0]?.textureFormat;
|
|
3333
|
+
if (!image || !["image/png", "image/jpeg"].includes(image.mimeType) || declaredFormat && (declaredFormat === "png" ? image.mimeType !== "image/png" : declaredFormat === "jpg" ? image.mimeType !== "image/jpeg" : true)) {
|
|
3334
|
+
throw new import_core26.TileConversionError(
|
|
3335
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3336
|
+
"I3S base-color bytes must have matching PNG/JPEG headers"
|
|
3337
|
+
);
|
|
3338
|
+
}
|
|
3339
|
+
const sampler = Object.fromEntries(
|
|
3340
|
+
["wrapS", "wrapT"].filter((name) => textureInfo[name] !== void 0).map((name) => [name, wrappingModes[textureInfo[name]]])
|
|
3341
|
+
);
|
|
3342
|
+
metallicRoughness.baseColorTexture = {
|
|
3343
|
+
texCoord: 0,
|
|
3344
|
+
texture: {
|
|
3345
|
+
source: { mimeType: image.mimeType, bufferView: { data } },
|
|
3346
|
+
...Object.keys(sampler).length ? { sampler } : {}
|
|
3347
|
+
}
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3350
|
+
const mapped = await mapMeshSourceMaterial(
|
|
3351
|
+
material,
|
|
3352
|
+
void 0,
|
|
3353
|
+
contentUri,
|
|
3354
|
+
signal
|
|
3355
|
+
);
|
|
3356
|
+
if (mapped?.baseColorTexture && !content.sourceAttributes?.uv0) {
|
|
3357
|
+
throw new import_core26.TileConversionError(
|
|
3358
|
+
"MESH_TEXCOORD_REQUIRED",
|
|
3359
|
+
"Base-color textures require TEXCOORD_0"
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
return mapped;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
// src/v5/i3s-mesh-source-features.ts
|
|
3366
|
+
var import_i3s2 = require("@loaders.gl/i3s");
|
|
3367
|
+
var import_core27 = require("@loaders.gl/tile-converter/v5/core");
|
|
3368
|
+
async function extractI3SMeshFeatures(content, item, metadata, options, readExternalResource, contentUri, signal) {
|
|
3369
|
+
const maximumAttributeBytes = options.maxAttributeBytes;
|
|
3370
|
+
const storage = metadata.tileset.attributeStorageInfo;
|
|
3371
|
+
const attributeUrls = item.tile.header.attributeUrls;
|
|
3372
|
+
if (!options.metadataClass || !options.objectIdProperty || options.rawMetadataField || !Number.isSafeInteger(maximumAttributeBytes) || maximumAttributeBytes <= 0 || !storage?.length || attributeUrls?.length !== storage.length || !readExternalResource || new Set(storage.map((attribute2) => attribute2.name)).size !== storage.length || new Set(storage.map((attribute2) => attribute2.key)).size !== storage.length || !storage.some((attribute2) => attribute2.name === options.objectIdProperty) || (metadata.tileset.fields || []).some(
|
|
3373
|
+
(field) => field.domain || field.type === "esriFieldTypeDate" || !storage.some((attribute2) => attribute2.name === field.name)
|
|
3374
|
+
)) {
|
|
3375
|
+
throw new import_core27.TileConversionError(
|
|
3376
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3377
|
+
"I3S attributes require an explicit scalar schema, object ID column, byte budget and resource reader; domains/dates/raw metadata need a custom mapper"
|
|
3378
|
+
);
|
|
3379
|
+
}
|
|
3380
|
+
const attributeLoader = await import_i3s2.I3SAttributeLoader.preload();
|
|
3381
|
+
const columns = /* @__PURE__ */ Object.create(null);
|
|
3382
|
+
let inputBytes = 0;
|
|
3383
|
+
let count;
|
|
3384
|
+
for (const [index, attribute2] of storage.entries()) {
|
|
3385
|
+
signal?.throwIfAborted();
|
|
3386
|
+
const values = attribute2.objectIds ?? attribute2.attributeValues;
|
|
3387
|
+
const type = values?.valueType;
|
|
3388
|
+
const expectedOrdering = attribute2.objectIds ? ["objectIds"] : type === "String" ? ["attributeByteCounts", "attributeValues"] : ["attributeValues"];
|
|
3389
|
+
const expectedHeader = type === "String" ? [
|
|
3390
|
+
{ property: "count", valueType: "UInt32" },
|
|
3391
|
+
{ property: "attributeValuesByteCount", valueType: "UInt32" }
|
|
3392
|
+
] : [{ property: "count", valueType: "UInt32" }];
|
|
3393
|
+
if (!type || values?.valuesPerElement !== 1 || attribute2.objectIds && attribute2.attributeValues || attribute2.header?.length !== expectedHeader.length || expectedHeader.some(
|
|
3394
|
+
(value, headerIndex) => attribute2.header[headerIndex]?.property !== value.property || attribute2.header[headerIndex]?.valueType !== value.valueType
|
|
3395
|
+
) || attribute2.ordering && JSON.stringify(
|
|
3396
|
+
attribute2.ordering.map((value) => value === "ObjectIds" ? "objectIds" : value)
|
|
3397
|
+
) !== JSON.stringify(expectedOrdering) || type === "String" && (values.encoding !== "UTF-8" || attribute2.attributeByteCounts?.valueType !== "UInt32" || attribute2.attributeByteCounts.valuesPerElement !== 1) || !attributeUrls[index]) {
|
|
3398
|
+
throw new import_core27.TileConversionError(
|
|
3399
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3400
|
+
`Attribute ${attribute2.name} requires a standard scalar/string resource layout`
|
|
3401
|
+
);
|
|
3402
|
+
}
|
|
3403
|
+
const bytes = await readExternalResource(attributeUrls[index], contentUri, signal);
|
|
3404
|
+
signal?.throwIfAborted();
|
|
3405
|
+
inputBytes += bytes instanceof Uint8Array ? bytes.byteLength : Infinity;
|
|
3406
|
+
if (!(bytes instanceof Uint8Array) || inputBytes > maximumAttributeBytes) {
|
|
3407
|
+
throw new import_core27.TileConversionError(
|
|
3408
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
3409
|
+
"I3S node attributes exceed maxAttributeBytes"
|
|
3410
|
+
);
|
|
3411
|
+
}
|
|
3412
|
+
let parsed;
|
|
3413
|
+
try {
|
|
3414
|
+
parsed = await attributeLoader.parse(Uint8Array.from(bytes).buffer, {
|
|
3415
|
+
attributeName: attribute2.name,
|
|
3416
|
+
attributeType: type,
|
|
3417
|
+
i3s: { attributeValues: "exact" }
|
|
3418
|
+
});
|
|
3419
|
+
} catch (error) {
|
|
3420
|
+
throw new import_core27.TileConversionError(
|
|
3421
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_INVALID",
|
|
3422
|
+
`Invalid attribute ${attribute2.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
const column = parsed[attribute2.name];
|
|
3426
|
+
if (!column || count !== void 0 && column.length !== count || !column.length) {
|
|
3427
|
+
throw new import_core27.TileConversionError(
|
|
3428
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_COUNT_INVALID",
|
|
3429
|
+
"Every attribute must have the same nonzero row count"
|
|
3430
|
+
);
|
|
3431
|
+
}
|
|
3432
|
+
if (Array.from(column).some(
|
|
3433
|
+
(value) => typeof value === "number" && !Number.isFinite(value)
|
|
3434
|
+
)) {
|
|
3435
|
+
throw new import_core27.TileConversionError(
|
|
3436
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3437
|
+
"Numeric noData values require an explicit custom mapper"
|
|
3438
|
+
);
|
|
3439
|
+
}
|
|
3440
|
+
count = column.length;
|
|
3441
|
+
columns[attribute2.name] = column;
|
|
3442
|
+
}
|
|
3443
|
+
const identifiers = columns[options.objectIdProperty];
|
|
3444
|
+
const attributeRows = /* @__PURE__ */ new Map();
|
|
3445
|
+
for (let index = 0; index < count; index++) {
|
|
3446
|
+
const identifier = identifiers[index];
|
|
3447
|
+
if (!(typeof identifier === "bigint" || typeof identifier === "number" && Number.isSafeInteger(identifier)) || attributeRows.has(BigInt(identifier))) {
|
|
3448
|
+
throw new import_core27.TileConversionError(
|
|
3449
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3450
|
+
"Object IDs must be unique exact integers"
|
|
3451
|
+
);
|
|
3452
|
+
}
|
|
3453
|
+
attributeRows.set(BigInt(identifier), index);
|
|
3454
|
+
}
|
|
3455
|
+
const vertexCount = content.vertexCount;
|
|
3456
|
+
const geometryIdentifiers = content.featureIds;
|
|
3457
|
+
const attribute = content.sourceAttributes?.id;
|
|
3458
|
+
if (geometryIdentifiers.length !== vertexCount || attribute?.size !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(geometryIdentifiers).some((value, index) => value !== attribute.value[index])) {
|
|
3459
|
+
throw new import_core27.TileConversionError(
|
|
3460
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3461
|
+
"Geometry requires one exact unnormalized object ID per vertex"
|
|
3462
|
+
);
|
|
3463
|
+
}
|
|
3464
|
+
const indices = content.indices;
|
|
3465
|
+
const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
|
|
3466
|
+
if (content.drawRanges?.length) {
|
|
3467
|
+
const covered = new Uint8Array(triangleFeatureIndices.length);
|
|
3468
|
+
for (const range of content.drawRanges) {
|
|
3469
|
+
if (!Number.isSafeInteger(range.firstPrimitive) || range.firstPrimitive < 0 || !Number.isSafeInteger(range.primitiveCount) || range.primitiveCount <= 0 || range.firstPrimitive + range.primitiveCount > covered.length) {
|
|
3470
|
+
throw new import_core27.TileConversionError(
|
|
3471
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3472
|
+
"Feature draw ranges must cover valid triangles"
|
|
3473
|
+
);
|
|
3474
|
+
}
|
|
3475
|
+
for (let triangle = range.firstPrimitive; triangle < range.firstPrimitive + range.primitiveCount; triangle++) {
|
|
3476
|
+
const vertex = indices?.[triangle * 3] ?? triangle * 3;
|
|
3477
|
+
if (covered[triangle] || geometryIdentifiers[vertex] !== range.featureId) {
|
|
3478
|
+
throw new import_core27.TileConversionError(
|
|
3479
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3480
|
+
"Feature draw ranges must agree with decoded ownership without overlapping"
|
|
3481
|
+
);
|
|
3482
|
+
}
|
|
3483
|
+
covered[triangle] = 1;
|
|
3484
|
+
}
|
|
3485
|
+
}
|
|
3486
|
+
if (covered.some((value) => value === 0)) {
|
|
3487
|
+
throw new import_core27.TileConversionError(
|
|
3488
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3489
|
+
"Feature draw ranges must cover every triangle"
|
|
3490
|
+
);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
const usedRows = /* @__PURE__ */ new Set();
|
|
3494
|
+
for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
|
|
3495
|
+
const corners = [0, 1, 2].map(
|
|
3496
|
+
(corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
|
|
3497
|
+
);
|
|
3498
|
+
const featureIds = corners.map((corner) => geometryIdentifiers[corner]);
|
|
3499
|
+
const row = attributeRows.get(BigInt(featureIds[0]));
|
|
3500
|
+
if (row === void 0 || featureIds.some((identifier) => identifier !== featureIds[0])) {
|
|
3501
|
+
throw new import_core27.TileConversionError(
|
|
3502
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3503
|
+
"Each triangle must reference one declared attribute row"
|
|
3504
|
+
);
|
|
3505
|
+
}
|
|
3506
|
+
triangleFeatureIndices[triangle] = row;
|
|
3507
|
+
usedRows.add(row);
|
|
3508
|
+
}
|
|
3509
|
+
if (usedRows.size !== count) {
|
|
3510
|
+
throw new import_core27.TileConversionError(
|
|
3511
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3512
|
+
"Attribute rows without geometry require a separately qualified mapping"
|
|
3513
|
+
);
|
|
3514
|
+
}
|
|
3515
|
+
const stableProperty = options.sourceFeatureIdProperty ?? options.objectIdProperty;
|
|
3516
|
+
if (!Object.hasOwn(columns, stableProperty)) {
|
|
3517
|
+
throw new import_core27.TileConversionError(
|
|
3518
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3519
|
+
"Stable identifier column is missing"
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
const batches = convertFeatureAttributesToArrowBatches(
|
|
3523
|
+
Array.from({ length: count }, (_, index) => ({
|
|
3524
|
+
featureId: columns[stableProperty][index],
|
|
3525
|
+
metadataClass: options.metadataClass,
|
|
3526
|
+
properties: Object.fromEntries(
|
|
3527
|
+
Object.entries(columns).filter(([name]) => name !== options.objectIdProperty && name !== stableProperty).map(([name, values]) => [name, values[index]])
|
|
3528
|
+
)
|
|
3529
|
+
})),
|
|
3530
|
+
options
|
|
3531
|
+
);
|
|
3532
|
+
const stableIds = /* @__PURE__ */ new Set();
|
|
3533
|
+
for (const batch of batches) {
|
|
3534
|
+
const column = batch.data.getChild(options.featureIdField || "feature_id");
|
|
3535
|
+
for (let index = 0; index < column.length; index++) {
|
|
3536
|
+
const identifier = column.get(index);
|
|
3537
|
+
if (stableIds.has(identifier)) {
|
|
3538
|
+
throw new import_core27.TileConversionError(
|
|
3539
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3540
|
+
"Stable IDs must be unique"
|
|
3541
|
+
);
|
|
3542
|
+
}
|
|
3543
|
+
stableIds.add(identifier);
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return {
|
|
3547
|
+
batches,
|
|
3548
|
+
triangleFeatureIndices,
|
|
3549
|
+
featureIdField: options.featureIdField || "feature_id",
|
|
3550
|
+
integer64Encoding: options.integer64Encoding
|
|
3551
|
+
};
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
// src/v5/i3s-mesh-source.ts
|
|
3555
|
+
function createI3SMeshTilesetConversionSource(tileset, options = {}) {
|
|
3556
|
+
const source = (0, import_core28.createTilesetConversionSource)(tileset, options);
|
|
3557
|
+
return {
|
|
3558
|
+
/** Validates the layer profile and explicit source decode policy before reading. */
|
|
3559
|
+
async inspect(signal) {
|
|
3560
|
+
if (options.features && options.getFeatures) {
|
|
3561
|
+
throw new import_core28.TileConversionError(
|
|
3562
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3563
|
+
"Choose explicit resource features or a custom getFeatures mapper, not both"
|
|
3564
|
+
);
|
|
3565
|
+
}
|
|
3566
|
+
const metadata = await source.inspect(signal);
|
|
3567
|
+
if (metadata.type !== "I3S" || !["3DObject", "IntegratedMesh"].includes(metadata.tileset.layerType) || tileset.options.i3s?.geometryMode !== "source") {
|
|
3568
|
+
throw new import_core28.TileConversionError(
|
|
3569
|
+
"I3S_MESH_SOURCE_PROFILE_UNSUPPORTED",
|
|
3570
|
+
"I3S mesh extraction requires a 3DObject/IntegratedMesh runtime with geometryMode=source"
|
|
3571
|
+
);
|
|
3572
|
+
}
|
|
3573
|
+
if (!metadata.spatialReference?.sourceCrs) {
|
|
3574
|
+
throw new import_core28.TileConversionError(
|
|
3575
|
+
"I3S_MESH_SOURCE_REFERENCE_REQUIRED",
|
|
3576
|
+
"I3S conversion requires an explicit source CRS"
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
return metadata;
|
|
3580
|
+
},
|
|
3581
|
+
/** Converts each supported content entry before shared traversal releases its buffers. */
|
|
3582
|
+
async *read(metadata, signal) {
|
|
3583
|
+
for await (const item of source.read(metadata, signal)) {
|
|
3584
|
+
for (const entry of item.contents) {
|
|
3585
|
+
signal?.throwIfAborted();
|
|
3586
|
+
const content = entry.payload;
|
|
3587
|
+
const attributes = content?.sourceAttributes;
|
|
3588
|
+
if (content?.geometryMode !== "source" || content.topology !== "triangle-list" || !attributes || !(attributes.position?.value instanceof Float64Array) || content.spatialReference?.status !== "native" || [
|
|
3589
|
+
"sourceCrs",
|
|
3590
|
+
"verticalCrs",
|
|
3591
|
+
"axisOrder",
|
|
3592
|
+
"units",
|
|
3593
|
+
"verticalUnitScale",
|
|
3594
|
+
"heightReference",
|
|
3595
|
+
"elevationMode",
|
|
3596
|
+
"elevationOffset",
|
|
3597
|
+
"elevationUnitScale",
|
|
3598
|
+
"coordinateEpoch"
|
|
3599
|
+
].some(
|
|
3600
|
+
(field) => JSON.stringify(content.spatialReference?.[field]) !== JSON.stringify(metadata.spatialReference?.[field])
|
|
3601
|
+
)) {
|
|
3602
|
+
throw new import_core28.TileConversionError(
|
|
3603
|
+
"I3S_MESH_SOURCE_GEOMETRY_UNSUPPORTED",
|
|
3604
|
+
"I3S extraction requires original source-frame Float64 triangle geometry"
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
if (!["earth-centered", "vertex-reference-frame"].includes(
|
|
3608
|
+
content.normalReferenceFrame || ""
|
|
3609
|
+
) || content.normalReferenceFrame !== (metadata.tileset.store?.normalReferenceFrame || "earth-centered")) {
|
|
3610
|
+
throw new import_core28.TileConversionError(
|
|
3611
|
+
"I3S_MESH_SOURCE_NORMAL_FRAME_UNSUPPORTED",
|
|
3612
|
+
"Unknown I3S vector basis"
|
|
3613
|
+
);
|
|
3614
|
+
}
|
|
3615
|
+
if (content.meshSegmentation || Object.entries(attributes).some(
|
|
3616
|
+
([name, value]) => value && !["position", "normal", "uv0", "id", "faceRange"].includes(name)
|
|
3617
|
+
)) {
|
|
3618
|
+
throw new import_core28.TileConversionError(
|
|
3619
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3620
|
+
"Only positions, normals, TEXCOORD_0 and explicitly mapped feature associations are supported"
|
|
3621
|
+
);
|
|
3622
|
+
}
|
|
3623
|
+
const hasFeatures = content.featureIds.length > 0 || metadata.tileset.fields?.length || metadata.tileset.attributeStorageInfo?.length;
|
|
3624
|
+
if (Array.from(content.featureIds).some((value) => !Number.isSafeInteger(value))) {
|
|
3625
|
+
throw new import_core28.TileConversionError(
|
|
3626
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3627
|
+
"Decoded numeric feature IDs must be exact safe integers"
|
|
3628
|
+
);
|
|
3629
|
+
}
|
|
3630
|
+
if (hasFeatures && !options.getFeatures && !options.features) {
|
|
3631
|
+
throw new import_core28.TileConversionError(
|
|
3632
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3633
|
+
"I3S feature data requires explicit resource features or a custom Arrow mapper"
|
|
3634
|
+
);
|
|
3635
|
+
}
|
|
3636
|
+
const mesh = validateMeshGeometry(
|
|
3637
|
+
{
|
|
3638
|
+
mode: 4,
|
|
3639
|
+
topology: "triangle-list",
|
|
3640
|
+
attributes: {
|
|
3641
|
+
POSITION: attributes.position,
|
|
3642
|
+
...attributes.normal ? { NORMAL: attributes.normal } : {},
|
|
3643
|
+
...attributes.uv0 ? { TEXCOORD_0: attributes.uv0 } : {}
|
|
3644
|
+
},
|
|
3645
|
+
...content.indices ? { indices: { value: content.indices, size: 1 } } : {}
|
|
3646
|
+
},
|
|
3647
|
+
true
|
|
3648
|
+
);
|
|
3649
|
+
if (mesh.attributes.POSITION.value.length / 3 !== content.vertexCount) {
|
|
3650
|
+
throw new import_core28.TileConversionError(
|
|
3651
|
+
"I3S_MESH_SOURCE_COUNT_INVALID",
|
|
3652
|
+
"I3S vertexCount must match the decoded geometry"
|
|
3653
|
+
);
|
|
3654
|
+
}
|
|
3655
|
+
const features = options.getFeatures ? await options.getFeatures(content, item, metadata, signal) : options.features ? await extractI3SMeshFeatures(
|
|
3656
|
+
content,
|
|
3657
|
+
item,
|
|
3658
|
+
metadata,
|
|
3659
|
+
options.features,
|
|
3660
|
+
options.readExternalResource,
|
|
3661
|
+
entry.uri,
|
|
3662
|
+
signal
|
|
3663
|
+
) : void 0;
|
|
3664
|
+
signal?.throwIfAborted();
|
|
3665
|
+
if (hasFeatures && !features) {
|
|
3666
|
+
throw new import_core28.TileConversionError(
|
|
3667
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3668
|
+
"The feature mapper must return all declared feature data"
|
|
3669
|
+
);
|
|
3670
|
+
}
|
|
3671
|
+
const mappedMaterial = await mapI3SMeshSourceMaterial(
|
|
3672
|
+
content,
|
|
3673
|
+
item.tile.header,
|
|
3674
|
+
options.readExternalResource,
|
|
3675
|
+
entry.uri,
|
|
3676
|
+
signal
|
|
3677
|
+
);
|
|
3678
|
+
signal?.throwIfAborted();
|
|
3679
|
+
yield {
|
|
3680
|
+
id: `${item.tile.id}/${entry.index}`,
|
|
3681
|
+
origin: [0, 0, 0],
|
|
3682
|
+
sourceLayerType: metadata.tileset.layerType,
|
|
3683
|
+
mesh,
|
|
3684
|
+
normalReferenceFrame: content.normalReferenceFrame === "vertex-reference-frame" ? "vertex-reference-frame" : "earth-centered",
|
|
3685
|
+
features,
|
|
3686
|
+
material: mappedMaterial
|
|
3687
|
+
};
|
|
3688
|
+
}
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
// src/v5/point-cloud-tileset.ts
|
|
3695
|
+
var import_point_cloud_tile_source = require("@loaders.gl/tiles/point-cloud-tile-source");
|
|
3696
|
+
var import_core29 = require("@loaders.gl/tile-converter/v5/core");
|
|
3697
|
+
async function convertPointCloudToTileset(input, options) {
|
|
3698
|
+
const reference = options.spatialReference;
|
|
3699
|
+
if (!reference || reference.status !== "native" || reference.sourceCrs !== "EPSG:4978" || reference.targetCrs && reference.targetCrs !== "EPSG:4978" || reference.coordinateFrame !== "geocentric" || reference.axisOrder !== "xyz" || reference.heightReference !== "ellipsoidal" || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter")) {
|
|
3700
|
+
throw new import_core29.TileConversionError(
|
|
3701
|
+
"POINT_CLOUD_TILESET_FRAME_UNSUPPORTED",
|
|
3702
|
+
"Point authoring requires explicit native EPSG:4978 xyz meters and ellipsoidal heights"
|
|
3703
|
+
);
|
|
3704
|
+
}
|
|
3705
|
+
if (options.archive && (!Number.isSafeInteger(options.archive.maxArchiveBytes) || options.archive.maxArchiveBytes < 0)) {
|
|
3706
|
+
throw new import_core29.TileConversionError(
|
|
3707
|
+
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
3708
|
+
"Archive byte limit must be a non-negative safe integer"
|
|
3709
|
+
);
|
|
3710
|
+
}
|
|
3711
|
+
options.signal?.throwIfAborted();
|
|
3712
|
+
const sink = createPointCloudTilesetSink(options);
|
|
3713
|
+
let source;
|
|
3714
|
+
try {
|
|
3715
|
+
source = new import_point_cloud_tile_source.PointCloudTileSource(input, {
|
|
3716
|
+
pointCloudTiler: { ...options.tiling, signal: options.signal, spatialReference: reference }
|
|
3717
|
+
});
|
|
3718
|
+
const { pointCount } = await source.getMetadata();
|
|
3719
|
+
const report = await convertPointCloudSource(source, {
|
|
3720
|
+
sink,
|
|
3721
|
+
signal: options.signal,
|
|
3722
|
+
onProgress: options.onProgress,
|
|
3723
|
+
measureInputBytes: (tile) => {
|
|
3724
|
+
const table = tile.content?.data.data;
|
|
3725
|
+
return table ? table.schema.fields.reduce(
|
|
3726
|
+
(bytes, _field, index) => bytes + (table.getChildAt(index)?.byteLength ?? 0),
|
|
3727
|
+
0
|
|
3728
|
+
) : 0;
|
|
3729
|
+
},
|
|
3730
|
+
getTileEncodingOptions: (tile) => ({
|
|
3731
|
+
rtcCenter: [
|
|
3732
|
+
tile.header.boundingVolume.center[0],
|
|
3733
|
+
tile.header.boundingVolume.center[1],
|
|
3734
|
+
tile.header.boundingVolume.center[2]
|
|
3735
|
+
],
|
|
3736
|
+
maxPositionError: options.geometricError
|
|
3737
|
+
})
|
|
3738
|
+
});
|
|
3739
|
+
options.signal?.throwIfAborted();
|
|
3740
|
+
const files = sink.getFiles();
|
|
3741
|
+
const archive = options.archive ? await createTileConversionArchive(files, {
|
|
3742
|
+
format: "3tz",
|
|
3743
|
+
maxArchiveBytes: options.archive.maxArchiveBytes,
|
|
3744
|
+
signal: options.signal
|
|
3745
|
+
}) : void 0;
|
|
3746
|
+
options.signal?.throwIfAborted();
|
|
3747
|
+
return { pointCount, report, files, archive };
|
|
3748
|
+
} catch (error) {
|
|
3749
|
+
await sink.abort(error);
|
|
3750
|
+
throw error;
|
|
3751
|
+
} finally {
|
|
3752
|
+
source?.close();
|
|
3753
|
+
}
|
|
3754
|
+
}
|
|
3755
|
+
|
|
3756
|
+
// src/v5/point-cloud-file.ts
|
|
3757
|
+
var import_copc = require("@loaders.gl/copc");
|
|
3758
|
+
var import_potree = require("@loaders.gl/potree");
|
|
3759
|
+
var import_schema_utils5 = require("@loaders.gl/schema-utils");
|
|
3760
|
+
var import_zip = require("@loaders.gl/zip");
|
|
3761
|
+
async function encodePotreeArchive(input, options) {
|
|
3762
|
+
validateLimit(options.maxArchiveBytes);
|
|
3763
|
+
const dataset = await (0, import_potree.encodePotreeDataset)(input, { potree: options.potree });
|
|
3764
|
+
return encodePotreeDatasetArchive(dataset, options.maxArchiveBytes, options.potree?.signal);
|
|
3765
|
+
}
|
|
3766
|
+
async function encodePotreeDatasetArchive(dataset, maxArchiveBytes, signal) {
|
|
3767
|
+
validateLimit(maxArchiveBytes);
|
|
3768
|
+
for (const path of dataset.files.keys()) validatePath(path);
|
|
3769
|
+
signal?.throwIfAborted();
|
|
3770
|
+
const archive = await (0, import_zip.encodeZip)(Object.fromEntries(dataset.files), {
|
|
3771
|
+
compression: "store",
|
|
3772
|
+
zip64: "never",
|
|
3773
|
+
maxEntries: 65535,
|
|
3774
|
+
maxArchiveBytes: BigInt(maxArchiveBytes),
|
|
3775
|
+
signal
|
|
3776
|
+
});
|
|
3777
|
+
signal?.throwIfAborted();
|
|
3778
|
+
return new Blob([archive], { type: "application/zip" });
|
|
3779
|
+
}
|
|
3780
|
+
async function openPointCloudFile(input, options) {
|
|
3781
|
+
validateLimit(options.maxFileBytes);
|
|
3782
|
+
if (input.size > options.maxFileBytes) throw new RangeError("Point file input budget exceeded");
|
|
3783
|
+
options.signal?.throwIfAborted();
|
|
3784
|
+
if (options.format === "copc") {
|
|
3785
|
+
const source2 = import_copc.COPCSourceLoader.createDataSource(input, options.sourceOptions || {});
|
|
3786
|
+
try {
|
|
3787
|
+
await source2.metadata;
|
|
3788
|
+
options.signal?.throwIfAborted();
|
|
3789
|
+
return { source: source2, close: () => source2.close() };
|
|
3790
|
+
} catch (error) {
|
|
3791
|
+
await source2.close();
|
|
3792
|
+
throw error;
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
if (options.format !== "potree-zip") throw new TypeError("Unsupported point file format");
|
|
3796
|
+
const maxEntries = options.maxEntries ?? 1e5;
|
|
3797
|
+
const maxResourceBytes = options.maxResourceBytes ?? 64 * 1024 * 1024;
|
|
3798
|
+
validateLimit(maxEntries);
|
|
3799
|
+
validateLimit(maxResourceBytes);
|
|
3800
|
+
const controller = new AbortController();
|
|
3801
|
+
const signals = [controller.signal, options.signal, options.sourceOptions?.potree?.signal].filter(
|
|
3802
|
+
(value) => Boolean(value)
|
|
3803
|
+
);
|
|
3804
|
+
const signal = AbortSignal.any(signals);
|
|
3805
|
+
let archive;
|
|
3806
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3807
|
+
let source;
|
|
3808
|
+
try {
|
|
3809
|
+
archive = await (0, import_zip.openZipArchive)(input, {
|
|
3810
|
+
maxArchiveBytes: BigInt(options.maxFileBytes),
|
|
3811
|
+
maxEntries,
|
|
3812
|
+
maxReadBytes: Math.max(65557, maxResourceBytes),
|
|
3813
|
+
maxCompressedBytes: BigInt(maxResourceBytes),
|
|
3814
|
+
maxDecodedBytes: BigInt(maxResourceBytes),
|
|
3815
|
+
signal
|
|
3816
|
+
});
|
|
3817
|
+
for await (const entry of archive.entries()) {
|
|
3818
|
+
if (entry.directory) continue;
|
|
3819
|
+
validatePath(entry.path);
|
|
3820
|
+
if (entry.compression !== "store")
|
|
3821
|
+
throw new Error("Potree bundles require unencrypted STORE entries");
|
|
3822
|
+
entries.set(entry.path, entry);
|
|
3823
|
+
}
|
|
3824
|
+
const candidates = ["metadata.json", "cloud.js"].filter((path) => entries.has(path));
|
|
3825
|
+
const metadataPath = options.metadataPath || (candidates.length === 1 ? candidates[0] : "");
|
|
3826
|
+
validatePath(metadataPath);
|
|
3827
|
+
if (!entries.has(metadataPath) || !/(^|\/)(metadata\.json|cloud\.js)$/.test(metadataPath))
|
|
3828
|
+
throw new Error("Potree ZIP requires one metadata entry or an explicit metadataPath");
|
|
3829
|
+
const prefix = "https://point-bundle.loaders.gl.invalid/";
|
|
3830
|
+
const fetchResource = async (resource, request = {}) => {
|
|
3831
|
+
signal.throwIfAborted();
|
|
3832
|
+
request.signal?.throwIfAborted();
|
|
3833
|
+
const url = new URL(resource);
|
|
3834
|
+
if (url.origin !== new URL(prefix).origin || url.search || url.hash)
|
|
3835
|
+
throw new Error("Resource outside the Potree bundle");
|
|
3836
|
+
const path = decodeURIComponent(url.pathname.slice(1));
|
|
3837
|
+
validatePath(path);
|
|
3838
|
+
const entry = entries.get(path);
|
|
3839
|
+
if (!entry) throw new Error(`Missing Potree bundle resource: ${path}`);
|
|
3840
|
+
const range = new Headers(request.headers).get("range");
|
|
3841
|
+
const match = range?.match(/^bytes=(\d+)-(\d+)$/);
|
|
3842
|
+
if (range && !match) throw new RangeError("Unsupported Potree bundle byte range");
|
|
3843
|
+
const begin = match ? Number(match[1]) : 0;
|
|
3844
|
+
const end = match ? Number(match[2]) + 1 : Number(entry.size);
|
|
3845
|
+
if (!Number.isSafeInteger(begin) || !Number.isSafeInteger(end) || begin < 0 || end < begin || BigInt(end) > entry.size || end - begin > maxResourceBytes)
|
|
3846
|
+
throw new RangeError("Potree bundle resource/range budget exceeded");
|
|
3847
|
+
const headers = new Headers({
|
|
3848
|
+
"content-length": String(end - begin),
|
|
3849
|
+
"accept-ranges": "bytes"
|
|
3850
|
+
});
|
|
3851
|
+
if (match) headers.set("content-range", `bytes ${begin}-${end - 1}/${entry.size}`);
|
|
3852
|
+
const bytes = match ? await archive.readRange(entry, {
|
|
3853
|
+
start: BigInt(begin),
|
|
3854
|
+
end: BigInt(end),
|
|
3855
|
+
signal: request.signal ?? void 0
|
|
3856
|
+
}) : await archive.read(entry, { signal: request.signal ?? void 0 });
|
|
3857
|
+
const response = new Response(bytes, {
|
|
3858
|
+
status: match ? 206 : 200,
|
|
3859
|
+
headers
|
|
3860
|
+
});
|
|
3861
|
+
Object.defineProperty(response, "url", { value: url.href });
|
|
3862
|
+
signal.throwIfAborted();
|
|
3863
|
+
return response;
|
|
3864
|
+
};
|
|
3865
|
+
source = import_potree.PotreeSourceLoader.createDataSource(`${prefix}${metadataPath}`, {
|
|
3866
|
+
...options.sourceOptions,
|
|
3867
|
+
core: { ...options.sourceOptions?.core, fetch: fetchResource },
|
|
3868
|
+
potree: { ...options.sourceOptions?.potree, signal }
|
|
3869
|
+
});
|
|
3870
|
+
await source.initialize();
|
|
3871
|
+
signal.throwIfAborted();
|
|
3872
|
+
return {
|
|
3873
|
+
source,
|
|
3874
|
+
async close() {
|
|
3875
|
+
controller.abort();
|
|
3876
|
+
source?.close();
|
|
3877
|
+
await archive?.close();
|
|
3878
|
+
}
|
|
3879
|
+
};
|
|
3880
|
+
} catch (error) {
|
|
3881
|
+
controller.abort();
|
|
3882
|
+
source?.close();
|
|
3883
|
+
await archive?.close();
|
|
3884
|
+
throw error;
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
function extractMeshVertices(mesh, options) {
|
|
3888
|
+
validateLimit(options.maxPoints);
|
|
3889
|
+
const position = mesh.attributes.POSITION;
|
|
3890
|
+
const pointCount = position?.value.length / 3;
|
|
3891
|
+
if (mesh.topology !== "triangle-list" || mesh.mode !== 4 || !position || position.size !== 3 || position.normalized || position.transform || position.byteStride || position.byteOffset || !(position.value instanceof Float32Array || position.value instanceof Float64Array) || !Number.isSafeInteger(pointCount) || pointCount > options.maxPoints || position.value.some((value) => !Number.isFinite(value)))
|
|
3892
|
+
throw new Error(
|
|
3893
|
+
"Mesh vertex harvesting requires packed finite triangle XYZ within the point budget"
|
|
3894
|
+
);
|
|
3895
|
+
const attributes = { POSITION: position };
|
|
3896
|
+
for (const name of options.attributes) {
|
|
3897
|
+
const attribute = mesh.attributes[name];
|
|
3898
|
+
if (!attribute || attribute.transform || attribute.byteStride || attribute.byteOffset || !Number.isSafeInteger(attribute.size) || attribute.size < 1 || attribute.value.length !== pointCount * attribute.size)
|
|
3899
|
+
throw new Error(`Invalid selected mesh vertex attribute: ${name}`);
|
|
3900
|
+
attributes[name] = attribute;
|
|
3901
|
+
}
|
|
3902
|
+
return { topology: "point-list", mode: 0, attributes, schema: (0, import_schema_utils5.deduceMeshSchema)(attributes) };
|
|
3903
|
+
}
|
|
3904
|
+
function validatePath(path) {
|
|
3905
|
+
if (!path || !/^[A-Za-z0-9_.\/-]+$/.test(path) || path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
3906
|
+
throw new Error("Potree bundle paths must be canonical relative ASCII paths");
|
|
3907
|
+
}
|
|
3908
|
+
function validateLimit(value) {
|
|
3909
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
3910
|
+
throw new RangeError("Point file limits must be positive safe integers");
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
// src/v5/point-cloud-potree.ts
|
|
3914
|
+
var import_schema_utils6 = require("@loaders.gl/schema-utils");
|
|
3915
|
+
var import_potree2 = require("@loaders.gl/potree");
|
|
3916
|
+
async function convertPointCloudToPotree(input, options) {
|
|
3917
|
+
if (typeof options.projection !== "string" || !options.projection.trim()) {
|
|
3918
|
+
throw new Error("Potree authoring requires an explicit native CRS declaration");
|
|
3919
|
+
}
|
|
3920
|
+
for (const limit of [
|
|
3921
|
+
options.maxGatheredBytes,
|
|
3922
|
+
options.maxOutputBytes,
|
|
3923
|
+
options.archive?.maxArchiveBytes
|
|
3924
|
+
]) {
|
|
3925
|
+
if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 1)) {
|
|
3926
|
+
throw new RangeError("Potree authoring byte limits must be positive safe integers");
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
if (options.maxGatheredBytes === void 0 || options.maxOutputBytes === void 0) {
|
|
3930
|
+
throw new RangeError("Potree authoring requires gathered and encoded byte limits");
|
|
3931
|
+
}
|
|
3932
|
+
options.signal?.throwIfAborted();
|
|
3933
|
+
const tiler = new import_schema_utils6.PointCloudTiler(input, {
|
|
3934
|
+
...options.tiling,
|
|
3935
|
+
maxNodes: Math.min(options.tiling?.maxNodes ?? 1e5, options.potree?.maxNodes ?? 1e5),
|
|
3936
|
+
maxTileBytes: Math.min(
|
|
3937
|
+
options.tiling?.maxTileBytes ?? 64 * 1024 * 1024,
|
|
3938
|
+
options.maxGatheredBytes
|
|
3939
|
+
),
|
|
3940
|
+
signal: options.signal
|
|
3941
|
+
});
|
|
3942
|
+
const nodes = [];
|
|
3943
|
+
let gatheredBytes = 0;
|
|
3944
|
+
let pointByteLength = 0;
|
|
3945
|
+
let pointCount;
|
|
3946
|
+
try {
|
|
3947
|
+
const pending = [await tiler.getRootNode()];
|
|
3948
|
+
pointCount = tiler.pointCount;
|
|
3949
|
+
while (pending.length) {
|
|
3950
|
+
options.signal?.throwIfAborted();
|
|
3951
|
+
const node = pending.pop();
|
|
3952
|
+
if (node.pointCount * pointByteLength > options.maxGatheredBytes - gatheredBytes) {
|
|
3953
|
+
throw new RangeError("Potree gathered node byte budget exceeded");
|
|
3954
|
+
}
|
|
3955
|
+
const mesh = await tiler.getTileMesh(node.id);
|
|
3956
|
+
const nodeBytes = Array.from(
|
|
3957
|
+
new Set(Object.values(mesh.attributes).map((attribute) => attribute.value.buffer))
|
|
3958
|
+
).reduce((bytes, buffer) => bytes + buffer.byteLength, 0);
|
|
3959
|
+
if (nodeBytes > options.maxGatheredBytes - gatheredBytes) {
|
|
3960
|
+
throw new RangeError("Potree gathered node byte budget exceeded");
|
|
3961
|
+
}
|
|
3962
|
+
if (node.pointCount) pointByteLength = nodeBytes / node.pointCount;
|
|
3963
|
+
gatheredBytes += nodeBytes;
|
|
3964
|
+
nodes.push({ id: node.id, mesh });
|
|
3965
|
+
const children = await tiler.getChildNodes(node.id);
|
|
3966
|
+
for (let index = children.length - 1; index >= 0; index--) pending.push(children[index]);
|
|
3967
|
+
}
|
|
3968
|
+
} finally {
|
|
3969
|
+
tiler.close();
|
|
3970
|
+
}
|
|
3971
|
+
const dataset = await (0, import_potree2.encodePotreeDataset)(nodes, {
|
|
3972
|
+
potree: {
|
|
3973
|
+
...options.potree,
|
|
3974
|
+
projection: options.projection,
|
|
3975
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
3976
|
+
signal: options.signal
|
|
3977
|
+
}
|
|
3978
|
+
});
|
|
3979
|
+
const archive = options.archive ? await encodePotreeDatasetArchive(dataset, options.archive.maxArchiveBytes, options.signal) : void 0;
|
|
3980
|
+
options.signal?.throwIfAborted();
|
|
3981
|
+
return { pointCount, nodeCount: nodes.length, dataset, archive };
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
// src/v5/mesh-hierarchy.ts
|
|
3985
|
+
var import_core30 = require("@math.gl/core");
|
|
3986
|
+
var import_tiles3 = require("@loaders.gl/tiles");
|
|
3987
|
+
var import_core31 = require("@loaders.gl/tile-converter/v5/core");
|
|
3988
|
+
function prepareMeshHierarchy(inspection, resourceIds) {
|
|
3989
|
+
const rejectHierarchy = (message) => {
|
|
3990
|
+
throw new import_core31.TileConversionError("MESH_HIERARCHY_UNSUPPORTED", message);
|
|
3991
|
+
};
|
|
3992
|
+
if (!inspection.documents?.length || inspection.documents.length > 32)
|
|
3993
|
+
rejectHierarchy(
|
|
3994
|
+
"Hierarchy preservation requires expanded metadata inspection of at most 32 explicit document placements"
|
|
3995
|
+
);
|
|
3996
|
+
if (!resourceIds.length || resourceIds.length > 64 || new Set(resourceIds).size !== resourceIds.length || resourceIds.length !== inspection.resources.length || inspection.resources.some((resource) => !resourceIds.includes(resource.resourceId)))
|
|
3997
|
+
rejectHierarchy(
|
|
3998
|
+
"Hierarchy preservation requires every content placement, without duplicates (at most 64)"
|
|
3999
|
+
);
|
|
4000
|
+
const entryTransform = new import_core30.Matrix4();
|
|
4001
|
+
if (inspection.documents[0].parentResourceId !== void 0 || !Array.isArray(inspection.documents[0].parentTransform) || inspection.documents[0].parentTransform.length !== 16 || inspection.documents[0].parentTransform.some((value, index) => value !== entryTransform[index]))
|
|
4002
|
+
rejectHierarchy("The entry document must have identity parent placement");
|
|
4003
|
+
const documents = inspection.documents.map((placement) => placement.tileset);
|
|
4004
|
+
const document = documents[0];
|
|
4005
|
+
const externalDocuments = /* @__PURE__ */ new Map();
|
|
4006
|
+
for (let index = 1; index < inspection.documents.length; index++) {
|
|
4007
|
+
const identifier = inspection.documents[index].parentResourceId;
|
|
4008
|
+
if (!identifier || externalDocuments.has(identifier))
|
|
4009
|
+
rejectHierarchy(
|
|
4010
|
+
"Every external document requires a distinct inspected parent content placement"
|
|
4011
|
+
);
|
|
4012
|
+
externalDocuments.set(identifier, index);
|
|
4013
|
+
}
|
|
4014
|
+
for (const document2 of documents) {
|
|
4015
|
+
if (document2.metadata || document2.groups || document2.schema || document2.schemaUri || Object.keys(document2.extensions ?? {}).some((name) => name !== "EXT_geospatial_crs") || (document2.extensionsRequired ?? []).some((name) => name !== "EXT_geospatial_crs"))
|
|
4016
|
+
rejectHierarchy(
|
|
4017
|
+
"Dataset metadata and unmapped extensions require a separate hierarchy profile"
|
|
4018
|
+
);
|
|
4019
|
+
if (!["1.0", "1.1"].includes(document2.asset?.version ?? ""))
|
|
4020
|
+
rejectHierarchy("Hierarchy preservation requires a 3D Tiles 1.0 or 1.1 document");
|
|
4021
|
+
if (document2.geometricError !== void 0 && (typeof document2.geometricError !== "number" || !Number.isFinite(document2.geometricError) || document2.geometricError < 0))
|
|
4022
|
+
rejectHierarchy("Dataset geometricError must be finite nonnegative meters");
|
|
4023
|
+
}
|
|
4024
|
+
const spatialReferences = documents.map(
|
|
4025
|
+
(document2) => (0, import_tiles3.get3DTilesSpatialReference)(document2)
|
|
4026
|
+
);
|
|
4027
|
+
const spatialReference = (0, import_core31.createTiles3DConversionSpatialContext)(
|
|
4028
|
+
spatialReferences[0]
|
|
4029
|
+
).spatialReference;
|
|
4030
|
+
if (spatialReference.status !== "native" || spatialReference.sourceCrs !== "EPSG:4978" || spatialReference.heightReference !== "ellipsoidal" || spatialReference.axisOrder !== "xyz" || spatialReference.verticalUnitScale !== 1 || spatialReference.units?.some((unit) => unit !== "meter"))
|
|
4031
|
+
rejectHierarchy(
|
|
4032
|
+
"Hierarchy preservation requires native EPSG:4978 xyz meters and ellipsoidal heights"
|
|
4033
|
+
);
|
|
4034
|
+
for (const reference of spatialReferences.slice(1)) {
|
|
4035
|
+
const inherited = reference.crs.state === "absent" && reference.coordinateEpoch === void 0 && !reference.verticalCrs && !reference.warnings.length;
|
|
4036
|
+
const context = (0, import_core31.createTiles3DConversionSpatialContext)(inherited ? spatialReference : reference);
|
|
4037
|
+
if (context.spatialReference.status !== "native" || context.spatialReference.sourceCrs !== spatialReference.sourceCrs || context.spatialReference.heightReference !== spatialReference.heightReference || context.spatialReference.axisOrder !== spatialReference.axisOrder || context.spatialReference.verticalUnitScale !== 1 || context.spatialReference.units?.some((unit) => unit !== "meter") || context.spatialReference.coordinateEpoch !== spatialReference.coordinateEpoch)
|
|
4038
|
+
rejectHierarchy("External document CRS and coordinate epoch must match the entry frame");
|
|
4039
|
+
}
|
|
4040
|
+
const nodes = [];
|
|
4041
|
+
const visitedDocuments = /* @__PURE__ */ new Set([0]);
|
|
4042
|
+
const stack = [
|
|
4043
|
+
{
|
|
4044
|
+
tile: document.root,
|
|
4045
|
+
documentIndex: 0,
|
|
4046
|
+
prefix: "",
|
|
4047
|
+
path: [],
|
|
4048
|
+
parent: -1,
|
|
4049
|
+
transform: new import_core30.Matrix4(),
|
|
4050
|
+
refine: void 0
|
|
4051
|
+
}
|
|
4052
|
+
];
|
|
4053
|
+
while (stack.length) {
|
|
4054
|
+
const {
|
|
4055
|
+
tile,
|
|
4056
|
+
path,
|
|
4057
|
+
parent,
|
|
4058
|
+
documentIndex,
|
|
4059
|
+
prefix,
|
|
4060
|
+
transform: parentTransform,
|
|
4061
|
+
refine: inheritedRefine
|
|
4062
|
+
} = stack.pop();
|
|
4063
|
+
const document2 = documents[documentIndex];
|
|
4064
|
+
if (nodes.length >= 256) rejectHierarchy("Hierarchy preservation supports at most 256 tiles");
|
|
4065
|
+
if (!tile || typeof tile !== "object" || Array.isArray(tile) || tile.implicitTiling !== void 0 || tile.extensions !== void 0 || tile.metadata !== void 0 || tile.viewerRequestVolume !== void 0 || tile.content !== void 0 && tile.contents !== void 0 || tile.contents !== void 0 && (!Array.isArray(tile.contents) || !tile.contents.length || document2.asset?.version !== "1.1") || tile.children !== void 0 && !Array.isArray(tile.children))
|
|
4066
|
+
rejectHierarchy(
|
|
4067
|
+
"Hierarchy tiles require explicit children, exclusive content/contents declarations and no unmapped metadata, extensions or request volumes"
|
|
4068
|
+
);
|
|
4069
|
+
const refine = tile.refine ?? inheritedRefine;
|
|
4070
|
+
if (refine !== "ADD" && refine !== "REPLACE")
|
|
4071
|
+
rejectHierarchy("Root refinement must be explicit ADD or REPLACE; children may inherit");
|
|
4072
|
+
const matrix = tile.transform;
|
|
4073
|
+
if (matrix !== void 0 && (!Array.isArray(matrix) || matrix.length !== 16 || matrix.some((value) => typeof value !== "number" || !Number.isFinite(value)) || matrix[3] !== 0 || matrix[7] !== 0 || matrix[11] !== 0 || matrix[15] !== 1))
|
|
4074
|
+
rejectHierarchy("Hierarchy transforms must be finite affine matrices");
|
|
4075
|
+
const transform = new import_core30.Matrix4(parentTransform).multiplyRight(matrix ?? new import_core30.Matrix4());
|
|
4076
|
+
const columns = [0, 1, 2].map(
|
|
4077
|
+
(column) => [0, 1, 2].reduce((sum, row) => sum + Math.abs(transform[column * 4 + row]), 0)
|
|
4078
|
+
);
|
|
4079
|
+
const rows = [0, 1, 2].map(
|
|
4080
|
+
(row) => [0, 1, 2].reduce((sum, column) => sum + Math.abs(transform[column * 4 + row]), 0)
|
|
4081
|
+
);
|
|
4082
|
+
const scale = document2.asset?.version === "1.0" ? 1 : Math.sqrt(Math.max(...columns)) * Math.sqrt(Math.max(...rows));
|
|
4083
|
+
const geometricError2 = (tile.geometricError ?? NaN) * scale;
|
|
4084
|
+
if (typeof tile.geometricError !== "number" || (tile.geometricError ?? -1) < 0 || !Number.isFinite(geometricError2) || Array.from(transform).some((value) => !Number.isFinite(value)))
|
|
4085
|
+
rejectHierarchy("Every hierarchy tile must declare a finite nonnegative metric error");
|
|
4086
|
+
const contents = tile.contents ?? (tile.content ? [tile.content] : []);
|
|
4087
|
+
const identifiers = contents.map(
|
|
4088
|
+
(_, contentIndex) => `${prefix}tile-${path.length ? path.join("-") : "root"}-content-${contentIndex}`
|
|
4089
|
+
);
|
|
4090
|
+
const externalIndexes = identifiers.map((identifier) => externalDocuments.get(identifier));
|
|
4091
|
+
const externalIndex = externalIndexes.find((index2) => index2 !== void 0);
|
|
4092
|
+
for (const [contentIndex, content] of contents.entries()) {
|
|
4093
|
+
if (!content || typeof content !== "object" || Array.isArray(content) || content.metadata !== void 0 || content.group !== void 0 || content.extensions !== void 0)
|
|
4094
|
+
rejectHierarchy("Content metadata, groups and extensions require a separate mapping");
|
|
4095
|
+
if (externalIndexes[contentIndex] === void 0 && !inspection.resources.some(
|
|
4096
|
+
(resource) => resource.resourceId === identifiers[contentIndex] && resource.documentIndex === documentIndex && resource.uri === (content.uri ?? content.url) && resource.tilePath.length === path.length && resource.tilePath.every((value, index2) => value === path[index2])
|
|
4097
|
+
))
|
|
4098
|
+
rejectHierarchy("Hierarchy content must match the inspected placement");
|
|
4099
|
+
}
|
|
4100
|
+
const resourceId = identifiers[0];
|
|
4101
|
+
if (externalIndex !== void 0) {
|
|
4102
|
+
if (contents.length !== 1 || tile.contents !== void 0 || tile.children?.length || visitedDocuments.has(externalIndex))
|
|
4103
|
+
rejectHierarchy(
|
|
4104
|
+
"External references require a single content, no source children and a unique document placement"
|
|
4105
|
+
);
|
|
4106
|
+
visitedDocuments.add(externalIndex);
|
|
4107
|
+
const placement = inspection.documents[externalIndex];
|
|
4108
|
+
if (!Array.isArray(placement.parentTransform) || placement.parentTransform.length !== 16 || placement.parentTransform.some((value, index2) => value !== transform[index2]))
|
|
4109
|
+
rejectHierarchy("External document placement must match its referencing tile transform");
|
|
4110
|
+
const contentUri = tile.content?.uri ?? tile.content?.url;
|
|
4111
|
+
if (!contentUri || !new URL(contentUri, inspection.documents[documentIndex].url).pathname.toLowerCase().endsWith(".json"))
|
|
4112
|
+
rejectHierarchy("External documents require an inspected .json content reference");
|
|
4113
|
+
}
|
|
4114
|
+
const index = nodes.length;
|
|
4115
|
+
nodes.push({
|
|
4116
|
+
children: [],
|
|
4117
|
+
resourceIds: externalIndex === void 0 ? identifiers : [],
|
|
4118
|
+
geometricError: Math.max(
|
|
4119
|
+
geometricError2,
|
|
4120
|
+
externalIndex === void 0 ? 0 : documents[externalIndex].geometricError ?? 0
|
|
4121
|
+
),
|
|
4122
|
+
refine
|
|
4123
|
+
});
|
|
4124
|
+
if (parent !== -1) nodes[parent].children.push(index);
|
|
4125
|
+
if (externalIndex !== void 0) {
|
|
4126
|
+
stack.push({
|
|
4127
|
+
tile: documents[externalIndex].root,
|
|
4128
|
+
path: [],
|
|
4129
|
+
parent: index,
|
|
4130
|
+
documentIndex: externalIndex,
|
|
4131
|
+
prefix: resourceId + "/",
|
|
4132
|
+
transform,
|
|
4133
|
+
refine: void 0
|
|
4134
|
+
});
|
|
4135
|
+
}
|
|
4136
|
+
if (nodes.length + stack.length + (tile.children?.length ?? 0) > 256)
|
|
4137
|
+
rejectHierarchy("Hierarchy preservation supports at most 256 tiles");
|
|
4138
|
+
for (let child = (tile.children?.length ?? 0) - 1; child >= 0; child--)
|
|
4139
|
+
stack.push({
|
|
4140
|
+
tile: tile.children[child],
|
|
4141
|
+
documentIndex,
|
|
4142
|
+
prefix,
|
|
4143
|
+
path: [...path, child],
|
|
4144
|
+
parent: index,
|
|
4145
|
+
transform,
|
|
4146
|
+
refine
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
4149
|
+
if (visitedDocuments.size !== documents.length || nodes.reduce((count, node) => count + node.resourceIds.length, 0) !== resourceIds.length || nodes.some((node) => !node.resourceIds.length && !node.children.length))
|
|
4150
|
+
rejectHierarchy("Contentless leaves or unmatched content placements cannot be preserved");
|
|
4151
|
+
const geometricError = document.geometricError ?? nodes[0].geometricError;
|
|
4152
|
+
if (typeof geometricError !== "number" || !Number.isFinite(geometricError) || geometricError < 0)
|
|
4153
|
+
rejectHierarchy("Dataset geometricError must be finite nonnegative meters");
|
|
4154
|
+
return {
|
|
4155
|
+
nodes,
|
|
4156
|
+
geometricError: Math.max(geometricError, nodes[0].geometricError),
|
|
4157
|
+
coordinateEpoch: spatialReference.coordinateEpoch
|
|
4158
|
+
};
|
|
4159
|
+
}
|
|
4160
|
+
function createMeshHierarchyTilesetSink(options) {
|
|
4161
|
+
const hierarchy = prepareMeshHierarchy(options.inspection, options.resourceIds);
|
|
4162
|
+
const extensions = structuredClone(
|
|
4163
|
+
options.inspection.tileset.extensions
|
|
4164
|
+
);
|
|
4165
|
+
const extensionsRequired = structuredClone(
|
|
4166
|
+
options.inspection.tileset.extensionsRequired
|
|
4167
|
+
);
|
|
4168
|
+
const { maxPositionError } = options;
|
|
4169
|
+
if (!Number.isFinite(maxPositionError) || maxPositionError < 0 || hierarchy.nodes.some((node) => !Number.isFinite(node.geometricError + maxPositionError)) || !Number.isFinite(hierarchy.geometricError + maxPositionError))
|
|
4170
|
+
throw new import_core31.TileConversionError(
|
|
4171
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
4172
|
+
"Hierarchy position allowance and resulting errors must be finite nonnegative meters"
|
|
4173
|
+
);
|
|
4174
|
+
const memory = (0, import_core31.createBoundedMemoryTileConversionSink)(options);
|
|
4175
|
+
const placements = /* @__PURE__ */ new Map();
|
|
4176
|
+
const completedContents = /* @__PURE__ */ new Set();
|
|
4177
|
+
const primitiveIdentifiers = /* @__PURE__ */ new Set();
|
|
4178
|
+
let state = "open";
|
|
4179
|
+
let completed = false;
|
|
4180
|
+
return {
|
|
4181
|
+
/** Exposes files only after complete finalization. */
|
|
4182
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
4183
|
+
/** Retains ordered codec outputs and requires a terminal primitive for each content. */
|
|
4184
|
+
async write(resource, signal) {
|
|
4185
|
+
signal?.throwIfAborted();
|
|
4186
|
+
const identifier = resource.contentResourceId;
|
|
4187
|
+
const { index: primitiveIndex, last: lastPrimitive } = resource.contentPrimitive ?? {
|
|
4188
|
+
index: 0,
|
|
4189
|
+
last: true
|
|
4190
|
+
};
|
|
4191
|
+
if (state !== "open" || !identifier || completedContents.has(identifier) || primitiveIdentifiers.has(resource.id) || primitiveIdentifiers.size >= 64 || !Number.isSafeInteger(primitiveIndex) || primitiveIndex !== (placements.get(identifier)?.length ?? 0) || typeof lastPrimitive !== "boolean" || !hierarchy.nodes.some((node2) => node2.resourceIds.includes(identifier)))
|
|
4192
|
+
throw new import_core31.TileConversionError(
|
|
4193
|
+
"MESH_HIERARCHY_PLACEMENT_INVALID",
|
|
4194
|
+
"Hierarchy writes require ordered, distinct primitives for declared contents before close (at most 64)"
|
|
4195
|
+
);
|
|
4196
|
+
if (resource.spatialReference.coordinateEpoch !== hierarchy.coordinateEpoch)
|
|
4197
|
+
throw new import_core31.TileConversionError(
|
|
4198
|
+
"MESH_SOURCE_EPOCH_MISMATCH",
|
|
4199
|
+
"Hierarchy output coordinate epoch must match its source"
|
|
4200
|
+
);
|
|
4201
|
+
const tile = createMeshTileset(resource, maxPositionError).root;
|
|
4202
|
+
const nodeIndex = hierarchy.nodes.findIndex((node2) => node2.resourceIds.includes(identifier));
|
|
4203
|
+
const node = hierarchy.nodes[nodeIndex];
|
|
4204
|
+
const sibling = node.resourceIds.map((identifier2) => placements.get(identifier2)?.[0]).find(Boolean);
|
|
4205
|
+
if (sibling && sibling.origin.some((value, axis) => value !== resource.origin[axis]))
|
|
4206
|
+
throw new import_core31.TileConversionError(
|
|
4207
|
+
"MESH_HIERARCHY_PLACEMENT_INVALID",
|
|
4208
|
+
"Contents on one tile must share an encoded origin"
|
|
4209
|
+
);
|
|
4210
|
+
const suffix = node.resourceIds.length > 1 ? "-" + node.resourceIds.indexOf(identifier) : "";
|
|
4211
|
+
const primitiveSuffix = primitiveIndex > 0 || !lastPrimitive ? "-primitive-" + primitiveIndex : "";
|
|
4212
|
+
const uri = `meshes/${nodeIndex}${suffix}${primitiveSuffix}.glb`;
|
|
4213
|
+
state = "writing";
|
|
4214
|
+
await memory.write(
|
|
4215
|
+
{ resourceId: uri, parts: [resource.glb], contentType: "model/gltf-binary" },
|
|
4216
|
+
signal
|
|
4217
|
+
);
|
|
4218
|
+
const contentPlacements = placements.get(identifier) ?? [];
|
|
4219
|
+
contentPlacements.push({
|
|
4220
|
+
transform: [...tile.transform],
|
|
4221
|
+
origin: [...resource.origin],
|
|
4222
|
+
localBoundingBox: resource.localBoundingBox.map((bound) => [...bound]),
|
|
4223
|
+
uri
|
|
4224
|
+
});
|
|
4225
|
+
placements.set(identifier, contentPlacements);
|
|
4226
|
+
primitiveIdentifiers.add(resource.id);
|
|
4227
|
+
if (lastPrimitive) completedContents.add(identifier);
|
|
4228
|
+
state = "open";
|
|
4229
|
+
},
|
|
4230
|
+
/** Publishes original topology only after every source content has completed. */
|
|
4231
|
+
async finalize(report) {
|
|
4232
|
+
if (state !== "open" || completedContents.size !== hierarchy.nodes.reduce((count, node) => count + node.resourceIds.length, 0))
|
|
4233
|
+
throw new import_core31.TileConversionError(
|
|
4234
|
+
"MESH_HIERARCHY_INCOMPLETE",
|
|
4235
|
+
"Finalization requires every declared hierarchy content"
|
|
4236
|
+
);
|
|
4237
|
+
state = "writing";
|
|
4238
|
+
const tiles = new Array(hierarchy.nodes.length);
|
|
4239
|
+
const bounds = new Array(hierarchy.nodes.length);
|
|
4240
|
+
const transforms = hierarchy.nodes.map((node) => {
|
|
4241
|
+
const placement = placements.get(node.resourceIds[0])?.[0];
|
|
4242
|
+
return placement ? new import_core30.Matrix4(placement.transform) : new import_core30.Matrix4();
|
|
4243
|
+
});
|
|
4244
|
+
for (let index = hierarchy.nodes.length - 1; index >= 0; index--) {
|
|
4245
|
+
const node = hierarchy.nodes[index];
|
|
4246
|
+
const placement = placements.get(node.resourceIds[0])?.[0];
|
|
4247
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
4248
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
4249
|
+
const contributors = node.children.map((child) => bounds[child]);
|
|
4250
|
+
const nodePlacements = node.resourceIds.flatMap((identifier) => placements.get(identifier));
|
|
4251
|
+
for (const placement2 of nodePlacements) {
|
|
4252
|
+
contributors.push(
|
|
4253
|
+
placement2.localBoundingBox.map(
|
|
4254
|
+
(bound, boundIndex) => bound.map((value, axis) => {
|
|
4255
|
+
const padding = 8 * Number.EPSILON * Math.max(
|
|
4256
|
+
1,
|
|
4257
|
+
Math.abs(placement2.origin[axis]),
|
|
4258
|
+
...placement2.localBoundingBox.map((limit) => Math.abs(limit[axis]))
|
|
4259
|
+
);
|
|
4260
|
+
return value + placement2.origin[axis] + (boundIndex ? padding : -padding);
|
|
4261
|
+
})
|
|
4262
|
+
)
|
|
4263
|
+
);
|
|
4264
|
+
}
|
|
4265
|
+
for (const [lower, upper] of contributors)
|
|
4266
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
4267
|
+
minimum[axis] = Math.min(minimum[axis], lower[axis]);
|
|
4268
|
+
maximum[axis] = Math.max(maximum[axis], upper[axis]);
|
|
4269
|
+
}
|
|
4270
|
+
if ([...minimum, ...maximum].some((value) => !Number.isFinite(value)))
|
|
4271
|
+
throw new import_core31.TileConversionError(
|
|
4272
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
4273
|
+
"Hierarchy absolute bounds must remain finite"
|
|
4274
|
+
);
|
|
4275
|
+
bounds[index] = [minimum, maximum];
|
|
4276
|
+
const inverse = new import_core30.Matrix4(transforms[index]).invert();
|
|
4277
|
+
const center = Array.from(
|
|
4278
|
+
inverse.transformAsPoint(minimum.map((value, axis) => value / 2 + maximum[axis] / 2))
|
|
4279
|
+
);
|
|
4280
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
4281
|
+
const halfAxes = [0, 1, 2].flatMap(
|
|
4282
|
+
(axis) => Array.from(
|
|
4283
|
+
inverse.transformAsVector(
|
|
4284
|
+
halfSize.map((value, coordinate) => coordinate === axis ? value : 0)
|
|
4285
|
+
)
|
|
4286
|
+
)
|
|
4287
|
+
);
|
|
4288
|
+
if ([...center, ...halfAxes].some((value) => !Number.isFinite(value)))
|
|
4289
|
+
throw new import_core31.TileConversionError(
|
|
4290
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
4291
|
+
"Hierarchy local bounds must remain finite"
|
|
4292
|
+
);
|
|
4293
|
+
tiles[index] = {
|
|
4294
|
+
transform: Array.from(transforms[index]),
|
|
4295
|
+
boundingVolume: { box: [...center, ...halfAxes] },
|
|
4296
|
+
geometricError: node.geometricError + maxPositionError,
|
|
4297
|
+
refine: node.refine,
|
|
4298
|
+
...nodePlacements.length > 1 ? { contents: nodePlacements.map((placement2) => ({ uri: placement2.uri })) } : placement ? { content: { uri: placement.uri } } : {},
|
|
4299
|
+
...node.children.length ? {
|
|
4300
|
+
children: node.children.map((child) => {
|
|
4301
|
+
tiles[child].transform = Array.from(
|
|
4302
|
+
new import_core30.Matrix4(inverse).multiplyRight(transforms[child])
|
|
4303
|
+
);
|
|
4304
|
+
if (tiles[child].transform.some((value) => !Number.isFinite(value)))
|
|
4305
|
+
throw new import_core31.TileConversionError(
|
|
4306
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
4307
|
+
"Relative hierarchy transforms must remain finite"
|
|
4308
|
+
);
|
|
4309
|
+
return tiles[child];
|
|
4310
|
+
})
|
|
4311
|
+
} : {}
|
|
4312
|
+
};
|
|
4313
|
+
}
|
|
4314
|
+
const json = JSON.stringify({
|
|
4315
|
+
asset: { version: "1.1" },
|
|
4316
|
+
...extensions && Object.keys(extensions).length ? { extensions, extensionsUsed: Object.keys(extensions) } : {},
|
|
4317
|
+
...extensionsRequired?.length ? { extensionsRequired } : {},
|
|
4318
|
+
geometricError: hierarchy.geometricError + maxPositionError,
|
|
4319
|
+
root: tiles[0]
|
|
4320
|
+
});
|
|
4321
|
+
await memory.write({
|
|
4322
|
+
resourceId: "tileset.json",
|
|
4323
|
+
parts: [json],
|
|
4324
|
+
contentType: "application/json"
|
|
4325
|
+
});
|
|
4326
|
+
await memory.finalize(report);
|
|
4327
|
+
state = "closed";
|
|
4328
|
+
completed = true;
|
|
4329
|
+
},
|
|
4330
|
+
/** Discards retained resources and hides all files after failure. */
|
|
4331
|
+
async abort(error) {
|
|
4332
|
+
state = "closed";
|
|
4333
|
+
completed = false;
|
|
4334
|
+
placements.clear();
|
|
4335
|
+
completedContents.clear();
|
|
4336
|
+
primitiveIdentifiers.clear();
|
|
4337
|
+
await memory.abort(error);
|
|
4338
|
+
}
|
|
4339
|
+
};
|
|
4340
|
+
}
|
|
4341
|
+
|
|
4342
|
+
// src/v5/selected-mesh-source.ts
|
|
4343
|
+
var import_core32 = require("@loaders.gl/core");
|
|
4344
|
+
var import_d_tiles2 = require("@loaders.gl/3d-tiles");
|
|
4345
|
+
var import_tiles4 = require("@loaders.gl/tiles");
|
|
4346
|
+
var import_core33 = require("@loaders.gl/tile-converter/v5/core");
|
|
4347
|
+
var import_adapters = require("@loaders.gl/tile-converter/v5/adapters");
|
|
4348
|
+
var SELECTED_MESH_LIMITS = {
|
|
4349
|
+
/** Root JSON plus selected content transport bytes; also the decoded geometry/image/feature byte gate. */
|
|
4350
|
+
maxInputBytes: 16 * 1024 * 1024,
|
|
4351
|
+
/** Maximum declared content placements inspected. */
|
|
4352
|
+
maxInputResources: 1e3,
|
|
4353
|
+
/** Maximum inspected root/external document placements. */
|
|
4354
|
+
maxTilesetDocuments: 32,
|
|
4355
|
+
/** Maximum explicitly selected content placements in one partial archive. */
|
|
4356
|
+
maxSelectedResources: 64,
|
|
4357
|
+
/** Maximum primitive placements authored across all selected contents. */
|
|
4358
|
+
maxMeshResources: 64,
|
|
4359
|
+
/** Maximum retained file bytes and final archive bytes. */
|
|
4360
|
+
maxOutputBytes: 32 * 1024 * 1024,
|
|
4361
|
+
/** Maximum measured reconstruction error in meters. */
|
|
4362
|
+
maxPositionError: 0.01
|
|
4363
|
+
};
|
|
4364
|
+
async function createSelectedMeshConversionSource(inspection, options) {
|
|
4365
|
+
const {
|
|
4366
|
+
resourceIds,
|
|
4367
|
+
signal,
|
|
4368
|
+
dracoLibraryOptions,
|
|
4369
|
+
features,
|
|
4370
|
+
preserveHierarchy,
|
|
4371
|
+
fetcher = fetch
|
|
4372
|
+
} = options;
|
|
4373
|
+
const maxInputBytes = options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes;
|
|
4374
|
+
if (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1 || maxInputBytes > SELECTED_MESH_LIMITS.maxInputBytes)
|
|
4375
|
+
throw new import_core33.TileConversionError(
|
|
4376
|
+
"INVALID_SELECTED_MESH_OPTIONS",
|
|
4377
|
+
"Selected mesh input budget must be a positive safe integer at most 16 MiB"
|
|
4378
|
+
);
|
|
4379
|
+
if (!resourceIds.length || resourceIds.length > SELECTED_MESH_LIMITS.maxSelectedResources || new Set(resourceIds).size !== resourceIds.length || resourceIds.some(
|
|
4380
|
+
(identifier) => !inspection.resources.some((resource) => resource.resourceId === identifier)
|
|
4381
|
+
))
|
|
4382
|
+
throw new Error("Select between 1 and 64 distinct content placements.");
|
|
4383
|
+
if (preserveHierarchy !== void 0 && typeof preserveHierarchy !== "boolean")
|
|
4384
|
+
throw new import_core33.TileConversionError(
|
|
4385
|
+
"MESH_HIERARCHY_UNSUPPORTED",
|
|
4386
|
+
"preserveHierarchy must be a boolean"
|
|
4387
|
+
);
|
|
4388
|
+
if (preserveHierarchy) prepareMeshHierarchy(inspection, resourceIds);
|
|
4389
|
+
const descriptors = inspection.resources.filter(
|
|
4390
|
+
(resource) => resourceIds.includes(resource.resourceId)
|
|
4391
|
+
);
|
|
4392
|
+
const selections = descriptors.map(
|
|
4393
|
+
(descriptor) => (0, import_core33.selectBrowserTilesetContent)(inspection, descriptor)
|
|
4394
|
+
);
|
|
4395
|
+
const selectedContents = new Map(
|
|
4396
|
+
descriptors.map((descriptor, index) => [descriptor.resourceId, selections[index]])
|
|
4397
|
+
);
|
|
4398
|
+
if (!preserveHierarchy && resourceIds.length > 1 && selections.some((selection) => !selection.isLeaf))
|
|
4399
|
+
throw new Error(
|
|
4400
|
+
"Multi-selection requires leaf contents; source LOD hierarchy is not exported."
|
|
4401
|
+
);
|
|
4402
|
+
signal.throwIfAborted();
|
|
4403
|
+
const boundedFetcher = (0, import_adapters.createTileConversionResourceFetcher)({
|
|
4404
|
+
maxInputBytes,
|
|
4405
|
+
initialInputBytes: inspection.tilesetBytes,
|
|
4406
|
+
fetcher,
|
|
4407
|
+
signal
|
|
4408
|
+
});
|
|
4409
|
+
const qualification = createSelectedRuntime(
|
|
4410
|
+
inspection.tileset,
|
|
4411
|
+
new Uint8Array(),
|
|
4412
|
+
signal,
|
|
4413
|
+
boundedFetcher,
|
|
4414
|
+
inspection.rootUrl,
|
|
4415
|
+
dracoLibraryOptions
|
|
4416
|
+
);
|
|
4417
|
+
let spatialContext;
|
|
4418
|
+
try {
|
|
4419
|
+
await qualification.tilesetInitializationPromise;
|
|
4420
|
+
const metadata = await (0, import_adapters.createMeshTilesetConversionSource)(qualification).inspect(signal);
|
|
4421
|
+
spatialContext = (0, import_core33.createTiles3DConversionSpatialContext)(metadata.spatialReference);
|
|
4422
|
+
} finally {
|
|
4423
|
+
qualification.destroy();
|
|
4424
|
+
}
|
|
4425
|
+
const rawSource = options.source ?? (0, import_core33.createBrowserTilesetConversionSource)({
|
|
4426
|
+
input: inspection.rootUrl,
|
|
4427
|
+
...SELECTED_MESH_LIMITS,
|
|
4428
|
+
maxInputBytes,
|
|
4429
|
+
fetcher: boundedFetcher
|
|
4430
|
+
});
|
|
4431
|
+
const selectedInspection = { ...inspection, resources: descriptors };
|
|
4432
|
+
const source = {
|
|
4433
|
+
/** Reuses inspected declarations without fetching the root again. */
|
|
4434
|
+
inspect: async (inspectionSignal) => {
|
|
4435
|
+
signal.throwIfAborted();
|
|
4436
|
+
inspectionSignal?.throwIfAborted();
|
|
4437
|
+
return selectedInspection;
|
|
4438
|
+
},
|
|
4439
|
+
/** Decodes and releases each selected placement in declaration order. */
|
|
4440
|
+
async *read(_inspection, readSignal) {
|
|
4441
|
+
const signal2 = readSignal ? AbortSignal.any([options.signal, readSignal]) : options.signal;
|
|
4442
|
+
signal2.throwIfAborted();
|
|
4443
|
+
let decodedBytes = 0;
|
|
4444
|
+
let primitiveCount = 0;
|
|
4445
|
+
const tileOrigins = /* @__PURE__ */ new Map();
|
|
4446
|
+
for await (const raw of rawSource.read(selectedInspection, signal2)) {
|
|
4447
|
+
const selection = selectedContents.get(raw.resourceId);
|
|
4448
|
+
if (!selection)
|
|
4449
|
+
throw new import_core33.TileConversionError(
|
|
4450
|
+
"INVALID_BROWSER_SELECTION",
|
|
4451
|
+
"Source yielded an uninspected content placement"
|
|
4452
|
+
);
|
|
4453
|
+
const contentUrl = raw.contentUrl ?? selection.contentUrl;
|
|
4454
|
+
const runtime = createSelectedRuntime(
|
|
4455
|
+
selection.tileset,
|
|
4456
|
+
raw.data,
|
|
4457
|
+
signal2,
|
|
4458
|
+
boundedFetcher,
|
|
4459
|
+
contentUrl,
|
|
4460
|
+
dracoLibraryOptions,
|
|
4461
|
+
spatialContext.spatialReference
|
|
4462
|
+
);
|
|
4463
|
+
try {
|
|
4464
|
+
await runtime.tilesetInitializationPromise;
|
|
4465
|
+
const meshSource = (0, import_adapters.createMeshTilesetConversionSource)(runtime, {
|
|
4466
|
+
unloadContent: true,
|
|
4467
|
+
features,
|
|
4468
|
+
readExternalResource: async (uri, _contentUri, resourceSignal) => {
|
|
4469
|
+
const response = await boundedFetcher(new URL(uri, contentUrl), {
|
|
4470
|
+
signal: resourceSignal
|
|
4471
|
+
});
|
|
4472
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
4473
|
+
}
|
|
4474
|
+
});
|
|
4475
|
+
const metadata = await meshSource.inspect(signal2);
|
|
4476
|
+
const selectedContext = (0, import_core33.createTiles3DConversionSpatialContext)(metadata.spatialReference);
|
|
4477
|
+
if (selectedContext.spatialReference.coordinateEpoch !== spatialContext.spatialReference.coordinateEpoch)
|
|
4478
|
+
throw new import_core33.TileConversionError(
|
|
4479
|
+
"MESH_SOURCE_EPOCH_MISMATCH",
|
|
4480
|
+
"Selected content coordinate epoch must match the root; epoch transformations are unsupported"
|
|
4481
|
+
);
|
|
4482
|
+
let meshCount = 0;
|
|
4483
|
+
for await (const resource of meshSource.read(metadata, signal2)) {
|
|
4484
|
+
meshCount++;
|
|
4485
|
+
if (++primitiveCount > SELECTED_MESH_LIMITS.maxMeshResources)
|
|
4486
|
+
throw new Error("Selected contents exceed the mesh primitive placement limit.");
|
|
4487
|
+
decodedBytes += measureMeshBytes(resource);
|
|
4488
|
+
if (decodedBytes > maxInputBytes)
|
|
4489
|
+
throw new import_core33.TileConversionError(
|
|
4490
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
4491
|
+
"Selected decoded geometry and encoded images exceed the aggregate input byte limit."
|
|
4492
|
+
);
|
|
4493
|
+
const tileIdentifier = raw.resourceId.replace(/content-\d+$/, "");
|
|
4494
|
+
if (preserveHierarchy && !tileOrigins.has(tileIdentifier))
|
|
4495
|
+
tileOrigins.set(tileIdentifier, [...resource.origin]);
|
|
4496
|
+
yield {
|
|
4497
|
+
...resource,
|
|
4498
|
+
origin: preserveHierarchy ? tileOrigins.get(tileIdentifier) : resource.origin,
|
|
4499
|
+
id: `${raw.resourceId}/${resource.id}`,
|
|
4500
|
+
contentResourceId: raw.resourceId
|
|
4501
|
+
};
|
|
4502
|
+
}
|
|
4503
|
+
if (!meshCount) throw new Error("Selected content has no mesh primitive.");
|
|
4504
|
+
} finally {
|
|
4505
|
+
runtime.destroy();
|
|
4506
|
+
}
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
};
|
|
4510
|
+
return {
|
|
4511
|
+
source,
|
|
4512
|
+
spatialContext,
|
|
4513
|
+
geometricError: Math.max(...selections.map((selection) => selection.geometricError)) + SELECTED_MESH_LIMITS.maxPositionError
|
|
4514
|
+
};
|
|
4515
|
+
}
|
|
4516
|
+
var SelectedTiles3DSource = class extends import_tiles4.Tiles3DSource {
|
|
4517
|
+
/** Parent frame; explicit, unknown or unresolved child declarations are never replaced. */
|
|
4518
|
+
inheritedSpatialReference;
|
|
4519
|
+
/** Creates a selected source with optional external-document frame inheritance. */
|
|
4520
|
+
constructor(input, options, inheritedSpatialReference) {
|
|
4521
|
+
super(input, options);
|
|
4522
|
+
this.inheritedSpatialReference = inheritedSpatialReference;
|
|
4523
|
+
}
|
|
4524
|
+
/** Returns source metadata with the same absent-CRS inheritance used by external tilesets. */
|
|
4525
|
+
getMetadata() {
|
|
4526
|
+
const metadata = super.getMetadata();
|
|
4527
|
+
const reference = metadata.spatialReference;
|
|
4528
|
+
if (this.inheritedSpatialReference && reference?.crs.state === "absent" && reference.coordinateEpoch === void 0 && !reference.verticalCrs && !reference.warnings.length)
|
|
4529
|
+
return { ...metadata, spatialReference: this.inheritedSpatialReference };
|
|
4530
|
+
return metadata;
|
|
4531
|
+
}
|
|
4532
|
+
};
|
|
4533
|
+
function createSelectedRuntime(document, data, signal, boundedFetcher, contentUrl, dracoLibraryOptions, inheritedSpatialReference) {
|
|
4534
|
+
const {
|
|
4535
|
+
modules,
|
|
4536
|
+
decoderProfile,
|
|
4537
|
+
CDN: contentDeliveryNetwork,
|
|
4538
|
+
useLocalLibraries
|
|
4539
|
+
} = dracoLibraryOptions;
|
|
4540
|
+
const loadOptions = {
|
|
4541
|
+
modules,
|
|
4542
|
+
core: {
|
|
4543
|
+
worker: false,
|
|
4544
|
+
fetch: boundedFetcher,
|
|
4545
|
+
CDN: contentDeliveryNetwork,
|
|
4546
|
+
useLocalLibraries
|
|
4547
|
+
},
|
|
4548
|
+
// Reuse the full decoder already bundled for output verification.
|
|
4549
|
+
draco: { decoderProfile },
|
|
4550
|
+
// Preserve authored UVs and the transform for the converter's qualified GLB writer.
|
|
4551
|
+
gltf: {
|
|
4552
|
+
loadImages: false,
|
|
4553
|
+
decompressMeshes: true,
|
|
4554
|
+
excludeExtensions: {
|
|
4555
|
+
KHR_texture_transform: false,
|
|
4556
|
+
// Only Draco input compression is qualified by this conversion profile.
|
|
4557
|
+
EXT_meshopt_compression: false,
|
|
4558
|
+
KHR_meshopt_compression: false
|
|
4559
|
+
}
|
|
4560
|
+
},
|
|
4561
|
+
"3d-tiles": { loadGLTF: true }
|
|
4562
|
+
};
|
|
4563
|
+
return new import_tiles4.Tileset3D(
|
|
4564
|
+
new SelectedTiles3DSource(
|
|
4565
|
+
{
|
|
4566
|
+
url: contentUrl,
|
|
4567
|
+
loader: import_d_tiles2.Tiles3DLoader,
|
|
4568
|
+
resolver: {
|
|
4569
|
+
/** Normalizes the selected hierarchy through the format loader, retaining ancestor transforms. */
|
|
4570
|
+
loadRoot: async (_url, loader, options) => (0, import_core32.parse)(new TextEncoder().encode(JSON.stringify(document)), loader, options),
|
|
4571
|
+
/** Decodes only the selected bounded payload; no sibling content can be requested. */
|
|
4572
|
+
loadResource: async (_url, loader, options) => {
|
|
4573
|
+
signal.throwIfAborted();
|
|
4574
|
+
return (0, import_core32.parse)(data, loader, options, {
|
|
4575
|
+
url: contentUrl,
|
|
4576
|
+
baseUrl: contentUrl.startsWith("blob:") ? contentUrl : new URL(".", contentUrl).href,
|
|
4577
|
+
fetch: boundedFetcher,
|
|
4578
|
+
coreApi: import_core32.coreApi,
|
|
4579
|
+
_parse: import_core32.parse
|
|
4580
|
+
});
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
},
|
|
4584
|
+
loadOptions,
|
|
4585
|
+
inheritedSpatialReference
|
|
4586
|
+
)
|
|
4587
|
+
);
|
|
4588
|
+
}
|
|
4589
|
+
function measureMeshBytes(resource) {
|
|
4590
|
+
const featureBytes = resource.features ? resource.features.triangleFeatureIndices.byteLength + resource.features.batches.reduce(
|
|
4591
|
+
(bytes, batch) => bytes + batch.data.schema.fields.reduce(
|
|
4592
|
+
(columnBytes, field) => columnBytes + (batch.data.getChild(field.name)?.byteLength ?? 0),
|
|
4593
|
+
0
|
|
4594
|
+
),
|
|
4595
|
+
0
|
|
4596
|
+
) : 0;
|
|
4597
|
+
return Object.values(resource.mesh.attributes).reduce(
|
|
4598
|
+
(bytes, attribute) => bytes + attribute.value.byteLength,
|
|
4599
|
+
(resource.mesh.indices?.value.byteLength ?? 0) + (resource.material?.baseColorTexture?.data.byteLength ?? 0) + featureBytes
|
|
4600
|
+
);
|
|
4601
|
+
}
|
|
4602
|
+
|
|
4603
|
+
// src/v5/archive-source.ts
|
|
4604
|
+
var import_loader_utils = require("@loaders.gl/loader-utils");
|
|
4605
|
+
var import_i3s3 = require("@loaders.gl/i3s");
|
|
4606
|
+
var import_d_tiles3 = require("@loaders.gl/3d-tiles");
|
|
4607
|
+
var import_core34 = require("@loaders.gl/tile-converter/v5/core");
|
|
4608
|
+
async function openTileConversionArchive(descriptor, maxInputBytes, signal, fetcher = fetch) {
|
|
4609
|
+
signal.throwIfAborted();
|
|
4610
|
+
if (!["slpk", "3tz"].includes(descriptor.format))
|
|
4611
|
+
throw new import_core34.TileConversionError("INVALID_ARCHIVE_SOURCE", "Select SLPK or 3TZ archive input");
|
|
4612
|
+
validateArchiveEntityTag(descriptor.identity?.etag);
|
|
4613
|
+
const transport = createTileConversionResourceFetcher({
|
|
4614
|
+
maxInputBytes,
|
|
4615
|
+
signal,
|
|
4616
|
+
fetcher: async (url, options) => {
|
|
4617
|
+
const response = await fetcher(url, options);
|
|
4618
|
+
if (response.status !== 206) {
|
|
4619
|
+
await response.body?.cancel();
|
|
4620
|
+
throw new Error("Remote conversion archives require HTTP 206 byte-range responses.");
|
|
4621
|
+
}
|
|
4622
|
+
try {
|
|
4623
|
+
validateArchiveResponseValidator(response.headers);
|
|
4624
|
+
} catch (error) {
|
|
4625
|
+
await response.body?.cancel(error);
|
|
4626
|
+
throw error;
|
|
4627
|
+
}
|
|
4628
|
+
return response;
|
|
4629
|
+
}
|
|
4630
|
+
});
|
|
4631
|
+
let file;
|
|
4632
|
+
let closePromise;
|
|
4633
|
+
async function closeFile() {
|
|
4634
|
+
if (file) {
|
|
4635
|
+
closePromise ??= file.close();
|
|
4636
|
+
await closePromise;
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
try {
|
|
4640
|
+
if (typeof descriptor.input === "string") {
|
|
4641
|
+
if (!["http:", "https:"].includes(new URL(descriptor.input).protocol))
|
|
4642
|
+
throw new Error("Use an HTTP(S) archive URL.");
|
|
4643
|
+
const remote = new import_loader_utils.HttpFile(descriptor.input, {
|
|
4644
|
+
fetch: (url, options) => transport(url, options),
|
|
4645
|
+
consistency: "strict",
|
|
4646
|
+
...descriptor.identity
|
|
4647
|
+
});
|
|
4648
|
+
file = remote;
|
|
4649
|
+
await remote.open(signal);
|
|
4650
|
+
} else file = new import_loader_utils.BlobFile(descriptor.input);
|
|
4651
|
+
const ownedFile = file;
|
|
4652
|
+
file = void 0;
|
|
4653
|
+
return await createTileConversionArchiveTransport(
|
|
4654
|
+
ownedFile,
|
|
4655
|
+
descriptor.format,
|
|
4656
|
+
maxInputBytes,
|
|
4657
|
+
signal,
|
|
4658
|
+
descriptor
|
|
4659
|
+
);
|
|
4660
|
+
} catch (error) {
|
|
4661
|
+
await closeFile();
|
|
4662
|
+
throw error;
|
|
4663
|
+
}
|
|
4664
|
+
}
|
|
4665
|
+
async function createTileConversionArchiveTransport(file, format, maxInputBytes, signal, descriptor) {
|
|
4666
|
+
const rootUrl = `https://conversion.invalid/input.${format}`;
|
|
4667
|
+
let closed = false;
|
|
4668
|
+
let releasePromise;
|
|
4669
|
+
let closePromise;
|
|
4670
|
+
const closeFile = () => {
|
|
4671
|
+
closed = true;
|
|
4672
|
+
closePromise ??= file.close();
|
|
4673
|
+
return closePromise;
|
|
4674
|
+
};
|
|
4675
|
+
const checkOpen = () => {
|
|
4676
|
+
signal.throwIfAborted();
|
|
4677
|
+
if (closed)
|
|
4678
|
+
throw new import_core34.TileConversionError("ARCHIVE_SOURCE_CLOSED", "Conversion archive is closed");
|
|
4679
|
+
};
|
|
4680
|
+
try {
|
|
4681
|
+
checkOpen();
|
|
4682
|
+
if (!["slpk", "3tz"].includes(format) || !Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1)
|
|
4683
|
+
throw new import_core34.TileConversionError(
|
|
4684
|
+
"INVALID_ARCHIVE_SOURCE",
|
|
4685
|
+
"Archive source requires a supported format and positive safe byte budget"
|
|
4686
|
+
);
|
|
4687
|
+
if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > maxInputBytes)
|
|
4688
|
+
throw new Error("Conversion archive input must be at most 16 MiB.");
|
|
4689
|
+
let readBytes = 0;
|
|
4690
|
+
const boundedFile = {
|
|
4691
|
+
handle: file.handle,
|
|
4692
|
+
size: file.size,
|
|
4693
|
+
bigsize: file.bigsize,
|
|
4694
|
+
url: file.url,
|
|
4695
|
+
...file.stat ? { stat: () => file.stat() } : {},
|
|
4696
|
+
close: closeFile,
|
|
4697
|
+
/** Checks indexed reads before allocation and forwards operation cancellation. */
|
|
4698
|
+
read: async (start, length) => {
|
|
4699
|
+
checkOpen();
|
|
4700
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maxInputBytes - readBytes)
|
|
4701
|
+
throw new import_core34.TileConversionError(
|
|
4702
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
4703
|
+
"Archive indexed reads exceed the input budget"
|
|
4704
|
+
);
|
|
4705
|
+
readBytes += length;
|
|
4706
|
+
const bytes = await file.read(start, length, signal);
|
|
4707
|
+
checkOpen();
|
|
4708
|
+
return bytes;
|
|
4709
|
+
}
|
|
4710
|
+
};
|
|
4711
|
+
const archive = format === "slpk" ? await (0, import_i3s3.parseSLPKArchive)(boundedFile) : await (0, import_d_tiles3.parse3DTilesArchive)(boundedFile);
|
|
4712
|
+
const root = new URL(rootUrl);
|
|
4713
|
+
return {
|
|
4714
|
+
rootUrl: format === "3tz" ? `${rootUrl}/tileset.json` : rootUrl,
|
|
4715
|
+
descriptor: descriptor ? {
|
|
4716
|
+
...descriptor,
|
|
4717
|
+
...file instanceof import_loader_utils.HttpFile ? { identity: file.getIdentitySnapshot() } : {}
|
|
4718
|
+
} : void 0,
|
|
4719
|
+
/** Resolves only resources belonging to this archive, without external network fallback. */
|
|
4720
|
+
fetcher: (async (input, options) => {
|
|
4721
|
+
checkOpen();
|
|
4722
|
+
options?.signal?.throwIfAborted();
|
|
4723
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
4724
|
+
if (url.origin !== root.origin || url.pathname !== root.pathname && !url.pathname.startsWith(`${root.pathname}/`))
|
|
4725
|
+
throw new Error("Conversion dependencies must resolve inside the selected archive.");
|
|
4726
|
+
const path = decodeURIComponent(
|
|
4727
|
+
url.pathname.slice(root.pathname.length).replace(/^\//, "")
|
|
4728
|
+
);
|
|
4729
|
+
if (path.includes("\\") || path.split("/").some((segment) => segment === ".." || segment === "."))
|
|
4730
|
+
throw new Error("Archive dependency path cannot escape its root.");
|
|
4731
|
+
const bytes = format === "slpk" ? await archive.getFile(path, "http") : await archive.getFile(path);
|
|
4732
|
+
checkOpen();
|
|
4733
|
+
options?.signal?.throwIfAborted();
|
|
4734
|
+
return new Response(bytes);
|
|
4735
|
+
}),
|
|
4736
|
+
/** Releases the readable file on success, failure or cancellation. */
|
|
4737
|
+
close: async () => {
|
|
4738
|
+
closed = true;
|
|
4739
|
+
releasePromise ??= archive.close();
|
|
4740
|
+
await releasePromise;
|
|
4741
|
+
}
|
|
4742
|
+
};
|
|
4743
|
+
} catch (error) {
|
|
4744
|
+
await closeFile();
|
|
4745
|
+
throw error;
|
|
4746
|
+
}
|
|
4747
|
+
}
|
|
4748
|
+
function validateArchiveEntityTag(entityTag) {
|
|
4749
|
+
if (entityTag !== void 0 && entityTag !== null && !/^"[\x21\x23-\x7e\x80-\xff]*"$/.test(entityTag))
|
|
4750
|
+
throw new import_core34.TileConversionError(
|
|
4751
|
+
"ARCHIVE_VALIDATOR_UNSUPPORTED",
|
|
4752
|
+
"Remote archives require a strong quoted ETag, or Last-Modified without an ETag; weak/malformed ETags are unsupported"
|
|
4753
|
+
);
|
|
4754
|
+
}
|
|
4755
|
+
function validateArchiveResponseValidator(headers) {
|
|
4756
|
+
const entityTag = headers.get("etag");
|
|
4757
|
+
validateArchiveEntityTag(entityTag);
|
|
4758
|
+
if (entityTag !== null) return;
|
|
4759
|
+
const lastModified = headers.get("last-modified");
|
|
4760
|
+
const responseDate = headers.get("date");
|
|
4761
|
+
const modifiedTime = parseArchiveHttpDate(lastModified);
|
|
4762
|
+
const responseTime = parseArchiveHttpDate(responseDate);
|
|
4763
|
+
if (!Number.isFinite(modifiedTime) || !Number.isFinite(responseTime) || responseTime - modifiedTime < 6e4)
|
|
4764
|
+
throw new import_core34.TileConversionError(
|
|
4765
|
+
"ARCHIVE_VALIDATOR_UNSUPPORTED",
|
|
4766
|
+
"Last-Modified archive identity requires a valid Date at least 60 seconds later; otherwise supply a strong ETag"
|
|
4767
|
+
);
|
|
4768
|
+
}
|
|
4769
|
+
function parseArchiveHttpDate(value) {
|
|
4770
|
+
const milliseconds = value === null ? NaN : Date.parse(value);
|
|
4771
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toUTCString() === value ? milliseconds : NaN;
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
// src/v5/i3s-selection-source.ts
|
|
4775
|
+
var import_core35 = require("@loaders.gl/core");
|
|
4776
|
+
var import_i3s4 = require("@loaders.gl/i3s");
|
|
4777
|
+
var import_tiles5 = require("@loaders.gl/tiles");
|
|
4778
|
+
var import_core36 = require("@loaders.gl/tile-converter/v5/core");
|
|
4779
|
+
function createRuntime(transport, layer, header) {
|
|
4780
|
+
const libraries = transport.dracoLibraryOptions;
|
|
4781
|
+
const sourceUrl = new URL(transport.rootUrl);
|
|
4782
|
+
sourceUrl.search = "";
|
|
4783
|
+
sourceUrl.hash = "";
|
|
4784
|
+
const options = {
|
|
4785
|
+
modules: libraries.modules,
|
|
4786
|
+
core: {
|
|
4787
|
+
worker: false,
|
|
4788
|
+
fetch: transport.fetcher,
|
|
4789
|
+
CDN: libraries.CDN,
|
|
4790
|
+
useLocalLibraries: libraries.useLocalLibraries
|
|
4791
|
+
},
|
|
4792
|
+
draco: { decoderProfile: libraries.decoderProfile },
|
|
4793
|
+
// The injected transport owns credential scope; runtime query parameters apply to every URL.
|
|
4794
|
+
i3s: { geometryMode: "source", decodeTextures: false, useCompressedTextures: false }
|
|
4795
|
+
};
|
|
4796
|
+
const loadResource = async (url, loader, loadOptions) => {
|
|
4797
|
+
const response = await transport.fetcher(url);
|
|
4798
|
+
const contextUrl = new URL(url);
|
|
4799
|
+
contextUrl.search = "";
|
|
4800
|
+
contextUrl.hash = "";
|
|
4801
|
+
return (0, import_core35.parse)(await response.arrayBuffer(), loader, loadOptions, {
|
|
4802
|
+
url: contextUrl.href,
|
|
4803
|
+
fetch: transport.fetcher,
|
|
4804
|
+
coreApi: import_core35.coreApi,
|
|
4805
|
+
_parse: import_core35.parse
|
|
4806
|
+
});
|
|
4807
|
+
};
|
|
4808
|
+
const source = new import_tiles5.I3SSource(
|
|
4809
|
+
header && layer ? {
|
|
4810
|
+
...layer,
|
|
4811
|
+
root: { ...header, children: [] },
|
|
4812
|
+
type: "I3S",
|
|
4813
|
+
url: sourceUrl.href,
|
|
4814
|
+
basePath: sourceUrl.href,
|
|
4815
|
+
loader: import_i3s4.I3SLoader,
|
|
4816
|
+
coreApi: import_core35.coreApi
|
|
4817
|
+
} : {
|
|
4818
|
+
url: sourceUrl.href,
|
|
4819
|
+
loader: import_i3s4.I3SLoader,
|
|
4820
|
+
coreApi: import_core35.coreApi,
|
|
4821
|
+
resolver: { loadRoot: loadResource, loadResource }
|
|
4822
|
+
},
|
|
4823
|
+
options
|
|
4824
|
+
);
|
|
4825
|
+
source.coreApi = { ...import_core35.coreApi, load: loadResource };
|
|
4826
|
+
return new import_tiles5.Tileset3D(source, { i3s: options.i3s });
|
|
4827
|
+
}
|
|
4828
|
+
async function inspectI3SMeshConversionSource(options) {
|
|
4829
|
+
const { signal } = options;
|
|
4830
|
+
validateSelectionOptions(options);
|
|
4831
|
+
signal.throwIfAborted();
|
|
4832
|
+
const transport = options;
|
|
4833
|
+
let runtime;
|
|
4834
|
+
try {
|
|
4835
|
+
const response = await transport.fetcher(transport.rootUrl);
|
|
4836
|
+
const layer = await response.json();
|
|
4837
|
+
if (!["3DObject", "IntegratedMesh"].includes(layer.layerType))
|
|
4838
|
+
throw new Error("Conversion accepts I3S 3DObject or IntegratedMesh layers.");
|
|
4839
|
+
const rootFetcher = transport.fetcher;
|
|
4840
|
+
const cachedFetcher = (url, options2) => {
|
|
4841
|
+
const requestUrl = new URL(url instanceof Request ? url.url : String(url));
|
|
4842
|
+
const root = new URL(transport.rootUrl);
|
|
4843
|
+
return requestUrl.origin === root.origin && requestUrl.pathname === root.pathname ? Promise.resolve(new Response(JSON.stringify(layer))) : rootFetcher(url, options2);
|
|
4844
|
+
};
|
|
4845
|
+
runtime = createRuntime({ ...transport, fetcher: cachedFetcher });
|
|
4846
|
+
await runtime.tilesetInitializationPromise;
|
|
4847
|
+
await createI3SMeshTilesetConversionSource(runtime).inspect(signal);
|
|
4848
|
+
const stack = [runtime.root];
|
|
4849
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
4850
|
+
const resources = [];
|
|
4851
|
+
while (stack.length) {
|
|
4852
|
+
signal.throwIfAborted();
|
|
4853
|
+
const tile = stack.pop();
|
|
4854
|
+
if (!["string", "number"].includes(typeof tile.header.id) || String(tile.header.id).length === 0)
|
|
4855
|
+
throw new Error("I3S nodes require a declared identity.");
|
|
4856
|
+
const identifier = String(tile.header.id);
|
|
4857
|
+
if (identifiers.has(identifier))
|
|
4858
|
+
throw new Error("Repeated I3S node identity is unsupported.");
|
|
4859
|
+
identifiers.add(identifier);
|
|
4860
|
+
if (identifiers.size + stack.length + (tile.header.children?.length ?? 0) > (options.maxInputResources ?? SELECTED_MESH_LIMITS.maxInputResources))
|
|
4861
|
+
throw new Error("I3S inspection exceeds the 1,000 node limit.");
|
|
4862
|
+
await runtime.source.loadTileChildrenForTraversal(tile, signal);
|
|
4863
|
+
if (!tile.children.length && tile.contentUrl)
|
|
4864
|
+
resources.push({
|
|
4865
|
+
resourceId: identifier,
|
|
4866
|
+
uri: tile.contentUrl,
|
|
4867
|
+
header: structuredClone(tile.header)
|
|
4868
|
+
});
|
|
4869
|
+
for (const child of [...tile.children].reverse()) stack.push(child);
|
|
4870
|
+
}
|
|
4871
|
+
signal.throwIfAborted();
|
|
4872
|
+
if (!resources.length) throw new Error("No supported leaf mesh nodes are declared.");
|
|
4873
|
+
return {
|
|
4874
|
+
kind: "i3s",
|
|
4875
|
+
rootUrl: transport.rootUrl,
|
|
4876
|
+
layer,
|
|
4877
|
+
resources,
|
|
4878
|
+
metadataBytes: 0
|
|
4879
|
+
};
|
|
4880
|
+
} finally {
|
|
4881
|
+
runtime?.destroy();
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
async function createSelectedI3SMeshConversionSource(inspection, options) {
|
|
4885
|
+
const { resourceIds, signal, features } = options;
|
|
4886
|
+
validateSelectionOptions(options);
|
|
4887
|
+
signal.throwIfAborted();
|
|
4888
|
+
if (inspection.rootUrl !== options.rootUrl)
|
|
4889
|
+
throw new import_core36.TileConversionError(
|
|
4890
|
+
"INVALID_I3S_SELECTION",
|
|
4891
|
+
"Selection transport must match the inspected layer"
|
|
4892
|
+
);
|
|
4893
|
+
if (!resourceIds.length || resourceIds.length > SELECTED_MESH_LIMITS.maxSelectedResources || new Set(resourceIds).size !== resourceIds.length || resourceIds.some(
|
|
4894
|
+
(identifier) => !inspection.resources.some((resource) => resource.resourceId === identifier)
|
|
4895
|
+
))
|
|
4896
|
+
throw new Error("Select between 1 and 64 distinct leaf mesh nodes.");
|
|
4897
|
+
const selected = inspection.resources.filter(
|
|
4898
|
+
(resource) => resourceIds.includes(resource.resourceId)
|
|
4899
|
+
);
|
|
4900
|
+
if (selected.some(
|
|
4901
|
+
(resource) => resource.header.children?.length || !resource.header.contentUrl || resource.uri !== resource.header.contentUrl
|
|
4902
|
+
))
|
|
4903
|
+
throw new Error("Only inspected leaf mesh headers can be converted.");
|
|
4904
|
+
const transport = options;
|
|
4905
|
+
let decodedBytes = 0;
|
|
4906
|
+
let meshCount = 0;
|
|
4907
|
+
{
|
|
4908
|
+
const qualification = createRuntime(transport, inspection.layer, selected[0].header);
|
|
4909
|
+
let spatialContext;
|
|
4910
|
+
try {
|
|
4911
|
+
await qualification.tilesetInitializationPromise;
|
|
4912
|
+
const metadata = await createI3SMeshTilesetConversionSource(qualification).inspect(signal);
|
|
4913
|
+
spatialContext = (0, import_core36.createI3SConversionSpatialContext)(metadata.spatialReference, {
|
|
4914
|
+
targetCrs: "EPSG:4978"
|
|
4915
|
+
});
|
|
4916
|
+
} finally {
|
|
4917
|
+
qualification.destroy();
|
|
4918
|
+
}
|
|
4919
|
+
const source = {
|
|
4920
|
+
/** Reuses only immutable inspected declarations. */
|
|
4921
|
+
inspect: async () => inspection,
|
|
4922
|
+
/** Releases each dedicated node runtime before moving on or closing early. */
|
|
4923
|
+
async *read() {
|
|
4924
|
+
for (const descriptor of selected) {
|
|
4925
|
+
const runtime = createRuntime(transport, inspection.layer, descriptor.header);
|
|
4926
|
+
try {
|
|
4927
|
+
await runtime.tilesetInitializationPromise;
|
|
4928
|
+
const meshSource = createI3SMeshTilesetConversionSource(runtime, {
|
|
4929
|
+
unloadContent: true,
|
|
4930
|
+
features,
|
|
4931
|
+
readExternalResource: async (uri) => new Uint8Array(
|
|
4932
|
+
await (await transport.fetcher(runtime.source.getTileUrl(uri))).arrayBuffer()
|
|
4933
|
+
)
|
|
4934
|
+
});
|
|
4935
|
+
const metadata = await meshSource.inspect(signal);
|
|
4936
|
+
for await (const resource of meshSource.read(metadata, signal)) {
|
|
4937
|
+
decodedBytes += measureMeshBytes(resource);
|
|
4938
|
+
if (++meshCount > SELECTED_MESH_LIMITS.maxMeshResources || decodedBytes > (options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes))
|
|
4939
|
+
throw new import_core36.TileConversionError(
|
|
4940
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
4941
|
+
"Selected I3S decoded resources exceed the input/mesh budget"
|
|
4942
|
+
);
|
|
4943
|
+
yield { ...resource, id: descriptor.resourceId };
|
|
4944
|
+
}
|
|
4945
|
+
} finally {
|
|
4946
|
+
runtime.destroy();
|
|
4947
|
+
}
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
};
|
|
4951
|
+
return { source, spatialContext };
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
function validateSelectionOptions(options) {
|
|
4955
|
+
for (const [value, maximum] of [
|
|
4956
|
+
[
|
|
4957
|
+
options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes,
|
|
4958
|
+
SELECTED_MESH_LIMITS.maxInputBytes
|
|
4959
|
+
],
|
|
4960
|
+
[
|
|
4961
|
+
options.maxInputResources ?? SELECTED_MESH_LIMITS.maxInputResources,
|
|
4962
|
+
SELECTED_MESH_LIMITS.maxInputResources
|
|
4963
|
+
]
|
|
4964
|
+
])
|
|
4965
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
4966
|
+
throw new import_core36.TileConversionError(
|
|
4967
|
+
"INVALID_I3S_SELECTION_LIMIT",
|
|
4968
|
+
"I3S selection limits must be positive safe integers within the bounded profile"
|
|
4969
|
+
);
|
|
4970
|
+
}
|