@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.
Files changed (107) hide show
  1. package/README.md +211 -2
  2. package/bin/converter-v5.js +2 -0
  3. package/dist/converter.min.cjs +162 -163
  4. package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
  5. package/dist/index.cjs +1933 -2232
  6. package/dist/index.js +1925 -2235
  7. package/dist/v5/adapters.cjs +3186 -208
  8. package/dist/v5/adapters.d.ts +29 -2
  9. package/dist/v5/adapters.js +3202 -196
  10. package/dist/v5/archive-source.d.ts +36 -0
  11. package/dist/v5/browser-tileset-source.d.ts +55 -11
  12. package/dist/v5/browser.cjs +30 -0
  13. package/dist/v5/browser.d.ts +26 -2
  14. package/dist/v5/browser.js +45 -1
  15. package/dist/v5/conversion-api.d.ts +9 -0
  16. package/dist/v5/converter-cli.d.ts +1 -0
  17. package/dist/v5/converter-cli.js +1214 -0
  18. package/dist/v5/core.cjs +515 -50
  19. package/dist/v5/core.d.ts +2 -0
  20. package/dist/v5/core.js +515 -50
  21. package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
  22. package/dist/v5/i3s-mesh-sink.d.ts +25 -0
  23. package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
  24. package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
  25. package/dist/v5/i3s-mesh-source.d.ts +30 -0
  26. package/dist/v5/i3s-selection-source.d.ts +70 -0
  27. package/dist/v5/index.cjs +20 -0
  28. package/dist/v5/index.d.ts +18 -2
  29. package/dist/v5/index.js +31 -1
  30. package/dist/v5/mesh-conversion.d.ts +29 -2
  31. package/dist/v5/mesh-features.d.ts +9 -0
  32. package/dist/v5/mesh-hierarchy.d.ts +47 -0
  33. package/dist/v5/mesh-source-features.d.ts +2 -1
  34. package/dist/v5/mesh-source-schema.d.ts +12 -0
  35. package/dist/v5/mesh-source-texture.d.ts +2 -0
  36. package/dist/v5/mesh-source.d.ts +13 -3
  37. package/dist/v5/mesh-tileset-sink.d.ts +5 -0
  38. package/dist/v5/mesh.d.ts +6 -1
  39. package/dist/v5/node-cli.d.ts +9 -0
  40. package/dist/v5/node-conversion.d.ts +32 -0
  41. package/dist/v5/node-feature-schema.d.ts +3 -0
  42. package/dist/v5/node-i3s-source.d.ts +41 -0
  43. package/dist/v5/node-source.d.ts +47 -0
  44. package/dist/v5/node.cjs +900 -0
  45. package/dist/v5/node.d.ts +6 -0
  46. package/dist/v5/node.js +877 -0
  47. package/dist/v5/point-cloud-file.d.ts +83 -0
  48. package/dist/v5/point-cloud-potree.d.ts +47 -0
  49. package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
  50. package/dist/v5/point-cloud-spatial.d.ts +22 -0
  51. package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
  52. package/dist/v5/point-cloud-tileset.d.ts +44 -0
  53. package/dist/v5/point-cloud.d.ts +26 -1
  54. package/dist/v5/resource-fetcher.d.ts +18 -0
  55. package/dist/v5/resource-manifest.d.ts +10 -1
  56. package/dist/v5/selected-mesh-source.d.ts +53 -0
  57. package/dist/v5/spatial-conversion.d.ts +3 -1
  58. package/package.json +33 -25
  59. package/src/index.ts +4 -1
  60. package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
  61. package/src/v4/converter-cli.ts +8 -2
  62. package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
  63. package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
  64. package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
  65. package/src/v4/i3s-converter/json-templates/store.ts +2 -1
  66. package/src/v5/adapters.ts +69 -2
  67. package/src/v5/archive-source.ts +235 -0
  68. package/src/v5/browser-tileset-source.ts +418 -48
  69. package/src/v5/browser.ts +81 -2
  70. package/src/v5/conversion-api.ts +69 -9
  71. package/src/v5/converter-cli.ts +28 -0
  72. package/src/v5/core.ts +6 -0
  73. package/src/v5/i3s-mesh-conversion.ts +97 -19
  74. package/src/v5/i3s-mesh-sink.ts +351 -0
  75. package/src/v5/i3s-mesh-source-features.ts +279 -0
  76. package/src/v5/i3s-mesh-source-material.ts +157 -0
  77. package/src/v5/i3s-mesh-source.ts +228 -0
  78. package/src/v5/i3s-selection-source.ts +298 -0
  79. package/src/v5/index.ts +60 -2
  80. package/src/v5/mesh-conversion.ts +61 -11
  81. package/src/v5/mesh-draco.ts +47 -3
  82. package/src/v5/mesh-features.ts +319 -0
  83. package/src/v5/mesh-hierarchy.ts +648 -0
  84. package/src/v5/mesh-source-features.ts +73 -10
  85. package/src/v5/mesh-source-schema.ts +134 -0
  86. package/src/v5/mesh-source-texture.ts +47 -0
  87. package/src/v5/mesh-source.ts +99 -25
  88. package/src/v5/mesh-tileset-sink.ts +1 -1
  89. package/src/v5/mesh.ts +16 -7
  90. package/src/v5/node-cli.ts +197 -0
  91. package/src/v5/node-conversion.ts +345 -0
  92. package/src/v5/node-feature-schema.ts +155 -0
  93. package/src/v5/node-i3s-source.ts +181 -0
  94. package/src/v5/node-source.ts +328 -0
  95. package/src/v5/node.ts +16 -0
  96. package/src/v5/point-cloud-conversion.ts +9 -1
  97. package/src/v5/point-cloud-file.ts +306 -0
  98. package/src/v5/point-cloud-potree.ts +130 -0
  99. package/src/v5/point-cloud-source-encoder.ts +31 -20
  100. package/src/v5/point-cloud-spatial.ts +280 -0
  101. package/src/v5/point-cloud-tileset-sink.ts +234 -0
  102. package/src/v5/point-cloud-tileset.ts +137 -0
  103. package/src/v5/point-cloud.ts +159 -32
  104. package/src/v5/resource-fetcher.ts +113 -0
  105. package/src/v5/resource-manifest.ts +40 -5
  106. package/src/v5/selected-mesh-source.ts +384 -0
  107. package/src/v5/spatial-conversion.ts +161 -6
@@ -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
- encodeTileConversionArchiveInBatches: () => encodeTileConversionArchiveInBatches
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
- if (!positionAttribute || positionAttribute.size !== 3) {
372
- throw new import_core2.TileConversionError(
373
- "POINT_CLOUD_POSITION_REQUIRED",
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
- return import_d_tiles.Tile3DWriter.encodeSync(
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 getPositionValues(attribute, rtcCenter) {
433
- if (rtcCenter && (rtcCenter.length !== 3 || rtcCenter.some((value) => !Number.isFinite(value)))) {
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 result = new Float32Array(values.length);
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
- for (let index = 0; index < values.length; index++) {
443
- const component = index % 3;
444
- const value = values[index];
445
- const absoluteValue = transform?.type === "quantization" ? transform.origin[component] + value / (2 ** transform.bits - 1) * transform.range : value;
446
- result[index] = absoluteValue - (rtcCenter?.[component] ?? 0);
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 result;
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 import_core3 = require("@loaders.gl/tile-converter/v5/core");
824
+ var import_core5 = require("@loaders.gl/tile-converter/v5/core");
563
825
  async function* encodePointCloudSource(source, options = {}) {
564
- for await (const sourceTile of (0, import_core3.traversePointCloudSource)(source, options)) {
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
- pnts: encodePointCloudTile(content.data, {
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 import_core4 = require("@loaders.gl/tile-converter/v5/core");
595
- var import_core5 = require("@loaders.gl/tile-converter/v5/core");
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, import_core4.convertTileset)({
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, import_core5.traversePointCloudSource)(source, { maxDepth: options.maxDepth, signal })
876
+ read: (_inspection, signal) => (0, import_core7.traversePointCloudSource)(source, { maxDepth: options.maxDepth, signal })
605
877
  },
606
878
  codec: {
607
- async *convert(sourceTile) {
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
- var import_core6 = require("@loaders.gl/tile-converter/v5/core");
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 = getMeshIndices(mesh.indices, positions.length / 3);
1247
+ const indices = validateMeshIndices(mesh.indices, positions.length / 3);
722
1248
  if (!indices && positions.length % 9 !== 0) {
723
- throw new import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError("MESH_ATTRIBUTE_NONFINITE", `${name} must contain finite values`);
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 getMeshIndices(attribute, vertexCount) {
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError("MESH_MATERIAL_INVALID", "Material must be an object");
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError("MESH_SAMPLER_INVALID", "Sampler must be an object");
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_core6.TileConversionError(
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 import_tiles = require("@loaders.gl/tiles");
936
- var import_core8 = require("@loaders.gl/tile-converter/v5/core");
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 import_core7 = require("@loaders.gl/tile-converter/v5/core");
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 import_core7.TileConversionError(
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 import_core7.TileConversionError(
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 import_core7.TileConversionError("MESH_DRACO_ENCODING_INVALID", `Draco must preserve ${name}`);
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 import_core8.TileConversionError("MESH_DRACO_OPTIONS_INVALID", "draco must be a boolean");
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 import_core8.TileConversionError(
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, import_tiles.getSpatialCoordinateFrame)(outputCrs) : "unknown";
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 import_core8.TileConversionError(
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 import_core8.TileConversionError(
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 targetPositions = spatialContext.transformPositions(sourcePositions);
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 import_core8.TileConversionError(
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 import_core8.TileConversionError(
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: spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions),
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 import_core9 = require("@loaders.gl/tile-converter/v5/core");
1151
- var import_core10 = require("@loaders.gl/tile-converter/v5/core");
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 import_core9.TileConversionError(
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, import_core10.createBoundedMemoryTileConversionSink)(options);
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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, import_core10.createBoundedMemoryTileConversionSink)(options);
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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 import_core9.TileConversionError(
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 import_core11 = require("@loaders.gl/tile-converter/v5/core");
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 import_core11.TileConversionError(
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 import_core11.TileConversionError("INVALID_ARCHIVE_FORMAT", "Expected 3tz or slpk archive format");
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 import_core11.TileConversionError(
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 import_core12 = require("@loaders.gl/tile-converter/v5/core");
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 import_core12.TileConversionError(
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 import_core12.TileConversionError(
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 import_core15 = require("@math.gl/core");
1439
- var import_core16 = require("@loaders.gl/tile-converter/v5/core");
1440
- var import_core17 = require("@loaders.gl/tile-converter/v5/core");
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 import_core13 = require("@loaders.gl/tile-converter/v5/core");
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 import_core13.TileConversionError(
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 import_core13.TileConversionError(
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 import_core13.TileConversionError("MESH_SOURCE_TEXTURE_UNSUPPORTED", "Invalid inline image base64");
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-features.ts
1517
- var import_core14 = require("@loaders.gl/tile-converter/v5/core");
1518
- function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
2212
+ throw new import_core18.TileConversionError(
1532
2213
  "MESH_FEATURE_SCHEMA_REQUIRED",
1533
- "Feature extraction requires an explicit Arrow schema and class mapping"
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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.default === void 0)
1584
- throw new import_core14.TileConversionError(
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
- properties[name] = data ?? Array.from({ length: count }, () => property.default);
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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 import_core14.TileConversionError(
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
- options
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 rows = [0, 1, 2].map(
1642
- (corner) => attribute.value[indices?.[triangle * 3 + corner] ?? triangle * 3 + corner]
2352
+ const corners = [0, 1, 2].map(
2353
+ (corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
1643
2354
  );
1644
- if (rows.some((row) => row !== rows[0]))
1645
- throw new import_core14.TileConversionError(
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, import_core17.createTilesetConversionSource)(tileset, options);
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core16.TileConversionError("MESH_SOURCE_AXIS_UNSUPPORTED", "Unknown glTF up axis");
1721
- const placement = new import_core15.Matrix4(item.tile.computedTransform);
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 import_core16.TileConversionError(
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 import_core15.Matrix4([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]));
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 import_core15.Matrix4([0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]));
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 import_core16.TileConversionError(
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
- for (const selected of traverseMeshNodes(roots, placement)) {
1743
- const resource = extractPrimitive(
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 { ...resource, features };
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core15.Matrix4(parent).multiplyRight(
1775
- node.matrix ? new import_core15.Matrix4(node.matrix) : new import_core15.Matrix4().translate(node.translation || [0, 0, 0]).multiplyRight(new import_core15.Matrix4().fromQuaternion(node.rotation || [0, 0, 0, 1])).scale(node.scale || [1, 1, 1])
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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 import_core16.TileConversionError(
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.indices ? { value: primitive.indices.value, size: 1 } : void 0
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 import_core15.Matrix4(transform).invert().transpose();
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: mapMaterial(primitive.material)
2579
+ material: await mapMeshSourceMaterial(
2580
+ primitive.material,
2581
+ readExternalResource,
2582
+ contentUri,
2583
+ signal
2584
+ )
1856
2585
  };
1857
2586
  }
1858
- function mapMaterial(material) {
2587
+ async function mapMeshSourceMaterial(material, readExternalResource, contentUri, signal) {
1859
2588
  if (!material) return void 0;
1860
2589
  const pbr = material.pbrMetallicRoughness;
1861
- if (Object.keys(material).some(
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.keys(pbr || {}).some(
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 import_core16.TileConversionError(
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 || {}) ? { baseColorTexture: mapMeshSourceTexture(pbr.baseColorTexture) } : {},
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 import_core18 = require("@loaders.gl/tile-converter/v5/core");
1893
- var import_core19 = require("@loaders.gl/tile-converter/v5/core");
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 import_core18.TileConversionError(
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
- mesh.attributes.POSITION = {
1909
- value: options.spatialContext.transformPositions(positions),
1910
- size: 3
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
- if (mesh.attributes.NORMAL)
1913
- mesh.attributes.NORMAL = {
1914
- value: options.spatialContext.transformNormals(mesh.attributes.NORMAL.value, positions),
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
- layer = (0, import_i3s.encodeI3SMeshLayer)(mesh, {
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 import_core18.TileConversionError(
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
- yield { id: resource.id, ...layer };
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, import_core19.createBoundedMemoryTileConversionSink)(options);
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 import_core18.TileConversionError(
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({ resourceId, parts: [bytes], contentType: "application/gzip" }, signal);
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 import_core18.TileConversionError(
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
+ }