@loaders.gl/tile-converter 5.0.0-alpha.10 → 5.0.0-alpha.11

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 (55) hide show
  1. package/README.md +33 -0
  2. package/dist/converter.min.cjs +103 -103
  3. package/dist/i3s-server/bin/i3s-server.min.cjs +1 -1
  4. package/dist/index.cjs +651 -547
  5. package/dist/index.js +641 -537
  6. package/dist/v5/adapters.cjs +1756 -200
  7. package/dist/v5/adapters.d.ts +13 -2
  8. package/dist/v5/adapters.js +1755 -190
  9. package/dist/v5/browser.cjs +12 -0
  10. package/dist/v5/browser.d.ts +12 -2
  11. package/dist/v5/browser.js +19 -1
  12. package/dist/v5/core.cjs +54 -0
  13. package/dist/v5/core.js +54 -0
  14. package/dist/v5/i3s-mesh-conversion.d.ts +10 -4
  15. package/dist/v5/i3s-mesh-sink.d.ts +25 -0
  16. package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
  17. package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
  18. package/dist/v5/i3s-mesh-source.d.ts +30 -0
  19. package/dist/v5/index.cjs +10 -0
  20. package/dist/v5/index.d.ts +10 -2
  21. package/dist/v5/index.js +17 -1
  22. package/dist/v5/mesh-conversion.d.ts +11 -2
  23. package/dist/v5/mesh-features.d.ts +9 -0
  24. package/dist/v5/mesh-source-texture.d.ts +2 -0
  25. package/dist/v5/mesh-source.d.ts +8 -1
  26. package/dist/v5/mesh.d.ts +6 -1
  27. package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
  28. package/dist/v5/point-cloud-spatial.d.ts +22 -0
  29. package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
  30. package/dist/v5/point-cloud.d.ts +26 -1
  31. package/dist/v5/resource-fetcher.d.ts +18 -0
  32. package/dist/v5/spatial-conversion.d.ts +3 -1
  33. package/package.json +23 -23
  34. package/src/v5/adapters.ts +21 -2
  35. package/src/v5/browser.ts +36 -2
  36. package/src/v5/i3s-mesh-conversion.ts +58 -18
  37. package/src/v5/i3s-mesh-sink.ts +351 -0
  38. package/src/v5/i3s-mesh-source-features.ts +279 -0
  39. package/src/v5/i3s-mesh-source-material.ts +157 -0
  40. package/src/v5/i3s-mesh-source.ts +227 -0
  41. package/src/v5/index.ts +34 -2
  42. package/src/v5/mesh-conversion.ts +41 -11
  43. package/src/v5/mesh-draco.ts +47 -3
  44. package/src/v5/mesh-features.ts +319 -0
  45. package/src/v5/mesh-source-features.ts +37 -6
  46. package/src/v5/mesh-source-texture.ts +47 -0
  47. package/src/v5/mesh-source.ts +83 -21
  48. package/src/v5/mesh.ts +16 -7
  49. package/src/v5/point-cloud-conversion.ts +9 -1
  50. package/src/v5/point-cloud-source-encoder.ts +31 -20
  51. package/src/v5/point-cloud-spatial.ts +280 -0
  52. package/src/v5/point-cloud-tileset-sink.ts +234 -0
  53. package/src/v5/point-cloud.ts +159 -32
  54. package/src/v5/resource-fetcher.ts +110 -0
  55. package/src/v5/spatial-conversion.ts +84 -0
@@ -23,18 +23,24 @@ __export(adapters_exports, {
23
23
  convertFeatureAttributesToArrowBatches: () => convertFeatureAttributesToArrowBatches,
24
24
  convertPointCloudSource: () => convertPointCloudSource,
25
25
  createI3SMeshConversionCodec: () => createI3SMeshConversionCodec,
26
+ createI3SMeshSink: () => createI3SMeshSink,
27
+ createI3SMeshTilesetConversionSource: () => createI3SMeshTilesetConversionSource,
26
28
  createMeshConversionCodec: () => createMeshConversionCodec,
27
29
  createMeshTilesetConversionSource: () => createMeshTilesetConversionSource,
28
30
  createMeshTilesetSink: () => createMeshTilesetSink,
31
+ createPointCloudTilesetSink: () => createPointCloudTilesetSink,
29
32
  createSingleMeshI3SSink: () => createSingleMeshI3SSink,
30
33
  createSingleMeshTilesetArchive: () => createSingleMeshTilesetArchive,
31
34
  createSingleMeshTilesetSink: () => createSingleMeshTilesetSink,
32
35
  createTileConversionArchive: () => createTileConversionArchive,
36
+ createTileConversionResourceFetcher: () => createTileConversionResourceFetcher,
33
37
  encodeMeshTile: () => encodeMeshTile,
34
38
  encodePointCloudSource: () => encodePointCloudSource,
35
39
  encodePointCloudSourceTile: () => encodePointCloudSourceTile,
36
40
  encodePointCloudTile: () => encodePointCloudTile,
37
- encodeTileConversionArchiveInBatches: () => encodeTileConversionArchiveInBatches
41
+ encodePointCloudTileWithMetadata: () => encodePointCloudTileWithMetadata,
42
+ encodeTileConversionArchiveInBatches: () => encodeTileConversionArchiveInBatches,
43
+ transformPointCloudSourceTile: () => transformPointCloudSourceTile
38
44
  });
39
45
  module.exports = __toCommonJS(adapters_exports);
40
46
 
@@ -365,29 +371,22 @@ var import_d_tiles = require("@loaders.gl/3d-tiles");
365
371
  var import_schema_utils2 = require("@loaders.gl/schema-utils");
366
372
  var import_core2 = require("@loaders.gl/tile-converter/v5/core");
367
373
  function encodePointCloudTile(pointBatch, options = {}) {
374
+ return encodePointCloudTileWithMetadata(pointBatch, options).pnts;
375
+ }
376
+ function encodePointCloudTileWithMetadata(pointBatch, options = {}) {
368
377
  const mesh = "attributes" in pointBatch ? pointBatch : (0, import_schema_utils2.convertTableToMesh)(pointBatch);
369
378
  validatePointAttributes(mesh.attributes);
370
379
  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);
380
+ validatePointCloudPosition(positionAttribute);
381
+ const placement = encodePointPositions(positionAttribute, options);
382
+ const { positions } = placement;
378
383
  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
384
  const colors = getColorAttribute(mesh.attributes.COLOR_0 || mesh.attributes.COLOR, pointCount);
386
385
  const constantRGBA = colors ? null : getConstantRgba(options.constantRGBA);
387
386
  const normals = getNormalAttribute(mesh.attributes.NORMAL, pointCount);
388
387
  const batchIds = getBatchIds(mesh.attributes.BATCH_ID, pointCount);
389
388
  const batchTableJson = validateBatchTable(options.batchTableJson, batchIds);
390
- return import_d_tiles.Tile3DWriter.encodeSync(
389
+ const pnts = import_d_tiles.Tile3DWriter.encodeSync(
391
390
  {
392
391
  type: import_d_tiles.TILE3D_TYPE.POINT_CLOUD,
393
392
  featureTableJson: options.rtcCenter || constantRGBA ? {
@@ -399,6 +398,13 @@ function encodePointCloudTile(pointBatch, options = {}) {
399
398
  },
400
399
  {}
401
400
  );
401
+ return {
402
+ pnts,
403
+ pointCount,
404
+ rtcCenter: placement.rtcCenter,
405
+ localBoundingBox: placement.localBoundingBox,
406
+ maximumPositionError: placement.maximumPositionError
407
+ };
402
408
  }
403
409
  function validatePointAttributes(attributes) {
404
410
  const supportedAttributes = /* @__PURE__ */ new Set([
@@ -429,23 +435,88 @@ function getConstantRgba(constantRGBA) {
429
435
  }
430
436
  return [constantRGBA[0], constantRGBA[1], constantRGBA[2], constantRGBA[3]];
431
437
  }
432
- function getPositionValues(attribute, rtcCenter) {
433
- if (rtcCenter && (rtcCenter.length !== 3 || rtcCenter.some((value) => !Number.isFinite(value)))) {
438
+ function validatePointCloudPosition(attribute) {
439
+ if (!attribute || attribute.size !== 3 || attribute.byteOffset || attribute.byteStride || attribute.normalized || attribute.componentType) {
440
+ throw new import_core2.TileConversionError(
441
+ "POINT_CLOUD_POSITION_REQUIRED",
442
+ "Point cloud batches must contain packed, non-normalized three-component POSITION values"
443
+ );
444
+ }
445
+ if (!attribute.value.length || attribute.value.length % 3) {
446
+ throw new import_core2.TileConversionError(
447
+ "POINT_CLOUD_POSITION_COUNT_INVALID",
448
+ "Point cloud POSITION must contain one or more xyz triples"
449
+ );
450
+ }
451
+ const transform = attribute.transform;
452
+ 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)) {
453
+ throw new import_core2.TileConversionError(
454
+ "POINT_CLOUD_POSITION_TRANSFORM_INVALID",
455
+ "Position quantization requires finite xyz origin, nonnegative range and 1\u201332 bits"
456
+ );
457
+ }
458
+ }
459
+ function decodePointCloudPositions(attribute) {
460
+ validatePointCloudPosition(attribute);
461
+ const transform = attribute.transform;
462
+ return Float64Array.from(
463
+ attribute.value,
464
+ (value, index) => transform ? transform.origin[index % 3] + value / (2 ** transform.bits - 1) * transform.range : value
465
+ );
466
+ }
467
+ function encodePointPositions(attribute, options) {
468
+ const { rtcCenter = [0, 0, 0], maxPositionError = Infinity } = options;
469
+ if (rtcCenter.length !== 3 || rtcCenter.some((value) => !Number.isFinite(value))) {
434
470
  throw new import_core2.TileConversionError(
435
471
  "POINT_CLOUD_RTC_CENTER_INVALID",
436
472
  "Point cloud RTC_CENTER must contain three finite values"
437
473
  );
438
474
  }
475
+ if (options.maxPositionError !== void 0 && (!Number.isFinite(maxPositionError) || maxPositionError < 0)) {
476
+ throw new import_core2.TileConversionError(
477
+ "POINT_CLOUD_PRECISION_INVALID",
478
+ "maxPositionError must be finite and nonnegative"
479
+ );
480
+ }
439
481
  const values = attribute.value;
440
- const result = new Float32Array(values.length);
482
+ const positions = new Float32Array(values.length);
483
+ const minimum = [Infinity, Infinity, Infinity];
484
+ const maximum = [-Infinity, -Infinity, -Infinity];
485
+ let maximumPositionError = 0;
441
486
  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);
487
+ const errors = [0, 0, 0];
488
+ for (let point = 0; point < values.length; point += 3) {
489
+ for (let axis = 0; axis < 3; axis++) {
490
+ const value = values[point + axis];
491
+ const absoluteValue = transform ? transform.origin[axis] + value / (2 ** transform.bits - 1) * transform.range : value;
492
+ const relativeValue = absoluteValue - rtcCenter[axis];
493
+ positions[point + axis] = relativeValue;
494
+ const encodedValue = positions[point + axis];
495
+ const reconstructed = encodedValue + rtcCenter[axis];
496
+ if (!Number.isFinite(absoluteValue) || !Number.isFinite(relativeValue) || !Number.isFinite(encodedValue) || !Number.isFinite(reconstructed)) {
497
+ throw new import_core2.TileConversionError(
498
+ "POINT_CLOUD_POSITION_INVALID",
499
+ "Point positions and their float32 reconstruction must be finite"
500
+ );
501
+ }
502
+ errors[axis] = reconstructed - absoluteValue;
503
+ minimum[axis] = Math.min(minimum[axis], encodedValue);
504
+ maximum[axis] = Math.max(maximum[axis], encodedValue);
505
+ }
506
+ maximumPositionError = Math.max(maximumPositionError, Math.hypot(...errors));
507
+ }
508
+ if (!Number.isFinite(maximumPositionError) || maximumPositionError > maxPositionError) {
509
+ throw new import_core2.TileConversionError(
510
+ "POINT_CLOUD_PRECISION_EXCEEDED",
511
+ "Float32 position reconstruction exceeds maxPositionError; select a closer RTC center"
512
+ );
447
513
  }
448
- return result;
514
+ return {
515
+ positions,
516
+ rtcCenter: [...rtcCenter],
517
+ localBoundingBox: [minimum, maximum],
518
+ maximumPositionError
519
+ };
449
520
  }
450
521
  function getColorAttribute(attribute, pointCount) {
451
522
  if (!attribute) {
@@ -558,10 +629,191 @@ function validateBatchTable(batchTableJson, batchIds) {
558
629
  return normalizedBatchTable;
559
630
  }
560
631
 
632
+ // src/v5/point-cloud-spatial.ts
633
+ var import_core3 = require("@math.gl/core");
634
+ var import_tiles = require("@loaders.gl/tiles");
635
+ var import_schema_utils3 = require("@loaders.gl/schema-utils");
636
+ var import_core4 = require("@loaders.gl/tile-converter/v5/core");
637
+ async function transformPointCloudSourceTile(sourceTile, options) {
638
+ options.signal?.throwIfAborted();
639
+ const { content } = sourceTile;
640
+ if (!content) return sourceTile;
641
+ const reference = options.spatialContext.spatialReference;
642
+ const contentReference = content.spatialReference;
643
+ 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);
644
+ const sourceReference = retainTransformedEcef ? (0, import_tiles.createTilesetSpatialReference)({
645
+ sourceCrs: "EPSG:4978",
646
+ coordinateFrame: "geocentric",
647
+ axisOrder: "xyz",
648
+ units: reference.units,
649
+ coordinateEpoch: contentReference.coordinateEpoch,
650
+ heightReference: contentReference.targetHeightReference === "native" ? contentReference.heightReference : contentReference.targetHeightReference
651
+ }) : contentReference;
652
+ 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)) {
653
+ throw new import_core4.TileConversionError(
654
+ "POINT_CLOUD_SPATIAL_REFERENCE_MISMATCH",
655
+ "Point content and context must agree on the input CRS, units, height reference and epoch"
656
+ );
657
+ }
658
+ const sourceFrame = reference.coordinateFrame === "unknown" && reference.sourceCrs ? (0, import_tiles.getSpatialCoordinateFrame)(reference.sourceCrs) : reference.coordinateFrame;
659
+ const geographic = sourceFrame === "geographic";
660
+ if (!["xy", "xyz", "unknown"].includes(reference.axisOrder) || (geographic ? !["lnglat", "lnglat-offsets"].includes(content.coordinateSystem) : content.coordinateSystem !== "cartesian" || !["projected", "geocentric", "cartesian", "local"].includes(sourceFrame))) {
661
+ throw new import_core4.TileConversionError(
662
+ "POINT_CLOUD_SOURCE_FRAME_UNSUPPORTED",
663
+ "Spatial preparation requires explicit Cartesian or longitude/latitude positions in source xyz order"
664
+ );
665
+ }
666
+ if (content.cartographicOrigin.length !== 3 || content.cartographicOrigin.some((value) => !Number.isFinite(value)) || content.coordinateSystem !== "lnglat-offsets" && content.cartographicOrigin.some((value) => value !== 0)) {
667
+ throw new import_core4.TileConversionError(
668
+ "POINT_CLOUD_SOURCE_ORIGIN_INVALID",
669
+ "Only longitude/latitude offsets use a nonzero cartographic origin"
670
+ );
671
+ }
672
+ if (content.modelMatrix && (content.modelMatrix.length !== 16 || Array.from(content.modelMatrix).some((value) => !Number.isFinite(value)))) {
673
+ throw new import_core4.TileConversionError(
674
+ "POINT_CLOUD_SOURCE_PLACEMENT_UNSUPPORTED",
675
+ "Point placement must contain sixteen finite matrix values"
676
+ );
677
+ }
678
+ const matrix = new import_core3.Matrix4(content.modelMatrix ? Array.from(content.modelMatrix) : void 0);
679
+ const determinant = matrix.determinant();
680
+ 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())) {
681
+ throw new import_core4.TileConversionError(
682
+ "POINT_CLOUD_SOURCE_PLACEMENT_UNSUPPORTED",
683
+ "Cartesian placement must be finite, affine and invertible; geographic placement must be identity"
684
+ );
685
+ }
686
+ const mesh = (0, import_schema_utils3.convertTableToMesh)(content.data);
687
+ const positionAttribute = mesh.attributes.POSITION;
688
+ const declaredTransform = mesh.schema?.fields.find((field) => field.name === "POSITION")?.metadata?.["loaders.gl.transform"];
689
+ if (declaredTransform && !positionAttribute?.transform) {
690
+ throw new import_core4.TileConversionError(
691
+ "POINT_CLOUD_POSITION_TRANSFORM_INVALID",
692
+ "Arrow position transform metadata is unsupported or invalid"
693
+ );
694
+ }
695
+ const sourcePositions = decodePointCloudPositions(positionAttribute);
696
+ if (sourcePositions.some((value) => !Number.isFinite(value))) {
697
+ throw new import_core4.TileConversionError(
698
+ "POINT_CLOUD_POSITION_INVALID",
699
+ "Decoded point positions must be finite"
700
+ );
701
+ }
702
+ const translationOnly = Array.from(matrix).every(
703
+ (value, index) => index >= 12 && index <= 14 || value === (index % 5 === 0 ? 1 : 0)
704
+ );
705
+ for (let index = 0; index < sourcePositions.length; index += 3) {
706
+ if (geographic || translationOnly) {
707
+ for (let axis = 0; axis < 3; axis++) {
708
+ sourcePositions[index + axis] += geographic ? content.cartographicOrigin[axis] : matrix[12 + axis];
709
+ }
710
+ } else {
711
+ sourcePositions.set(
712
+ matrix.transformAsPoint(Array.from(sourcePositions.subarray(index, index + 3))),
713
+ index
714
+ );
715
+ }
716
+ }
717
+ const normalAttribute = mesh.attributes.NORMAL;
718
+ let normals;
719
+ if (normalAttribute) {
720
+ if (!options.normalReferenceFrame || normalAttribute.size !== 3 || normalAttribute.value.length !== sourcePositions.length || normalAttribute.transform || normalAttribute.normalized || normalAttribute.byteOffset || normalAttribute.byteStride || normalAttribute.componentType || !translationOnly) {
721
+ throw new import_core4.TileConversionError(
722
+ "POINT_CLOUD_NORMAL_FRAME_REQUIRED",
723
+ "Spatial point normals require a declared vector frame and packed xyz values"
724
+ );
725
+ }
726
+ normals = new Float32Array(normalAttribute.value.length);
727
+ for (let index = 0; index < normals.length; index += 3) {
728
+ const normal = Array.from(normalAttribute.value.slice(index, index + 3));
729
+ const length = Math.hypot(...normal);
730
+ if (!Number.isFinite(length) || length === 0) {
731
+ throw new import_core4.TileConversionError(
732
+ "POINT_CLOUD_NORMAL_INVALID",
733
+ "Spatial point normals must be finite and nonzero"
734
+ );
735
+ }
736
+ normals.set(
737
+ normal.map((value) => value / length),
738
+ index
739
+ );
740
+ }
741
+ }
742
+ const transformed = await options.spatialContext.transformGeometryAsync(
743
+ sourcePositions,
744
+ normals,
745
+ options.normalReferenceFrame
746
+ );
747
+ options.signal?.throwIfAborted();
748
+ if (transformed.positions.length !== sourcePositions.length) {
749
+ throw new import_core4.TileConversionError(
750
+ "POINT_CLOUD_POSITION_INVALID",
751
+ "Spatial conversion must return one xyz triple per point"
752
+ );
753
+ }
754
+ const minimum = [Infinity, Infinity, Infinity];
755
+ const maximum = [-Infinity, -Infinity, -Infinity];
756
+ for (let index = 0; index < transformed.positions.length; index++) {
757
+ const value = transformed.positions[index];
758
+ if (!Number.isFinite(value)) {
759
+ throw new import_core4.TileConversionError(
760
+ "POINT_CLOUD_POSITION_INVALID",
761
+ "Transformed point positions must be finite"
762
+ );
763
+ }
764
+ const axis = index % 3;
765
+ minimum[axis] = Math.min(minimum[axis], value);
766
+ maximum[axis] = Math.max(maximum[axis], value);
767
+ }
768
+ const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
769
+ const targetFrame = reference.status === "native" ? sourceFrame : (0, import_tiles.getSpatialCoordinateFrame)(reference.targetCrs || reference.sourceCrs);
770
+ const targetIsGeographic = targetFrame === "geographic";
771
+ const attributes = {
772
+ ...mesh.attributes,
773
+ POSITION: { value: transformed.positions, size: 3 },
774
+ ...transformed.normals ? { NORMAL: { value: transformed.normals, size: 3 } } : {}
775
+ };
776
+ return {
777
+ header: sourceTile.header,
778
+ content: {
779
+ ...content,
780
+ data: (0, import_schema_utils3.makeMeshArrowTable)(attributes, {
781
+ topology: "point-list",
782
+ boundingBox: [minimum, maximum],
783
+ schema: mesh.schema ? {
784
+ ...mesh.schema,
785
+ metadata: {
786
+ ...mesh.schema.metadata,
787
+ topology: "point-list",
788
+ boundingBox: JSON.stringify([minimum, maximum])
789
+ },
790
+ fields: mesh.schema.fields.map(
791
+ (field) => field.name === "POSITION" || transformed.normals && field.name === "NORMAL" ? { ...field, metadata: {} } : field
792
+ )
793
+ } : void 0
794
+ }),
795
+ coordinateSystem: targetIsGeographic ? "lnglat" : "cartesian",
796
+ cartographicOrigin: [0, 0, 0],
797
+ modelMatrix: void 0,
798
+ spatialReference: reference,
799
+ spatialBoundingVolume: {
800
+ cartographicBounds: [minimum, maximum],
801
+ coordinateFrame: targetIsGeographic ? "geographic" : "cartesian",
802
+ center,
803
+ radius: Math.hypot(...maximum.map((value, axis) => value - center[axis]))
804
+ }
805
+ }
806
+ };
807
+ }
808
+
561
809
  // src/v5/point-cloud-source-encoder.ts
562
- var import_core3 = require("@loaders.gl/tile-converter/v5/core");
810
+ var import_core5 = require("@loaders.gl/tile-converter/v5/core");
563
811
  async function* encodePointCloudSource(source, options = {}) {
564
- for await (const sourceTile of (0, import_core3.traversePointCloudSource)(source, options)) {
812
+ for await (const inputTile of (0, import_core5.traversePointCloudSource)(source, options)) {
813
+ const sourceTile = options.spatialContext ? await transformPointCloudSourceTile(inputTile, {
814
+ ...options,
815
+ spatialContext: options.spatialContext
816
+ }) : inputTile;
565
817
  const encodingOptions = sourceTile.content ? options.getTileEncodingOptions?.(sourceTile) : void 0;
566
818
  const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
567
819
  if (encodedTile) {
@@ -574,14 +826,20 @@ function encodePointCloudSourceTile(sourceTile, options = {}) {
574
826
  if (!content) {
575
827
  return null;
576
828
  }
829
+ const encoded = encodePointCloudTileWithMetadata(content.data, {
830
+ ...options,
831
+ constantRGBA: options.constantRGBA ?? content.constantRGBA
832
+ });
833
+ if (content.pointCount !== encoded.pointCount) {
834
+ throw new import_core5.TileConversionError(
835
+ "POINT_CLOUD_SOURCE_COUNT_INVALID",
836
+ "Source pointCount must match the encoded Arrow point rows"
837
+ );
838
+ }
577
839
  return {
578
840
  id: header.id,
579
841
  header,
580
- pnts: encodePointCloudTile(content.data, {
581
- ...options,
582
- constantRGBA: options.constantRGBA ?? content.constantRGBA
583
- }),
584
- pointCount: content.pointCount,
842
+ ...encoded,
585
843
  coordinateSystem: content.coordinateSystem,
586
844
  cartographicOrigin: [...content.cartographicOrigin],
587
845
  modelMatrix: content.modelMatrix ? Array.from(content.modelMatrix) : void 0,
@@ -591,20 +849,25 @@ function encodePointCloudSourceTile(sourceTile, options = {}) {
591
849
  }
592
850
 
593
851
  // 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");
852
+ var import_core6 = require("@loaders.gl/tile-converter/v5/core");
853
+ var import_core7 = require("@loaders.gl/tile-converter/v5/core");
596
854
  function convertPointCloudSource(source, options) {
597
- return (0, import_core4.convertTileset)({
855
+ return (0, import_core6.convertTileset)({
598
856
  source: {
599
857
  async inspect() {
600
858
  if (!source.isReady) {
601
859
  await source.initialize();
602
860
  }
603
861
  },
604
- read: (_inspection, signal) => (0, import_core5.traversePointCloudSource)(source, { maxDepth: options.maxDepth, signal })
862
+ read: (_inspection, signal) => (0, import_core7.traversePointCloudSource)(source, { maxDepth: options.maxDepth, signal })
605
863
  },
606
864
  codec: {
607
- async *convert(sourceTile) {
865
+ async *convert(inputTile, _inspection, signal) {
866
+ const sourceTile = options.spatialContext ? await transformPointCloudSourceTile(inputTile, {
867
+ ...options,
868
+ spatialContext: options.spatialContext,
869
+ signal
870
+ }) : inputTile;
608
871
  const encodingOptions = sourceTile.content ? options.getTileEncodingOptions?.(sourceTile) : void 0;
609
872
  const encodedTile = encodePointCloudSourceTile(sourceTile, encodingOptions);
610
873
  if (encodedTile) {
@@ -625,9 +888,257 @@ function convertPointCloudSource(source, options) {
625
888
  // src/v5/mesh.ts
626
889
  var import_gltf = require("@loaders.gl/gltf");
627
890
  var import_images = require("@loaders.gl/images");
628
- var import_core6 = require("@loaders.gl/tile-converter/v5/core");
891
+
892
+ // src/v5/mesh-features.ts
893
+ var import_schema_utils4 = require("@loaders.gl/schema-utils");
894
+ var import_core8 = require("@loaders.gl/tile-converter/v5/core");
895
+ function prepareMeshFeatureGeometry(mesh, features) {
896
+ if (!features) return mesh;
897
+ const count = features.batches.reduce((total, batch) => total + batch.data.numRows, 0);
898
+ const indices = mesh.indices?.value;
899
+ const vertexCount = indices?.length ?? mesh.attributes.POSITION.value.length / 3;
900
+ const associations = features.triangleFeatureIndices;
901
+ if (!(associations instanceof Uint32Array) || associations.length !== vertexCount / 3 || associations.some((row) => row >= count) || new Set(associations).size !== count) {
902
+ throw new import_core8.TileConversionError(
903
+ "MESH_FEATURE_ASSOCIATION_INVALID",
904
+ "Every feature row must own a triangle and every triangle must reference a valid row"
905
+ );
906
+ }
907
+ if (count > 2 ** 24)
908
+ throw new import_core8.TileConversionError(
909
+ "MESH_FEATURE_COUNT_UNSUPPORTED",
910
+ "Feature row indices must be exactly representable in a glTF vertex attribute"
911
+ );
912
+ const attributes = Object.fromEntries(
913
+ Object.entries(mesh.attributes).map(([name, attribute]) => {
914
+ const values = attribute.value;
915
+ const Constructor = values.constructor;
916
+ const output = new Constructor(vertexCount * attribute.size);
917
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
918
+ const sourceVertex = indices?.[vertex] ?? vertex;
919
+ for (let component = 0; component < attribute.size; component++)
920
+ output[vertex * attribute.size + component] = Number(
921
+ values[sourceVertex * attribute.size + component]
922
+ );
923
+ }
924
+ return [name, { ...attribute, value: output }];
925
+ })
926
+ );
927
+ const featureIndices = count <= 65536 ? new Uint16Array(vertexCount) : new Float32Array(vertexCount);
928
+ for (let vertex = 0; vertex < vertexCount; vertex++)
929
+ featureIndices[vertex] = associations[Math.floor(vertex / 3)];
930
+ attributes._FEATURE_ID_0 = { value: featureIndices, size: 1 };
931
+ return { ...mesh, indices: void 0, attributes };
932
+ }
933
+ function encodeMeshFeatures(scenegraph, meshIndex, features) {
934
+ if (!features) return;
935
+ const { batches, featureIdField } = features;
936
+ const schema = batches[0]?.schema;
937
+ const count = batches.reduce((total, batch) => total + batch.data.numRows, 0);
938
+ const metadataClass = schema?.metadata?.["loaders.gl:feature-class"] || "features";
939
+ 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) {
940
+ throw new import_core8.TileConversionError(
941
+ "MESH_FEATURE_SCHEMA_INVALID",
942
+ "Features require a single named class, unique fields, and a non-null stable identifier"
943
+ );
944
+ }
945
+ for (const batch of batches) {
946
+ const actual = (0, import_schema_utils4.convertArrowToSchema)(batch.data.schema).fields;
947
+ 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]))) {
948
+ throw new import_core8.TileConversionError(
949
+ "MESH_FEATURE_SCHEMA_INVALID",
950
+ "Declared feature schemas and Arrow column types must agree across batches"
951
+ );
952
+ }
953
+ }
954
+ const properties = /* @__PURE__ */ Object.create(null);
955
+ const columns = /* @__PURE__ */ Object.create(null);
956
+ for (const field of schema.fields) {
957
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(field.name))
958
+ throw new import_core8.TileConversionError(
959
+ "MESH_FEATURE_SCHEMA_INVALID",
960
+ "Property names must be metadata identifiers"
961
+ );
962
+ const values = batches.flatMap((batch) => {
963
+ const column = batch.data.getChild(field.name);
964
+ if (!column)
965
+ throw new import_core8.TileConversionError(
966
+ "MESH_FEATURE_SCHEMA_INVALID",
967
+ `Missing Arrow column ${field.name}`
968
+ );
969
+ return Array.from({ length: column.length }, (_, index) => column.get(index));
970
+ });
971
+ if (field.name === featureIdField && (values.some(
972
+ (value) => !(typeof value === "string" || typeof value === "bigint" || Number.isSafeInteger(value))
973
+ ) || new Set(values).size !== count))
974
+ throw new import_core8.TileConversionError(
975
+ "MESH_FEATURE_IDS_INVALID",
976
+ "Stable identifiers must be unique strings or exact integers"
977
+ );
978
+ const encoded = encodeFeatureColumn(field, values);
979
+ properties[field.name] = encoded.property;
980
+ columns[field.name] = {
981
+ values: addMetadataBuffer(scenegraph, encoded.data),
982
+ ...encoded.offsets ? {
983
+ stringOffsets: addMetadataBuffer(scenegraph, encoded.offsets),
984
+ stringOffsetType: "UINT32"
985
+ } : {}
986
+ };
987
+ }
988
+ scenegraph.json.extensions ||= {};
989
+ scenegraph.json.extensions.EXT_structural_metadata = {
990
+ schema: { id: "loaders_gl_features", classes: { [metadataClass]: { properties } } },
991
+ propertyTables: [{ class: metadataClass, count, properties: columns }]
992
+ };
993
+ const primitive = scenegraph.json.meshes[meshIndex].primitives[0];
994
+ primitive.extensions ||= {};
995
+ primitive.extensions.EXT_mesh_features = {
996
+ featureIds: [{ featureCount: count, attribute: 0, propertyTable: 0 }]
997
+ };
998
+ scenegraph.registerUsedExtension("EXT_structural_metadata");
999
+ scenegraph.registerUsedExtension("EXT_mesh_features");
1000
+ }
1001
+ function addMetadataBuffer(scenegraph, data) {
1002
+ if (scenegraph.byteLength % 8) scenegraph.addBufferView(new Uint8Array(4));
1003
+ return scenegraph.addBufferView(data);
1004
+ }
1005
+ function encodeFeatureColumn(field, values) {
1006
+ const type = field.type;
1007
+ const hasNull = values.includes(null);
1008
+ if (hasNull && field.nullable !== true)
1009
+ throw new import_core8.TileConversionError(
1010
+ "MESH_FEATURE_VALUE_INVALID",
1011
+ `Required field ${field.name} contains null`
1012
+ );
1013
+ const present = values.filter((value) => value !== null);
1014
+ let noData;
1015
+ if (type === "utf8") {
1016
+ if (present.some(
1017
+ (value) => typeof value !== "string" || new TextDecoder().decode(new TextEncoder().encode(value)) !== value
1018
+ ))
1019
+ throw new import_core8.TileConversionError("MESH_FEATURE_VALUE_INVALID", "Strings require valid Unicode");
1020
+ if (hasNull) {
1021
+ noData = "";
1022
+ const strings = new Set(present);
1023
+ while (strings.has(noData)) noData += "_";
1024
+ }
1025
+ const encoded = values.map(
1026
+ (value) => new TextEncoder().encode(value === null ? noData : value)
1027
+ );
1028
+ const offsets = new Uint32Array(values.length + 1);
1029
+ for (let index = 0; index < values.length; index++)
1030
+ offsets[index + 1] = offsets[index] + encoded[index].length;
1031
+ const data2 = new Uint8Array(offsets[values.length]);
1032
+ encoded.forEach((bytes, index) => data2.set(bytes, offsets[index]));
1033
+ return {
1034
+ property: { type: "STRING", required: field.nullable !== true, ...hasNull ? { noData } : {} },
1035
+ data: data2,
1036
+ offsets
1037
+ };
1038
+ }
1039
+ const widths = {
1040
+ int8: 1,
1041
+ uint8: 1,
1042
+ int16: 2,
1043
+ uint16: 2,
1044
+ int32: 4,
1045
+ uint32: 4,
1046
+ int64: 8,
1047
+ uint64: 8,
1048
+ float32: 4,
1049
+ float64: 8
1050
+ };
1051
+ const width = widths[String(type)];
1052
+ if (!width)
1053
+ throw new import_core8.TileConversionError(
1054
+ "MESH_FEATURE_TYPE_UNSUPPORTED",
1055
+ `No qualified GLB property mapping for ${String(type)}`
1056
+ );
1057
+ const isInteger64 = type === "int64" || type === "uint64";
1058
+ const isFloat = type === "float32" || type === "float64";
1059
+ const minimum = isFloat ? -Infinity : String(type).startsWith("uint") ? 0n : -(1n << BigInt(width * 8 - 1));
1060
+ const maximum = isFloat ? Infinity : (1n << BigInt(width * 8 - (String(type).startsWith("uint") ? 0 : 1))) - 1n;
1061
+ if (present.some(
1062
+ (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)
1063
+ ))
1064
+ throw new import_core8.TileConversionError(
1065
+ "MESH_FEATURE_VALUE_INVALID",
1066
+ `Values do not fit ${String(type)}`
1067
+ );
1068
+ if (hasNull) {
1069
+ const used = new Set(present.map(String));
1070
+ noData = 0;
1071
+ while (used.has(String(noData))) noData++;
1072
+ if (noData > maximum && minimum < 0) {
1073
+ noData = -1;
1074
+ while (used.has(String(noData))) noData--;
1075
+ }
1076
+ if (noData > maximum || noData < minimum)
1077
+ throw new import_core8.TileConversionError(
1078
+ "MESH_FEATURE_NULL_UNREPRESENTABLE",
1079
+ "No unused value remains for null without losing a real value"
1080
+ );
1081
+ }
1082
+ const data = new Uint8Array(values.length * width);
1083
+ const view = new DataView(data.buffer);
1084
+ values.forEach((value, index) => {
1085
+ const selected = value === null ? isInteger64 ? BigInt(noData) : noData : value;
1086
+ const offset = index * width;
1087
+ switch (type) {
1088
+ case "int8":
1089
+ view.setInt8(offset, selected);
1090
+ break;
1091
+ case "uint8":
1092
+ view.setUint8(offset, selected);
1093
+ break;
1094
+ case "int16":
1095
+ view.setInt16(offset, selected, true);
1096
+ break;
1097
+ case "uint16":
1098
+ view.setUint16(offset, selected, true);
1099
+ break;
1100
+ case "int32":
1101
+ view.setInt32(offset, selected, true);
1102
+ break;
1103
+ case "uint32":
1104
+ view.setUint32(offset, selected, true);
1105
+ break;
1106
+ case "int64":
1107
+ view.setBigInt64(offset, selected, true);
1108
+ break;
1109
+ case "uint64":
1110
+ view.setBigUint64(offset, selected, true);
1111
+ break;
1112
+ case "float32":
1113
+ view.setFloat32(offset, selected, true);
1114
+ break;
1115
+ case "float64":
1116
+ view.setFloat64(offset, selected, true);
1117
+ break;
1118
+ }
1119
+ if (isFloat && !Number.isFinite(
1120
+ type === "float32" ? view.getFloat32(offset, true) : view.getFloat64(offset, true)
1121
+ ))
1122
+ throw new import_core8.TileConversionError(
1123
+ "MESH_FEATURE_VALUE_INVALID",
1124
+ "Floating point property overflow"
1125
+ );
1126
+ });
1127
+ return {
1128
+ property: {
1129
+ type: "SCALAR",
1130
+ componentType: String(type).toUpperCase(),
1131
+ required: field.nullable !== true,
1132
+ ...hasNull ? { noData } : {}
1133
+ },
1134
+ data
1135
+ };
1136
+ }
1137
+
1138
+ // src/v5/mesh.ts
1139
+ var import_core9 = require("@loaders.gl/tile-converter/v5/core");
629
1140
  function encodeMeshTile(mesh, options = {}) {
630
- const geometry = validateMeshGeometry(mesh);
1141
+ const geometry = prepareMeshFeatureGeometry(validateMeshGeometry(mesh), options.features);
631
1142
  const { scenegraph, materialIndex } = createMeshTileScenegraph(options, geometry);
632
1143
  const meshIndex = scenegraph.addMesh({
633
1144
  attributes: geometry.attributes,
@@ -635,6 +1146,7 @@ function encodeMeshTile(mesh, options = {}) {
635
1146
  material: materialIndex,
636
1147
  mode: 4
637
1148
  });
1149
+ encodeMeshFeatures(scenegraph, meshIndex, options.features);
638
1150
  return finalizeMeshTileScenegraph(scenegraph, meshIndex);
639
1151
  }
640
1152
  function createMeshTileScenegraph(options, geometry) {
@@ -651,14 +1163,14 @@ function finalizeMeshTileScenegraph(scenegraph, meshIndex) {
651
1163
  }
652
1164
  function validateMeshGeometry(mesh, allowFloat64Positions = false) {
653
1165
  if (mesh.topology !== "triangle-list" || mesh.mode !== 4) {
654
- throw new import_core6.TileConversionError(
1166
+ throw new import_core9.TileConversionError(
655
1167
  "MESH_TOPOLOGY_UNSUPPORTED",
656
1168
  "Only triangle-list mode 4 is supported"
657
1169
  );
658
1170
  }
659
1171
  for (const name of Object.keys(mesh.attributes)) {
660
1172
  if (name !== "POSITION" && name !== "NORMAL" && name !== "COLOR_0" && name !== "TEXCOORD_0") {
661
- throw new import_core6.TileConversionError(
1173
+ throw new import_core9.TileConversionError(
662
1174
  "MESH_ATTRIBUTE_UNSUPPORTED",
663
1175
  `Mesh attribute ${name} is unsupported`
664
1176
  );
@@ -671,14 +1183,14 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
671
1183
  if (mesh.attributes.NORMAL) {
672
1184
  const normals = getFloatAttribute(mesh.attributes.NORMAL, "NORMAL");
673
1185
  if (normals.length !== positions.length) {
674
- throw new import_core6.TileConversionError(
1186
+ throw new import_core9.TileConversionError(
675
1187
  "MESH_NORMAL_COUNT_MISMATCH",
676
1188
  "NORMAL count must match POSITION count"
677
1189
  );
678
1190
  }
679
1191
  for (let index = 0; index < normals.length; index += 3) {
680
1192
  if (Math.abs(Math.hypot(normals[index], normals[index + 1], normals[index + 2]) - 1) > 1e-4) {
681
- throw new import_core6.TileConversionError(
1193
+ throw new import_core9.TileConversionError(
682
1194
  "MESH_NORMAL_INVALID",
683
1195
  "NORMAL vectors must have unit length"
684
1196
  );
@@ -690,7 +1202,7 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
690
1202
  const colors = mesh.attributes.COLOR_0;
691
1203
  const normalized = isNormalizedUnsignedAttribute(colors);
692
1204
  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(
1205
+ throw new import_core9.TileConversionError(
694
1206
  "MESH_COLOR_INVALID",
695
1207
  "COLOR_0 requires packed linear RGB/RGBA: Float32 in [0, 1] or normalized Uint8/Uint16, one per vertex"
696
1208
  );
@@ -706,7 +1218,7 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
706
1218
  const textureCoordinates = mesh.attributes.TEXCOORD_0;
707
1219
  const normalized = isNormalizedUnsignedAttribute(textureCoordinates);
708
1220
  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(
1221
+ throw new import_core9.TileConversionError(
710
1222
  "MESH_TEXCOORD_INVALID",
711
1223
  "TEXCOORD_0 requires packed finite Float32 or normalized Uint8/Uint16 UV pairs, one per vertex"
712
1224
  );
@@ -718,9 +1230,9 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
718
1230
  ...normalized ? { normalized: true } : {}
719
1231
  };
720
1232
  }
721
- const indices = getMeshIndices(mesh.indices, positions.length / 3);
1233
+ const indices = validateMeshIndices(mesh.indices, positions.length / 3);
722
1234
  if (!indices && positions.length % 9 !== 0) {
723
- throw new import_core6.TileConversionError(
1235
+ throw new import_core9.TileConversionError(
724
1236
  "MESH_TRIANGLE_COUNT_INVALID",
725
1237
  "Nonindexed meshes require complete triangles"
726
1238
  );
@@ -734,36 +1246,36 @@ function validateMeshGeometry(mesh, allowFloat64Positions = false) {
734
1246
  }
735
1247
  function getFloatAttribute(attribute, name, allowFloat64 = false) {
736
1248
  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(
1249
+ throw new import_core9.TileConversionError(
738
1250
  "MESH_ATTRIBUTE_INVALID",
739
1251
  `${name} must contain packed ${allowFloat64 ? "Float32 or Float64" : "Float32"} xyz triples`
740
1252
  );
741
1253
  }
742
1254
  validateAttributeLayout(attribute, name);
743
1255
  if (attribute.value.some((value) => !Number.isFinite(value))) {
744
- throw new import_core6.TileConversionError("MESH_ATTRIBUTE_NONFINITE", `${name} must contain finite values`);
1256
+ throw new import_core9.TileConversionError("MESH_ATTRIBUTE_NONFINITE", `${name} must contain finite values`);
745
1257
  }
746
1258
  return attribute.value;
747
1259
  }
748
- function getMeshIndices(attribute, vertexCount) {
1260
+ function validateMeshIndices(attribute, vertexCount, primitiveMode = 4) {
749
1261
  if (!attribute) return void 0;
750
1262
  validateAttributeLayout(attribute, "indices");
751
1263
  const values = attribute.value;
752
1264
  if (attribute.size !== 1 || !(values instanceof Uint8Array || values instanceof Uint16Array || values instanceof Uint32Array)) {
753
- throw new import_core6.TileConversionError(
1265
+ throw new import_core9.TileConversionError(
754
1266
  "MESH_INDICES_INVALID",
755
1267
  "Indices must contain packed unsigned integer scalars"
756
1268
  );
757
1269
  }
758
- if (values.length === 0 || values.length % 3 !== 0) {
759
- throw new import_core6.TileConversionError(
1270
+ if (primitiveMode === 5 ? values.length < 3 : values.length === 0 || values.length % 3 !== 0) {
1271
+ throw new import_core9.TileConversionError(
760
1272
  "MESH_TRIANGLE_COUNT_INVALID",
761
- "Indices must contain complete triangles"
1273
+ "Indices must contain complete triangles or at least three strip vertices"
762
1274
  );
763
1275
  }
764
1276
  const restartIndex = 2 ** (values.BYTES_PER_ELEMENT * 8) - 1;
765
1277
  if (values.some((value) => value >= vertexCount || value === restartIndex)) {
766
- throw new import_core6.TileConversionError(
1278
+ throw new import_core9.TileConversionError(
767
1279
  "MESH_INDEX_OUT_OF_RANGE",
768
1280
  "Indices must reference existing vertices and exclude primitive-restart values"
769
1281
  );
@@ -775,7 +1287,7 @@ function isNormalizedUnsignedAttribute(attribute) {
775
1287
  }
776
1288
  function validateAttributeLayout(attribute, name, allowNormalized = false) {
777
1289
  if ((attribute.byteOffset ?? 0) !== 0 || (attribute.byteStride ?? 0) !== 0 || attribute.normalized && !allowNormalized || attribute.transform || attribute.componentType) {
778
- throw new import_core6.TileConversionError(
1290
+ throw new import_core9.TileConversionError(
779
1291
  "MESH_ATTRIBUTE_LAYOUT_UNSUPPORTED",
780
1292
  `${name} must use packed contiguous storage with supported normalization`
781
1293
  );
@@ -783,7 +1295,7 @@ function validateAttributeLayout(attribute, name, allowNormalized = false) {
783
1295
  }
784
1296
  function validateMeshMaterial(material, scenegraph, hasTextureCoordinates) {
785
1297
  if (!material || typeof material !== "object" || Array.isArray(material)) {
786
- throw new import_core6.TileConversionError("MESH_MATERIAL_INVALID", "Material must be an object");
1298
+ throw new import_core9.TileConversionError("MESH_MATERIAL_INVALID", "Material must be an object");
787
1299
  }
788
1300
  const allowedProperties = [
789
1301
  "baseColorFactor",
@@ -796,7 +1308,7 @@ function validateMeshMaterial(material, scenegraph, hasTextureCoordinates) {
796
1308
  ];
797
1309
  for (const name of Object.keys(material)) {
798
1310
  if (!allowedProperties.includes(name)) {
799
- throw new import_core6.TileConversionError(
1311
+ throw new import_core9.TileConversionError(
800
1312
  "MESH_MATERIAL_UNSUPPORTED",
801
1313
  `Material property ${name} is unsupported`
802
1314
  );
@@ -808,7 +1320,7 @@ function validateMeshMaterial(material, scenegraph, hasTextureCoordinates) {
808
1320
  )) || [metallicFactor, roughnessFactor].some(
809
1321
  (value) => value !== void 0 && (!Number.isFinite(value) || value < 0 || value > 1)
810
1322
  ) || 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(
1323
+ throw new import_core9.TileConversionError(
812
1324
  "MESH_MATERIAL_INVALID",
813
1325
  "Material values must satisfy the selected glTF profile"
814
1326
  );
@@ -829,13 +1341,13 @@ function encodeBaseColorTexture(texture, scenegraph, hasTextureCoordinates) {
829
1341
  if (!texture || Object.keys(texture).some(
830
1342
  (name) => name !== "data" && name !== "mimeType" && name !== "sampler" && name !== "transform"
831
1343
  ) || !(texture.data instanceof Uint8Array) || !["image/png", "image/jpeg"].includes(texture.mimeType)) {
832
- throw new import_core6.TileConversionError(
1344
+ throw new import_core9.TileConversionError(
833
1345
  "MESH_TEXTURE_INVALID",
834
1346
  "Base-color texture must supply Uint8Array data, image/png or image/jpeg mimeType, and optional sampling/transform controls"
835
1347
  );
836
1348
  }
837
1349
  if (!hasTextureCoordinates) {
838
- throw new import_core6.TileConversionError(
1350
+ throw new import_core9.TileConversionError(
839
1351
  "MESH_TEXCOORD_REQUIRED",
840
1352
  "Base-color texture requires TEXCOORD_0"
841
1353
  );
@@ -847,7 +1359,7 @@ function encodeBaseColorTexture(texture, scenegraph, hasTextureCoordinates) {
847
1359
  } catch {
848
1360
  }
849
1361
  if (!metadata || metadata.mimeType !== texture.mimeType || metadata.width <= 0 || metadata.height <= 0) {
850
- throw new import_core6.TileConversionError(
1362
+ throw new import_core9.TileConversionError(
851
1363
  "MESH_TEXTURE_INVALID",
852
1364
  "Image header must match mimeType and declare positive dimensions"
853
1365
  );
@@ -865,7 +1377,7 @@ function encodeBaseColorTexture(texture, scenegraph, hasTextureCoordinates) {
865
1377
  }
866
1378
  function validateMeshSampler(sampler) {
867
1379
  if (!sampler || typeof sampler !== "object" || Array.isArray(sampler)) {
868
- throw new import_core6.TileConversionError("MESH_SAMPLER_INVALID", "Sampler must be an object");
1380
+ throw new import_core9.TileConversionError("MESH_SAMPLER_INVALID", "Sampler must be an object");
869
1381
  }
870
1382
  const allowedValues = {
871
1383
  wrapS: [33071, 33648, 10497],
@@ -875,7 +1387,7 @@ function validateMeshSampler(sampler) {
875
1387
  };
876
1388
  for (const name in sampler) {
877
1389
  if (!Object.hasOwn(allowedValues, name)) {
878
- throw new import_core6.TileConversionError(
1390
+ throw new import_core9.TileConversionError(
879
1391
  "MESH_SAMPLER_UNSUPPORTED",
880
1392
  `Sampler property ${name} is unsupported`
881
1393
  );
@@ -890,7 +1402,7 @@ function validateMeshSampler(sampler) {
890
1402
  for (const name of Object.keys(allowedValues)) {
891
1403
  const property = name;
892
1404
  if (selectedSampler[property] !== void 0 && !allowedValues[property].includes(selectedSampler[property])) {
893
- throw new import_core6.TileConversionError(
1405
+ throw new import_core9.TileConversionError(
894
1406
  "MESH_SAMPLER_INVALID",
895
1407
  `Sampler ${name} must use a glTF wrapping/filtering value`
896
1408
  );
@@ -900,14 +1412,14 @@ function validateMeshSampler(sampler) {
900
1412
  }
901
1413
  function validateMeshTextureTransform(transform) {
902
1414
  if (!transform || typeof transform !== "object" || Array.isArray(transform)) {
903
- throw new import_core6.TileConversionError(
1415
+ throw new import_core9.TileConversionError(
904
1416
  "MESH_TEXTURE_TRANSFORM_INVALID",
905
1417
  "Texture transform must be an object"
906
1418
  );
907
1419
  }
908
1420
  for (const name in transform) {
909
1421
  if (name !== "offset" && name !== "rotation" && name !== "scale") {
910
- throw new import_core6.TileConversionError(
1422
+ throw new import_core9.TileConversionError(
911
1423
  "MESH_TEXTURE_TRANSFORM_UNSUPPORTED",
912
1424
  `Texture transform property ${name} is unsupported`
913
1425
  );
@@ -919,7 +1431,7 @@ function validateMeshTextureTransform(transform) {
919
1431
  if ([offsetValues, scaleValues].some(
920
1432
  (value) => value !== void 0 && (!Array.isArray(value) || value.length !== 2 || value.some((component) => !Number.isFinite(component)))
921
1433
  ) || rotation !== void 0 && !Number.isFinite(rotation)) {
922
- throw new import_core6.TileConversionError(
1434
+ throw new import_core9.TileConversionError(
923
1435
  "MESH_TEXTURE_TRANSFORM_INVALID",
924
1436
  "Texture transform requires finite UV pairs and a finite rotation"
925
1437
  );
@@ -932,15 +1444,15 @@ function validateMeshTextureTransform(transform) {
932
1444
  }
933
1445
 
934
1446
  // src/v5/mesh-conversion.ts
935
- var import_tiles = require("@loaders.gl/tiles");
936
- var import_core8 = require("@loaders.gl/tile-converter/v5/core");
1447
+ var import_tiles2 = require("@loaders.gl/tiles");
1448
+ var import_core11 = require("@loaders.gl/tile-converter/v5/core");
937
1449
 
938
1450
  // src/v5/mesh-draco.ts
939
1451
  var import_draco = require("@loaders.gl/draco");
940
- var import_core7 = require("@loaders.gl/tile-converter/v5/core");
1452
+ var import_core10 = require("@loaders.gl/tile-converter/v5/core");
941
1453
  var DRACO_EXTENSION = "KHR_draco_mesh_compression";
942
1454
  async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
943
- geometry = validateMeshGeometry(geometry);
1455
+ geometry = prepareMeshFeatureGeometry(validateMeshGeometry(geometry), options.features);
944
1456
  if (!geometry.indices) {
945
1457
  geometry.indices = {
946
1458
  size: 1,
@@ -962,7 +1474,7 @@ async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
962
1474
  const decoded = await loader.parse(encoded.data, loaderOptions);
963
1475
  const positions = decoded.attributes.POSITION.value;
964
1476
  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(
1477
+ throw new import_core10.TileConversionError(
966
1478
  "MESH_DRACO_ENCODING_INVALID",
967
1479
  "Draco must preserve the triangle count and emit finite positions"
968
1480
  );
@@ -974,12 +1486,18 @@ async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
974
1486
  }
975
1487
  for (let index = 0; index < positions.length; index += 3) {
976
1488
  if (!positionKeys.has(getPositionKey(positions, index))) {
977
- throw new import_core7.TileConversionError(
1489
+ throw new import_core10.TileConversionError(
978
1490
  "MESH_DRACO_POSITION_CHANGED",
979
1491
  "Lossless Draco must preserve the encoded Float32 positions"
980
1492
  );
981
1493
  }
982
1494
  }
1495
+ if (JSON.stringify(getMeshTriangles(geometry)) !== JSON.stringify(getMeshTriangles(decoded))) {
1496
+ throw new import_core10.TileConversionError(
1497
+ "MESH_DRACO_ATTRIBUTE_CHANGED",
1498
+ "Lossless Draco must preserve oriented triangles and every attribute, including feature ownership"
1499
+ );
1500
+ }
983
1501
  const bufferView = scenegraph.addBufferView(encoded.data);
984
1502
  const attributes = {};
985
1503
  const attributeIdentifiers = {};
@@ -987,12 +1505,12 @@ async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
987
1505
  for (const [name, attribute] of Object.entries(geometry.attributes)) {
988
1506
  const decodedAttribute = decoded.attributes[name];
989
1507
  if (!decodedAttribute || decodedAttribute.value.length !== encoded.report.pointCount * attribute.size) {
990
- throw new import_core7.TileConversionError("MESH_DRACO_ENCODING_INVALID", `Draco must preserve ${name}`);
1508
+ throw new import_core10.TileConversionError("MESH_DRACO_ENCODING_INVALID", `Draco must preserve ${name}`);
991
1509
  }
992
1510
  const accessor = {
993
- componentType: attribute.value instanceof Uint8Array ? 5121 : attribute.value instanceof Uint16Array ? 5123 : 5126,
1511
+ componentType: attribute.value instanceof Uint8Array ? 5121 : attribute.value instanceof Uint16Array ? 5123 : attribute.value instanceof Uint32Array ? 5125 : 5126,
994
1512
  count: encoded.report.pointCount,
995
- type: attribute.size === 2 ? "VEC2" : attribute.size === 3 ? "VEC3" : "VEC4",
1513
+ type: attribute.size === 1 ? "SCALAR" : attribute.size === 2 ? "VEC2" : attribute.size === 3 ? "VEC3" : "VEC4",
996
1514
  ...attribute.normalized ? { normalized: true } : {}
997
1515
  };
998
1516
  if (name === "POSITION") {
@@ -1027,30 +1545,51 @@ async function encodeDracoMeshTile(geometry, options, libraryOptions = {}) {
1027
1545
  ]
1028
1546
  }
1029
1547
  ];
1548
+ encodeMeshFeatures(scenegraph, 0, options.features);
1030
1549
  scenegraph.registerRequiredExtension(DRACO_EXTENSION);
1031
1550
  return { glb: finalizeMeshTileScenegraph(scenegraph, 0), positions };
1032
1551
  }
1033
1552
  function getPositionKey(positions, index) {
1034
1553
  return `${positions[index]},${positions[index + 1]},${positions[index + 2]}`;
1035
1554
  }
1555
+ function getMeshTriangles(mesh) {
1556
+ const names = Object.keys(mesh.attributes).sort();
1557
+ const indices = mesh.indices.value;
1558
+ const triangles = [];
1559
+ for (let index = 0; index < indices.length; index += 3) {
1560
+ const vertices = [0, 1, 2].map(
1561
+ (corner) => names.map((name) => {
1562
+ const attribute = mesh.attributes[name];
1563
+ return Array.from(
1564
+ { length: attribute.size },
1565
+ (_, component) => attribute.value[indices[index + corner] * attribute.size + component]
1566
+ ).join(",");
1567
+ }).join("|")
1568
+ );
1569
+ triangles.push(
1570
+ [0, 1, 2].map((corner) => [...vertices.slice(corner), ...vertices.slice(0, corner)].join(";")).sort()[0]
1571
+ );
1572
+ }
1573
+ return triangles.sort();
1574
+ }
1036
1575
 
1037
1576
  // src/v5/mesh-conversion.ts
1038
1577
  function createMeshConversionCodec(options) {
1039
1578
  const { spatialContext, maxPositionError, draco = true, dracoLibraryOptions } = options;
1040
1579
  if (typeof draco !== "boolean") {
1041
- throw new import_core8.TileConversionError("MESH_DRACO_OPTIONS_INVALID", "draco must be a boolean");
1580
+ throw new import_core11.TileConversionError("MESH_DRACO_OPTIONS_INVALID", "draco must be a boolean");
1042
1581
  }
1043
1582
  if (!Number.isFinite(maxPositionError) || maxPositionError < 0) {
1044
- throw new import_core8.TileConversionError(
1583
+ throw new import_core11.TileConversionError(
1045
1584
  "MESH_PRECISION_LIMIT_INVALID",
1046
1585
  "maxPositionError must be finite and nonnegative"
1047
1586
  );
1048
1587
  }
1049
1588
  const spatialReference = spatialContext.spatialReference;
1050
1589
  const outputCrs = spatialReference.targetCrs || spatialReference.sourceCrs;
1051
- const outputFrame = spatialReference.status === "native" ? spatialReference.coordinateFrame : outputCrs ? (0, import_tiles.getSpatialCoordinateFrame)(outputCrs) : "unknown";
1590
+ const outputFrame = spatialReference.status === "native" ? spatialReference.coordinateFrame : outputCrs ? (0, import_tiles2.getSpatialCoordinateFrame)(outputCrs) : "unknown";
1052
1591
  if (!["native", "transformed"].includes(spatialReference.status) || !["projected", "geocentric", "local", "cartesian"].includes(outputFrame)) {
1053
- throw new import_core8.TileConversionError(
1592
+ throw new import_core11.TileConversionError(
1054
1593
  "MESH_OUTPUT_FRAME_UNSUPPORTED",
1055
1594
  "Mesh output requires a resolved Cartesian coordinate frame"
1056
1595
  );
@@ -1059,28 +1598,41 @@ function createMeshConversionCodec(options) {
1059
1598
  /** Prepares and encodes one geometry while honoring cancellation. */
1060
1599
  async *convert(resource, _inspection, signal) {
1061
1600
  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
1601
  const geometry = validateMeshGeometry(resource.mesh, true);
1069
1602
  const sourcePositions = geometry.attributes.POSITION.value;
1070
1603
  const origin = [...resource.origin];
1071
1604
  if (origin.length !== 3 || origin.some((value) => !Number.isFinite(value))) {
1072
- throw new import_core8.TileConversionError(
1605
+ throw new import_core11.TileConversionError(
1073
1606
  "MESH_ORIGIN_INVALID",
1074
1607
  "Target origin must contain three finite coordinates"
1075
1608
  );
1076
1609
  }
1077
- const targetPositions = spatialContext.transformPositions(sourcePositions);
1610
+ const transformed = "transformGeometryAsync" in spatialContext ? await spatialContext.transformGeometryAsync(
1611
+ sourcePositions,
1612
+ geometry.attributes.NORMAL?.value,
1613
+ resource.normalReferenceFrame ?? options.normalReferenceFrame
1614
+ ) : {
1615
+ positions: spatialContext.transformPositions(sourcePositions),
1616
+ normals: geometry.attributes.NORMAL ? spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions) : void 0
1617
+ };
1618
+ signal?.throwIfAborted();
1619
+ const targetPositions = transformed.positions;
1078
1620
  if (targetPositions.length !== sourcePositions.length || targetPositions.some((value) => !Number.isFinite(value))) {
1079
- throw new import_core8.TileConversionError(
1621
+ throw new import_core11.TileConversionError(
1080
1622
  "MESH_TRANSFORM_INVALID",
1081
1623
  "Spatial transformation must return one finite xyz position per vertex"
1082
1624
  );
1083
1625
  }
1626
+ if (options.autoOrigin) {
1627
+ const minimum2 = [Infinity, Infinity, Infinity];
1628
+ const maximum2 = [-Infinity, -Infinity, -Infinity];
1629
+ for (let index = 0; index < targetPositions.length; index++) {
1630
+ const axis = index % 3;
1631
+ minimum2[axis] = Math.min(minimum2[axis], targetPositions[index]);
1632
+ maximum2[axis] = Math.max(maximum2[axis], targetPositions[index]);
1633
+ }
1634
+ for (let axis = 0; axis < 3; axis++) origin[axis] = minimum2[axis] / 2 + maximum2[axis] / 2;
1635
+ }
1084
1636
  const localPositions = new Float32Array(targetPositions.length);
1085
1637
  const minimum = [Infinity, Infinity, Infinity];
1086
1638
  const maximum = [-Infinity, -Infinity, -Infinity];
@@ -1097,7 +1649,7 @@ function createMeshConversionCodec(options) {
1097
1649
  localPositions[index + 2] + origin[2] - targetPositions[index + 2]
1098
1650
  );
1099
1651
  if (!Number.isFinite(positionError) || positionError > maxPositionError) {
1100
- throw new import_core8.TileConversionError(
1652
+ throw new import_core11.TileConversionError(
1101
1653
  "MESH_POSITION_PRECISION_EXCEEDED",
1102
1654
  "Float32 mesh positions exceed the configured reconstruction error budget"
1103
1655
  );
@@ -1107,11 +1659,11 @@ function createMeshConversionCodec(options) {
1107
1659
  geometry.attributes.POSITION = { value: localPositions, size: 3 };
1108
1660
  if (geometry.attributes.NORMAL) {
1109
1661
  geometry.attributes.NORMAL = {
1110
- value: spatialContext.transformNormals(geometry.attributes.NORMAL.value, sourcePositions),
1662
+ value: transformed.normals,
1111
1663
  size: 3
1112
1664
  };
1113
1665
  }
1114
- const appearance = { material: resource.material };
1666
+ const appearance = { material: resource.material, features: resource.features };
1115
1667
  const encoded = draco ? await encodeDracoMeshTile(geometry, appearance, dracoLibraryOptions) : { glb: encodeMeshTile(geometry, appearance), positions: localPositions };
1116
1668
  signal?.throwIfAborted();
1117
1669
  for (let index = 0; index < encoded.positions.length; index++) {
@@ -1147,17 +1699,17 @@ function createMeshConversionCodec(options) {
1147
1699
  }
1148
1700
 
1149
1701
  // 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");
1702
+ var import_core12 = require("@loaders.gl/tile-converter/v5/core");
1703
+ var import_core13 = require("@loaders.gl/tile-converter/v5/core");
1152
1704
  function createSingleMeshTilesetSink(options) {
1153
1705
  const { geometricError } = options;
1154
1706
  if (!Number.isFinite(geometricError) || geometricError < 0) {
1155
- throw new import_core9.TileConversionError(
1707
+ throw new import_core12.TileConversionError(
1156
1708
  "MESH_GEOMETRIC_ERROR_INVALID",
1157
1709
  "geometricError must be finite and nonnegative, in meters"
1158
1710
  );
1159
1711
  }
1160
- const memorySink = (0, import_core10.createBoundedMemoryTileConversionSink)(options);
1712
+ const memorySink = (0, import_core13.createBoundedMemoryTileConversionSink)(options);
1161
1713
  let state = "empty";
1162
1714
  let completed = false;
1163
1715
  return {
@@ -1167,7 +1719,7 @@ function createSingleMeshTilesetSink(options) {
1167
1719
  async write(resource, signal) {
1168
1720
  signal?.throwIfAborted();
1169
1721
  if (state !== "empty") {
1170
- throw new import_core9.TileConversionError(
1722
+ throw new import_core12.TileConversionError(
1171
1723
  "SINGLE_MESH_SINK_UNAVAILABLE",
1172
1724
  "The sink accepts exactly one mesh before finalization or abort"
1173
1725
  );
@@ -1186,7 +1738,7 @@ function createSingleMeshTilesetSink(options) {
1186
1738
  },
1187
1739
  async finalize(report) {
1188
1740
  if (state !== "written") {
1189
- throw new import_core9.TileConversionError(
1741
+ throw new import_core12.TileConversionError(
1190
1742
  "SINGLE_MESH_SINK_INCOMPLETE",
1191
1743
  "Finalization requires exactly one successfully written mesh"
1192
1744
  );
@@ -1207,13 +1759,13 @@ function createMeshTileset(resource, geometricError) {
1207
1759
  const outputCrs = reference.targetCrs || reference.sourceCrs;
1208
1760
  const heightReference = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
1209
1761
  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(
1762
+ throw new import_core12.TileConversionError(
1211
1763
  "MESH_TILESET_FRAME_UNSUPPORTED",
1212
1764
  "Single-mesh tilesets require resolved EPSG:4978 xyz coordinates in meters with ellipsoidal heights"
1213
1765
  );
1214
1766
  }
1215
1767
  if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
1216
- throw new import_core9.TileConversionError(
1768
+ throw new import_core12.TileConversionError(
1217
1769
  "MESH_GEOMETRIC_ERROR_INVALID",
1218
1770
  "geometricError must include the measured position reconstruction error"
1219
1771
  );
@@ -1221,7 +1773,7 @@ function createMeshTileset(resource, geometricError) {
1221
1773
  const origin = [...resource.origin];
1222
1774
  const [minimum, maximum] = resource.localBoundingBox.map((bound) => [...bound]);
1223
1775
  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(
1776
+ throw new import_core12.TileConversionError(
1225
1777
  "MESH_TILESET_BOUNDS_INVALID",
1226
1778
  "Origin and local bounds must be finite xyz coordinates with ordered bounds"
1227
1779
  );
@@ -1260,12 +1812,12 @@ function createMeshTileset(resource, geometricError) {
1260
1812
  function createMeshTilesetSink(options) {
1261
1813
  const { geometricError, maxMeshes } = options;
1262
1814
  if (!Number.isFinite(geometricError) || geometricError < 0 || !Number.isSafeInteger(maxMeshes) || maxMeshes < 1) {
1263
- throw new import_core9.TileConversionError(
1815
+ throw new import_core12.TileConversionError(
1264
1816
  "MESH_TILESET_OPTIONS_INVALID",
1265
1817
  "A leaf collection requires finite nonnegative geometricError and positive maxMeshes"
1266
1818
  );
1267
1819
  }
1268
- const memory = (0, import_core10.createBoundedMemoryTileConversionSink)(options);
1820
+ const memory = (0, import_core13.createBoundedMemoryTileConversionSink)(options);
1269
1821
  const children = [];
1270
1822
  const identifiers = /* @__PURE__ */ new Set();
1271
1823
  const minimum = [Infinity, Infinity, Infinity];
@@ -1279,12 +1831,12 @@ function createMeshTilesetSink(options) {
1279
1831
  async write(resource, signal) {
1280
1832
  signal?.throwIfAborted();
1281
1833
  if (state !== "open")
1282
- throw new import_core9.TileConversionError(
1834
+ throw new import_core12.TileConversionError(
1283
1835
  "MESH_TILESET_SINK_UNAVAILABLE",
1284
1836
  "The mesh sink is busy or closed"
1285
1837
  );
1286
1838
  if (children.length >= maxMeshes || identifiers.has(resource.id))
1287
- throw new import_core9.TileConversionError(
1839
+ throw new import_core12.TileConversionError(
1288
1840
  "MESH_TILESET_PLACEMENT_INVALID",
1289
1841
  "Mesh count exceeds maxMeshes or a placement identifier is repeated"
1290
1842
  );
@@ -1296,7 +1848,7 @@ function createMeshTilesetSink(options) {
1296
1848
  (value, axis) => value + resource.origin[axis]
1297
1849
  );
1298
1850
  if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
1299
- throw new import_core9.TileConversionError(
1851
+ throw new import_core12.TileConversionError(
1300
1852
  "MESH_TILESET_BOUNDS_INVALID",
1301
1853
  "Absolute bounds must be finite"
1302
1854
  );
@@ -1318,7 +1870,7 @@ function createMeshTilesetSink(options) {
1318
1870
  /** Writes the enclosing root only when every leaf has completed. */
1319
1871
  async finalize(report) {
1320
1872
  if (state !== "open" || !children.length)
1321
- throw new import_core9.TileConversionError(
1873
+ throw new import_core12.TileConversionError(
1322
1874
  "MESH_TILESET_SINK_INCOMPLETE",
1323
1875
  "Finalization requires written mesh leaves"
1324
1876
  );
@@ -1327,7 +1879,7 @@ function createMeshTilesetSink(options) {
1327
1879
  const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
1328
1880
  const rootGeometricError = Math.max(geometricError, 2 * Math.hypot(...halfSize));
1329
1881
  if (!Number.isFinite(rootGeometricError))
1330
- throw new import_core9.TileConversionError(
1882
+ throw new import_core12.TileConversionError(
1331
1883
  "MESH_TILESET_BOUNDS_INVALID",
1332
1884
  "Root bounds must have a finite diagonal"
1333
1885
  );
@@ -1379,7 +1931,7 @@ function createMeshTilesetSink(options) {
1379
1931
  // src/v5/conversion-archive.ts
1380
1932
  var import_d_tiles_archive_writer = require("@loaders.gl/3d-tiles/3d-tiles-archive-writer");
1381
1933
  var import_i3s_slpk_writer = require("@loaders.gl/i3s/i3s-slpk-writer");
1382
- var import_core11 = require("@loaders.gl/tile-converter/v5/core");
1934
+ var import_core14 = require("@loaders.gl/tile-converter/v5/core");
1383
1935
  async function createTileConversionArchive(files, options) {
1384
1936
  const { resources, format, maxArchiveBytes, signal } = prepareArchiveInput(files, options);
1385
1937
  const writer = format === "3tz" ? import_d_tiles_archive_writer.Tiles3DArchiveWriter : import_i3s_slpk_writer.SLPKWriter;
@@ -1394,17 +1946,17 @@ async function* encodeTileConversionArchiveInBatches(files, options) {
1394
1946
  function prepareArchiveInput(files, options) {
1395
1947
  const { format, maxArchiveBytes, signal } = options;
1396
1948
  if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
1397
- throw new import_core11.TileConversionError(
1949
+ throw new import_core14.TileConversionError(
1398
1950
  "INVALID_ARCHIVE_BYTE_LIMIT",
1399
1951
  "Archive byte limit must be a non-negative safe integer"
1400
1952
  );
1401
1953
  }
1402
1954
  if (format !== "3tz" && format !== "slpk") {
1403
- throw new import_core11.TileConversionError("INVALID_ARCHIVE_FORMAT", "Expected 3tz or slpk archive format");
1955
+ throw new import_core14.TileConversionError("INVALID_ARCHIVE_FORMAT", "Expected 3tz or slpk archive format");
1404
1956
  }
1405
1957
  const resources = Object.fromEntries(files.map((file) => [file.resourceId, file.blob]));
1406
1958
  if (Object.keys(resources).length !== files.length) {
1407
- throw new import_core11.TileConversionError(
1959
+ throw new import_core14.TileConversionError(
1408
1960
  "DUPLICATE_ARCHIVE_RESOURCE",
1409
1961
  "Archive resource identifiers must be unique"
1410
1962
  );
@@ -1414,17 +1966,17 @@ function prepareArchiveInput(files, options) {
1414
1966
 
1415
1967
  // src/v5/mesh-tileset-archive.ts
1416
1968
  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");
1969
+ var import_core15 = require("@loaders.gl/tile-converter/v5/core");
1418
1970
  async function createSingleMeshTilesetArchive(files, options) {
1419
1971
  const { maxArchiveBytes, signal } = options;
1420
1972
  if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
1421
- throw new import_core12.TileConversionError(
1973
+ throw new import_core15.TileConversionError(
1422
1974
  "INVALID_ARCHIVE_BYTE_LIMIT",
1423
1975
  "Archive byte limit must be a non-negative safe integer"
1424
1976
  );
1425
1977
  }
1426
1978
  if (files.length !== 2 || !files.some((file) => file.resourceId === "mesh.glb") || !files.some((file) => file.resourceId === "tileset.json")) {
1427
- throw new import_core12.TileConversionError(
1979
+ throw new import_core15.TileConversionError(
1428
1980
  "INVALID_SINGLE_MESH_ARCHIVE",
1429
1981
  "Expected mesh.glb and tileset.json from a finalized single-mesh sink"
1430
1982
  );
@@ -1435,12 +1987,13 @@ async function createSingleMeshTilesetArchive(files, options) {
1435
1987
  }
1436
1988
 
1437
1989
  // 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");
1990
+ var import_core18 = require("@math.gl/core");
1991
+ var import_core19 = require("@loaders.gl/tile-converter/v5/core");
1992
+ var import_core20 = require("@loaders.gl/tile-converter/v5/core");
1441
1993
 
1442
1994
  // src/v5/mesh-source-texture.ts
1443
- var import_core13 = require("@loaders.gl/tile-converter/v5/core");
1995
+ var import_images2 = require("@loaders.gl/images");
1996
+ var import_core16 = require("@loaders.gl/tile-converter/v5/core");
1444
1997
  function mapMeshSourceTexture(textureInfo) {
1445
1998
  const texture = textureInfo?.texture;
1446
1999
  const image = texture?.source;
@@ -1467,7 +2020,7 @@ function mapMeshSourceTexture(textureInfo) {
1467
2020
  "extras"
1468
2021
  ].includes(name)
1469
2022
  ))) {
1470
- throw new import_core13.TileConversionError(
2023
+ throw new import_core16.TileConversionError(
1471
2024
  "MESH_SOURCE_TEXTURE_UNSUPPORTED",
1472
2025
  "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
2026
  );
@@ -1496,7 +2049,7 @@ function mapMeshSourceImage(image) {
1496
2049
  }
1497
2050
  const match = typeof image.uri === "string" ? /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/]+={0,2})$/.exec(image.uri) : null;
1498
2051
  if (!match || image.mimeType !== void 0 && image.mimeType !== match[1]) {
1499
- throw new import_core13.TileConversionError(
2052
+ throw new import_core16.TileConversionError(
1500
2053
  "MESH_SOURCE_TEXTURE_UNSUPPORTED",
1501
2054
  "Inline images require a PNG/JPEG base64 data URI and matching optional mimeType"
1502
2055
  );
@@ -1505,35 +2058,66 @@ function mapMeshSourceImage(image) {
1505
2058
  try {
1506
2059
  encodedBytes = atob(match[2]);
1507
2060
  } catch {
1508
- throw new import_core13.TileConversionError("MESH_SOURCE_TEXTURE_UNSUPPORTED", "Invalid inline image base64");
2061
+ throw new import_core16.TileConversionError("MESH_SOURCE_TEXTURE_UNSUPPORTED", "Invalid inline image base64");
1509
2062
  }
1510
2063
  return {
1511
2064
  data: Uint8Array.from(encodedBytes, (character) => character.charCodeAt(0)),
1512
2065
  mimeType: match[1]
1513
2066
  };
1514
2067
  }
2068
+ async function resolveMeshSourceTexture(textureInfo, readExternalResource, contentUri, signal) {
2069
+ const image = textureInfo?.texture?.source;
2070
+ if (!image || image.uri === void 0 || image.uri.startsWith("data:"))
2071
+ return mapMeshSourceTexture(textureInfo);
2072
+ if (!readExternalResource || image.bufferView !== void 0)
2073
+ throw new import_core16.TileConversionError(
2074
+ "MESH_SOURCE_TEXTURE_UNSUPPORTED",
2075
+ "External images require an explicit bounded resource reader"
2076
+ );
2077
+ signal?.throwIfAborted();
2078
+ const data = await readExternalResource(image.uri, contentUri, signal);
2079
+ signal?.throwIfAborted();
2080
+ let metadata;
2081
+ try {
2082
+ metadata = (0, import_images2.getBinaryImageMetadata)(new DataView(data.buffer, data.byteOffset, data.byteLength));
2083
+ } catch {
2084
+ }
2085
+ if (!metadata || !["image/png", "image/jpeg"].includes(metadata.mimeType) || image.mimeType !== void 0 && image.mimeType !== metadata.mimeType)
2086
+ throw new import_core16.TileConversionError(
2087
+ "MESH_SOURCE_TEXTURE_UNSUPPORTED",
2088
+ "External images require matching PNG/JPEG headers"
2089
+ );
2090
+ const resolved = {
2091
+ ...textureInfo,
2092
+ texture: {
2093
+ ...textureInfo.texture,
2094
+ source: { ...image, uri: void 0, mimeType: metadata.mimeType, bufferView: { data } }
2095
+ }
2096
+ };
2097
+ return mapMeshSourceTexture(resolved);
2098
+ }
1515
2099
 
1516
2100
  // src/v5/mesh-source-features.ts
1517
- var import_core14 = require("@loaders.gl/tile-converter/v5/core");
2101
+ var import_core17 = require("@loaders.gl/tile-converter/v5/core");
1518
2102
  function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
1519
2103
  const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
1520
2104
  const metadata = gltf.extensions?.EXT_structural_metadata;
1521
2105
  const legacy = primitive.attributes._BATCHID;
1522
2106
  if (!declarations && !legacy && !metadata && !Object.keys(payload.batchTableJson || {}).length && !payload.header?.batchLength) {
1523
2107
  if (Object.keys(primitive.attributes).some((name) => /^_FEATURE_ID_\d+$/.test(name)))
1524
- throw new import_core14.TileConversionError(
2108
+ throw new import_core17.TileConversionError(
1525
2109
  "MESH_FEATURE_MAPPING_REQUIRED",
1526
2110
  "Feature attributes require an EXT_mesh_features declaration"
1527
2111
  );
1528
2112
  return void 0;
1529
2113
  }
1530
2114
  if (!options)
1531
- throw new import_core14.TileConversionError(
2115
+ throw new import_core17.TileConversionError(
1532
2116
  "MESH_FEATURE_SCHEMA_REQUIRED",
1533
2117
  "Feature extraction requires an explicit Arrow schema and class mapping"
1534
2118
  );
1535
2119
  if (!options.metadataClass)
1536
- throw new import_core14.TileConversionError(
2120
+ throw new import_core17.TileConversionError(
1537
2121
  "MESH_FEATURE_CLASS_REQUIRED",
1538
2122
  "Feature metadata class must be explicit"
1539
2123
  );
@@ -1542,20 +2126,20 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
1542
2126
  let properties;
1543
2127
  if (declarations) {
1544
2128
  if (legacy || declarations.length !== 1 || metadata?.propertyTables?.length !== 1 || metadata.propertyTextures?.length || metadata.propertyAttributes?.length)
1545
- throw new import_core14.TileConversionError(
2129
+ throw new import_core17.TileConversionError(
1546
2130
  "MESH_FEATURE_MAPPING_UNSUPPORTED",
1547
2131
  "Only one attribute-backed feature set and one property table are supported"
1548
2132
  );
1549
2133
  const declaration = declarations[0];
1550
2134
  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(
2135
+ throw new import_core17.TileConversionError(
1552
2136
  "MESH_FEATURE_MAPPING_UNSUPPORTED",
1553
2137
  "Features require an attribute and property table 0 without null/texture/implicit IDs"
1554
2138
  );
1555
2139
  attribute = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
1556
2140
  const table = metadata.propertyTables[0];
1557
2141
  if (table.class !== options.metadataClass || table.count !== declaration.featureCount)
1558
- throw new import_core14.TileConversionError(
2142
+ throw new import_core17.TileConversionError(
1559
2143
  "MESH_FEATURE_CLASS_MISMATCH",
1560
2144
  "Feature class and declared table counts must agree"
1561
2145
  );
@@ -1563,33 +2147,49 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
1563
2147
  properties = {};
1564
2148
  const classProperties = metadata.schema?.classes?.[table.class]?.properties;
1565
2149
  if (!classProperties)
1566
- throw new import_core14.TileConversionError(
2150
+ throw new import_core17.TileConversionError(
1567
2151
  "MESH_FEATURE_SCHEMA_REQUIRED",
1568
2152
  "A decoded inline structural metadata class is required"
1569
2153
  );
1570
2154
  if (Object.keys(table.properties || {}).some((name) => !Object.hasOwn(classProperties, name)))
1571
- throw new import_core14.TileConversionError(
2155
+ throw new import_core17.TileConversionError(
1572
2156
  "MESH_FEATURE_PROPERTY_UNMAPPED",
1573
2157
  "Property table columns must be declared in the metadata class"
1574
2158
  );
1575
2159
  for (const [name, property] of Object.entries(classProperties)) {
1576
2160
  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(
2161
+ 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))
2162
+ throw new import_core17.TileConversionError(
1579
2163
  "MESH_FEATURE_PROPERTY_UNSUPPORTED",
1580
2164
  `Property ${name} requires a separately qualified semantic mapping`
1581
2165
  );
1582
2166
  const data = column?.data;
1583
2167
  if (data === void 0 && property.default === void 0)
1584
- throw new import_core14.TileConversionError(
2168
+ throw new import_core17.TileConversionError(
1585
2169
  "MESH_FEATURE_PROPERTY_UNAVAILABLE",
1586
2170
  `Property ${name} has not been decoded`
1587
2171
  );
1588
- properties[name] = data ?? Array.from({ length: count }, () => property.default);
2172
+ const decoded = data ?? Array.from({ length: count }, () => property.default);
2173
+ if (property.noData === void 0 || data === void 0) {
2174
+ properties[name] = decoded;
2175
+ continue;
2176
+ }
2177
+ const sentinel = property.noData;
2178
+ const integer64 = ["INT64", "UINT64"].includes(property.componentType || "");
2179
+ 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)
2180
+ throw new import_core17.TileConversionError(
2181
+ "MESH_FEATURE_PROPERTY_UNSUPPORTED",
2182
+ `Property ${name} requires a valid decoded noData mapping`
2183
+ );
2184
+ const comparable = integer64 ? BigInt(sentinel) : sentinel;
2185
+ properties[name] = Array.from(
2186
+ decoded,
2187
+ (value) => value === comparable ? property.default ?? null : value
2188
+ );
1589
2189
  }
1590
2190
  } else {
1591
2191
  if (metadata)
1592
- throw new import_core14.TileConversionError(
2192
+ throw new import_core17.TileConversionError(
1593
2193
  "MESH_FEATURE_MAPPING_UNSUPPORTED",
1594
2194
  "Structural metadata requires an attribute-backed feature declaration"
1595
2195
  );
@@ -1600,7 +2200,7 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
1600
2200
  if (!Number.isSafeInteger(count) || count <= 0 || !attribute || attribute.components !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(attribute.value).some(
1601
2201
  (value) => !Number.isSafeInteger(value) || value < 0 || value >= count
1602
2202
  ))
1603
- throw new import_core14.TileConversionError(
2203
+ throw new import_core17.TileConversionError(
1604
2204
  "MESH_FEATURE_IDS_INVALID",
1605
2205
  "Feature IDs must be one valid property-table row index per vertex"
1606
2206
  );
@@ -1608,20 +2208,20 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
1608
2208
  if (Object.keys(primitive.attributes).some(
1609
2209
  (name) => (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) && name !== selectedAttribute
1610
2210
  ))
1611
- throw new import_core14.TileConversionError(
2211
+ throw new import_core17.TileConversionError(
1612
2212
  "MESH_FEATURE_MAPPING_UNSUPPORTED",
1613
2213
  "Additional feature ID sets require separate mappings"
1614
2214
  );
1615
2215
  for (const [name, values] of Object.entries(properties)) {
1616
2216
  if (!values || !Array.isArray(values) && !ArrayBuffer.isView(values) || values.length !== count)
1617
- throw new import_core14.TileConversionError(
2217
+ throw new import_core17.TileConversionError(
1618
2218
  "MESH_FEATURE_PROPERTY_UNAVAILABLE",
1619
2219
  `Property ${name} must be a decoded column matching the table count`
1620
2220
  );
1621
2221
  }
1622
2222
  const sourceIdentifier = options.sourceFeatureIdProperty;
1623
2223
  if (sourceIdentifier && !properties[sourceIdentifier])
1624
- throw new import_core14.TileConversionError(
2224
+ throw new import_core17.TileConversionError(
1625
2225
  "MESH_FEATURE_ID_PROPERTY_REQUIRED",
1626
2226
  "Stable source identifier property is missing"
1627
2227
  );
@@ -1638,13 +2238,14 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
1638
2238
  const indices = mesh.indices?.value;
1639
2239
  const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
1640
2240
  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]
2241
+ const corners = [0, 1, 2].map(
2242
+ (corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
1643
2243
  );
1644
- if (rows.some((row) => row !== rows[0]))
1645
- throw new import_core14.TileConversionError(
2244
+ const rows = corners.map((corner) => attribute.value[corner]);
2245
+ if (rows.some((row) => row !== rows[0]) && new Set(corners).size === 3)
2246
+ throw new import_core17.TileConversionError(
1646
2247
  "MESH_FEATURE_TRIANGLE_MIXED",
1647
- "All vertices of a triangle must reference the same feature row"
2248
+ "All vertices of a nondegenerate triangle must reference the same feature row"
1648
2249
  );
1649
2250
  triangleFeatureIndices[triangle] = Number(rows[0]);
1650
2251
  }
@@ -1658,20 +2259,20 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
1658
2259
 
1659
2260
  // src/v5/mesh-source.ts
1660
2261
  function createMeshTilesetConversionSource(tileset, options = {}) {
1661
- const source = (0, import_core17.createTilesetConversionSource)(tileset, options);
2262
+ const source = (0, import_core20.createTilesetConversionSource)(tileset, options);
1662
2263
  return {
1663
2264
  /** Validates the source frame and supported dataset metadata before traversal. */
1664
2265
  async inspect(signal) {
1665
2266
  const metadata = await source.inspect(signal);
1666
2267
  const reference = metadata.spatialReference;
1667
2268
  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(
2269
+ throw new import_core19.TileConversionError(
1669
2270
  "MESH_SOURCE_FRAME_UNSUPPORTED",
1670
2271
  "Mesh extraction requires native EPSG:4978 xyz coordinates in meters and ellipsoidal heights"
1671
2272
  );
1672
2273
  }
1673
2274
  if (metadata.tileset.metadata || metadata.tileset.groups?.length) {
1674
- throw new import_core16.TileConversionError(
2275
+ throw new import_core19.TileConversionError(
1675
2276
  "MESH_SOURCE_METADATA_UNSUPPORTED",
1676
2277
  "Tileset/group metadata requires a separate mapping"
1677
2278
  );
@@ -1683,25 +2284,25 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
1683
2284
  for await (const item of source.read(inspection, signal)) {
1684
2285
  const placementReference = item.tile.header._spatialReference || tileset.spatialReference;
1685
2286
  if (placementReference && (placementReference.status !== "native" || placementReference.sourceCrs !== "EPSG:4978"))
1686
- throw new import_core16.TileConversionError(
2287
+ throw new import_core19.TileConversionError(
1687
2288
  "MESH_SOURCE_FRAME_UNSUPPORTED",
1688
2289
  "Nested mesh placements require native ECEF coordinates"
1689
2290
  );
1690
2291
  if (item.tile.header.metadata || item.tile.header.implicitMetadata)
1691
- throw new import_core16.TileConversionError(
2292
+ throw new import_core19.TileConversionError(
1692
2293
  "MESH_SOURCE_METADATA_UNSUPPORTED",
1693
2294
  "Tile metadata requires a separate mapping"
1694
2295
  );
1695
2296
  for (const content of item.contents) {
1696
2297
  signal?.throwIfAborted();
1697
2298
  if (content.metadata || content.group !== void 0)
1698
- throw new import_core16.TileConversionError(
2299
+ throw new import_core19.TileConversionError(
1699
2300
  "MESH_SOURCE_METADATA_UNSUPPORTED",
1700
2301
  "Content metadata requires a separate mapping"
1701
2302
  );
1702
2303
  const payload = content.payload;
1703
2304
  if (!payload?.gltf || !["glTF", "gltf", "b3dm"].includes(payload.type || content.type || "")) {
1704
- throw new import_core16.TileConversionError(
2305
+ throw new import_core19.TileConversionError(
1705
2306
  "MESH_SOURCE_CONTENT_UNSUPPORTED",
1706
2307
  "Mesh extraction requires decoded GLB/glTF or B3DM content"
1707
2308
  );
@@ -1710,40 +2311,43 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
1710
2311
  if (gltf.animations?.length || gltf.skins?.length || Object.keys(gltf.extensions || {}).some(
1711
2312
  (name) => !["EXT_structural_metadata", "CESIUM_RTC"].includes(name)
1712
2313
  )) {
1713
- throw new import_core16.TileConversionError(
2314
+ throw new import_core19.TileConversionError(
1714
2315
  "MESH_SOURCE_SCENE_UNSUPPORTED",
1715
2316
  "Animated/skinned scenes and unmapped scene extensions are unsupported"
1716
2317
  );
1717
2318
  }
1718
2319
  const axis = payload.gltfUpAxis || "Y";
1719
2320
  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);
2321
+ throw new import_core19.TileConversionError("MESH_SOURCE_AXIS_UNSUPPORTED", "Unknown glTF up axis");
2322
+ const placement = new import_core18.Matrix4(item.tile.computedTransform);
1722
2323
  const rtc = payload.rtcCenter || gltf.extensions?.CESIUM_RTC?.center;
1723
2324
  if (rtc) {
1724
2325
  if (rtc.length !== 3 || rtc.some((value) => !Number.isFinite(value)))
1725
- throw new import_core16.TileConversionError(
2326
+ throw new import_core19.TileConversionError(
1726
2327
  "MESH_SOURCE_TRANSFORM_INVALID",
1727
2328
  "RTC center must contain finite xyz coordinates"
1728
2329
  );
1729
2330
  placement.translate(rtc);
1730
2331
  }
1731
2332
  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]));
2333
+ placement.multiplyRight(new import_core18.Matrix4([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]));
1733
2334
  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]));
2335
+ placement.multiplyRight(new import_core18.Matrix4([0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]));
1735
2336
  const roots = gltf.scene?.nodes || (gltf.scenes?.length === 1 ? gltf.scenes[0].nodes : void 0);
1736
2337
  if (!roots?.length)
1737
- throw new import_core16.TileConversionError(
2338
+ throw new import_core19.TileConversionError(
1738
2339
  "MESH_SOURCE_SCENE_UNSUPPORTED",
1739
2340
  "A default scene or exactly one scene is required"
1740
2341
  );
1741
2342
  let primitiveIndex = 0;
1742
2343
  for (const selected of traverseMeshNodes(roots, placement)) {
1743
- const resource = extractPrimitive(
2344
+ const resource = await extractPrimitive(
1744
2345
  selected.primitive,
1745
2346
  selected.transform,
1746
- `${item.tile.id}/${content.index}/${primitiveIndex++}`
2347
+ `${item.tile.id}/${content.index}/${primitiveIndex++}`,
2348
+ options.readExternalResource,
2349
+ content.uri,
2350
+ signal
1747
2351
  );
1748
2352
  const features = extractMeshFeatures(
1749
2353
  gltf,
@@ -1755,7 +2359,7 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
1755
2359
  yield { ...resource, features };
1756
2360
  }
1757
2361
  if (!primitiveIndex)
1758
- throw new import_core16.TileConversionError(
2362
+ throw new import_core19.TileConversionError(
1759
2363
  "MESH_SOURCE_CONTENT_EMPTY",
1760
2364
  "Declared content contains no supported mesh primitive"
1761
2365
  );
@@ -1767,16 +2371,16 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
1767
2371
  function* traverseMeshNodes(nodes, parent, ancestors = /* @__PURE__ */ new Set()) {
1768
2372
  for (const node of nodes) {
1769
2373
  if (ancestors.has(node) || node.skin || node.weights || Object.keys(node.extensions || {}).length)
1770
- throw new import_core16.TileConversionError(
2374
+ throw new import_core19.TileConversionError(
1771
2375
  "MESH_SOURCE_NODE_UNSUPPORTED",
1772
2376
  "Cyclic, skinned, morphed, or extended nodes are unsupported"
1773
2377
  );
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])
2378
+ const transform = new import_core18.Matrix4(parent).multiplyRight(
2379
+ node.matrix ? new import_core18.Matrix4(node.matrix) : new import_core18.Matrix4().translate(node.translation || [0, 0, 0]).multiplyRight(new import_core18.Matrix4().fromQuaternion(node.rotation || [0, 0, 0, 1])).scale(node.scale || [1, 1, 1])
1776
2380
  );
1777
2381
  if (node.mesh) {
1778
2382
  if (node.mesh.weights || Object.keys(node.mesh.extensions || {}).length)
1779
- throw new import_core16.TileConversionError(
2383
+ throw new import_core19.TileConversionError(
1780
2384
  "MESH_SOURCE_NODE_UNSUPPORTED",
1781
2385
  "Morph weights and mesh extensions are unsupported"
1782
2386
  );
@@ -1785,14 +2389,14 @@ function* traverseMeshNodes(nodes, parent, ancestors = /* @__PURE__ */ new Set()
1785
2389
  yield* traverseMeshNodes(node.children || [], transform, /* @__PURE__ */ new Set([...ancestors, node]));
1786
2390
  }
1787
2391
  }
1788
- function extractPrimitive(primitive, transform, id) {
2392
+ async function extractPrimitive(primitive, transform, id, readExternalResource, contentUri, signal) {
1789
2393
  if (primitive.targets?.length || Object.keys(primitive.extensions || {}).some((name) => name !== "EXT_mesh_features"))
1790
- throw new import_core16.TileConversionError(
2394
+ throw new import_core19.TileConversionError(
1791
2395
  "MESH_SOURCE_PRIMITIVE_UNSUPPORTED",
1792
2396
  "Morph targets and unmapped primitive extensions are unsupported"
1793
2397
  );
1794
2398
  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(
2399
+ throw new import_core19.TileConversionError(
1796
2400
  "MESH_SOURCE_TRANSFORM_INVALID",
1797
2401
  "Mesh placement requires a finite invertible affine transform with positive determinant"
1798
2402
  );
@@ -1800,7 +2404,7 @@ function extractPrimitive(primitive, transform, id) {
1800
2404
  for (const [name, accessor] of Object.entries(primitive.attributes)) {
1801
2405
  if (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) continue;
1802
2406
  if (!["POSITION", "NORMAL", "COLOR_0", "TEXCOORD_0"].includes(name))
1803
- throw new import_core16.TileConversionError(
2407
+ throw new import_core19.TileConversionError(
1804
2408
  "MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
1805
2409
  `Unsupported mesh attribute ${name}`
1806
2410
  );
@@ -1813,9 +2417,9 @@ function extractPrimitive(primitive, transform, id) {
1813
2417
  const mesh = validateMeshGeometry(
1814
2418
  {
1815
2419
  topology: "triangle-list",
1816
- mode: primitive.mode ?? 4,
2420
+ mode: primitive.mode === 5 ? 4 : primitive.mode ?? 4,
1817
2421
  attributes,
1818
- indices: primitive.indices ? { value: primitive.indices.value, size: 1 } : void 0
2422
+ indices: createTriangleIndices(primitive, (attributes.POSITION?.value.length ?? 0) / 3)
1819
2423
  },
1820
2424
  true
1821
2425
  );
@@ -1836,7 +2440,7 @@ function extractPrimitive(primitive, transform, id) {
1836
2440
  mesh.attributes.POSITION = { value: positions, size: 3 };
1837
2441
  if (mesh.attributes.NORMAL) {
1838
2442
  const values = mesh.attributes.NORMAL.value;
1839
- const normalMatrix = new import_core15.Matrix4(transform).invert().transpose();
2443
+ const normalMatrix = new import_core18.Matrix4(transform).invert().transpose();
1840
2444
  const normals = new Float32Array(values.length);
1841
2445
  for (let index = 0; index < values.length; index += 3) {
1842
2446
  const normal = normalMatrix.transformAsVector(Array.from(values.subarray(index, index + 3)));
@@ -1852,14 +2456,19 @@ function extractPrimitive(primitive, transform, id) {
1852
2456
  id,
1853
2457
  mesh,
1854
2458
  origin: minimum.map((value, axis) => value / 2 + maximum[axis] / 2),
1855
- material: mapMaterial(primitive.material)
2459
+ material: await mapMeshSourceMaterial(
2460
+ primitive.material,
2461
+ readExternalResource,
2462
+ contentUri,
2463
+ signal
2464
+ )
1856
2465
  };
1857
2466
  }
1858
- function mapMaterial(material) {
2467
+ async function mapMeshSourceMaterial(material, readExternalResource, contentUri, signal) {
1859
2468
  if (!material) return void 0;
1860
2469
  const pbr = material.pbrMetallicRoughness;
1861
- if (Object.keys(material).some(
1862
- (name) => ![
2470
+ if (Object.entries(material).some(
2471
+ ([name, value]) => value !== void 0 && ![
1863
2472
  "id",
1864
2473
  "name",
1865
2474
  "pbrMetallicRoughness",
@@ -1869,16 +2478,23 @@ function mapMaterial(material) {
1869
2478
  "emissiveFactor",
1870
2479
  "extras"
1871
2480
  ].includes(name)
1872
- ) || Object.keys(pbr || {}).some(
1873
- (name) => !["baseColorFactor", "baseColorTexture", "metallicFactor", "roughnessFactor"].includes(name)
2481
+ ) || Object.entries(pbr || {}).some(
2482
+ ([name, value]) => value !== void 0 && !["baseColorFactor", "baseColorTexture", "metallicFactor", "roughnessFactor"].includes(name)
1874
2483
  ) || material.emissiveFactor?.some((value) => value !== 0))
1875
- throw new import_core16.TileConversionError(
2484
+ throw new import_core19.TileConversionError(
1876
2485
  "MESH_SOURCE_MATERIAL_UNSUPPORTED",
1877
2486
  "Only metallic-roughness factors and one embedded base-color image are supported"
1878
2487
  );
1879
2488
  return {
1880
2489
  baseColorFactor: pbr?.baseColorFactor,
1881
- ..."baseColorTexture" in (pbr || {}) ? { baseColorTexture: mapMeshSourceTexture(pbr.baseColorTexture) } : {},
2490
+ ..."baseColorTexture" in (pbr || {}) ? {
2491
+ baseColorTexture: await resolveMeshSourceTexture(
2492
+ pbr.baseColorTexture,
2493
+ readExternalResource,
2494
+ contentUri,
2495
+ signal
2496
+ )
2497
+ } : {},
1882
2498
  metallicFactor: pbr?.metallicFactor,
1883
2499
  roughnessFactor: pbr?.roughnessFactor,
1884
2500
  alphaMode: material.alphaMode,
@@ -1886,48 +2502,81 @@ function mapMaterial(material) {
1886
2502
  doubleSided: material.doubleSided
1887
2503
  };
1888
2504
  }
2505
+ function createTriangleIndices(primitive, vertexCount) {
2506
+ const attribute = primitive.indices ? {
2507
+ value: primitive.indices.value,
2508
+ size: primitive.indices.components,
2509
+ normalized: primitive.indices.normalized
2510
+ } : void 0;
2511
+ if (primitive.mode !== 5) return attribute;
2512
+ const sourceIndices = validateMeshIndices(attribute, vertexCount, 5);
2513
+ const indexCount = sourceIndices?.length ?? vertexCount;
2514
+ if (!Number.isSafeInteger(indexCount) || indexCount < 3) {
2515
+ throw new import_core19.TileConversionError(
2516
+ "MESH_TRIANGLE_COUNT_INVALID",
2517
+ "Triangle strips require at least three vertices"
2518
+ );
2519
+ }
2520
+ const IndexArray = vertexCount < 65536 ? Uint16Array : Uint32Array;
2521
+ const indices = new IndexArray((indexCount - 2) * 3);
2522
+ for (let triangle = 0; triangle < indexCount - 2; triangle++) {
2523
+ const second = triangle + 1 + triangle % 2;
2524
+ const third = triangle + 2 - triangle % 2;
2525
+ indices[triangle * 3] = sourceIndices?.[triangle] ?? triangle;
2526
+ indices[triangle * 3 + 1] = sourceIndices?.[second] ?? second;
2527
+ indices[triangle * 3 + 2] = sourceIndices?.[third] ?? third;
2528
+ }
2529
+ return { value: indices, size: 1 };
2530
+ }
1889
2531
 
1890
2532
  // src/v5/i3s-mesh-conversion.ts
1891
2533
  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");
2534
+ var import_core21 = require("@loaders.gl/tile-converter/v5/core");
2535
+ var import_core22 = require("@loaders.gl/tile-converter/v5/core");
1894
2536
  function createI3SMeshConversionCodec(options) {
1895
2537
  const reference = options.spatialContext.spatialReference;
1896
2538
  const height = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
1897
2539
  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(
2540
+ throw new import_core21.TileConversionError(
1899
2541
  "I3S_MESH_FRAME_UNSUPPORTED",
1900
2542
  "I3S mesh authoring requires resolved ECEF coordinates and ellipsoidal heights"
1901
2543
  );
2544
+ let objectIdOffset = 0;
1902
2545
  return {
1903
2546
  /** Places a source mesh in ECEF and authors one layer without modifying source arrays. */
1904
2547
  async *convert(resource, _inspection, signal) {
1905
2548
  signal?.throwIfAborted();
1906
2549
  const mesh = validateMeshGeometry(resource.mesh, true);
1907
2550
  const positions = mesh.attributes.POSITION.value;
1908
- mesh.attributes.POSITION = {
1909
- value: options.spatialContext.transformPositions(positions),
1910
- size: 3
2551
+ const context = options.spatialContext;
2552
+ const transformed = "transformGeometryAsync" in context ? await context.transformGeometryAsync(
2553
+ positions,
2554
+ mesh.attributes.NORMAL?.value,
2555
+ resource.normalReferenceFrame ?? options.normalReferenceFrame
2556
+ ) : {
2557
+ positions: context.transformPositions(positions),
2558
+ normals: mesh.attributes.NORMAL ? context.transformNormals(mesh.attributes.NORMAL.value, positions) : void 0
1911
2559
  };
1912
- if (mesh.attributes.NORMAL)
1913
- mesh.attributes.NORMAL = {
1914
- value: options.spatialContext.transformNormals(mesh.attributes.NORMAL.value, positions),
1915
- size: 3
1916
- };
2560
+ signal?.throwIfAborted();
2561
+ mesh.attributes.POSITION = { value: transformed.positions, size: 3 };
2562
+ if (mesh.attributes.NORMAL) mesh.attributes.NORMAL = { value: transformed.normals, size: 3 };
1917
2563
  let layer;
1918
2564
  try {
1919
- layer = (0, import_i3s.encodeI3SMeshLayer)(mesh, {
2565
+ const writerOptions = {
1920
2566
  ...options,
1921
2567
  material: resource.material,
1922
- features: resource.features
1923
- });
2568
+ features: resource.features,
2569
+ objectIdOffset
2570
+ };
2571
+ layer = options.draco === false ? (0, import_i3s.encodeI3SMeshLayer)(mesh, writerOptions) : await (0, import_i3s.encodeI3SMeshLayerWithDraco)(mesh, writerOptions, options.dracoLibraryOptions);
1924
2572
  } catch (error) {
1925
- throw new import_core18.TileConversionError(
2573
+ throw new import_core21.TileConversionError(
1926
2574
  "I3S_MESH_PROFILE_UNSUPPORTED",
1927
2575
  error instanceof Error ? error.message : String(error)
1928
2576
  );
1929
2577
  }
1930
2578
  signal?.throwIfAborted();
2579
+ objectIdOffset += resource.features?.batches.reduce((total, batch) => total + batch.data.numRows, 0) ?? 1;
1931
2580
  yield { id: resource.id, ...layer };
1932
2581
  },
1933
2582
  /** Reports measured precision and explicitly authorized integer representation changes. */
@@ -1952,7 +2601,7 @@ function createI3SMeshConversionCodec(options) {
1952
2601
  };
1953
2602
  }
1954
2603
  function createSingleMeshI3SSink(options) {
1955
- const memory = (0, import_core19.createBoundedMemoryTileConversionSink)(options);
2604
+ const memory = (0, import_core22.createBoundedMemoryTileConversionSink)(options);
1956
2605
  let state = "empty";
1957
2606
  let completed = false;
1958
2607
  return {
@@ -1962,19 +2611,26 @@ function createSingleMeshI3SSink(options) {
1962
2611
  async write(resource, signal) {
1963
2612
  signal?.throwIfAborted();
1964
2613
  if (state !== "empty")
1965
- throw new import_core18.TileConversionError(
2614
+ throw new import_core21.TileConversionError(
1966
2615
  "SINGLE_I3S_MESH_UNAVAILABLE",
1967
2616
  "The initial I3S sink accepts exactly one mesh layer"
1968
2617
  );
1969
2618
  state = "writing";
1970
2619
  for (const [resourceId, bytes] of Object.entries(resource.files))
1971
- await memory.write({ resourceId, parts: [bytes], contentType: "application/gzip" }, signal);
2620
+ await memory.write(
2621
+ {
2622
+ resourceId,
2623
+ parts: [bytes],
2624
+ contentType: resourceId.endsWith(".png") ? "image/png" : resourceId.endsWith(".jpg") ? "image/jpeg" : "application/gzip"
2625
+ },
2626
+ signal
2627
+ );
1972
2628
  state = "written";
1973
2629
  },
1974
2630
  /** Commits a complete single-layer result. */
1975
2631
  async finalize(report) {
1976
2632
  if (state !== "written")
1977
- throw new import_core18.TileConversionError(
2633
+ throw new import_core21.TileConversionError(
1978
2634
  "SINGLE_I3S_MESH_INCOMPLETE",
1979
2635
  "Finalization requires exactly one successfully written layer"
1980
2636
  );
@@ -1990,3 +2646,903 @@ function createSingleMeshI3SSink(options) {
1990
2646
  }
1991
2647
  };
1992
2648
  }
2649
+
2650
+ // src/v5/i3s-mesh-sink.ts
2651
+ var import_geospatial = require("@math.gl/geospatial");
2652
+ var import_compression = require("@loaders.gl/compression");
2653
+ var import_core23 = require("@loaders.gl/tile-converter/v5/core");
2654
+ function createI3SMeshSink(options) {
2655
+ const { maxMeshes, maxResourceBytes } = options;
2656
+ if (!Number.isInteger(maxMeshes) || maxMeshes < 1 || maxMeshes > 64 || !Number.isSafeInteger(maxResourceBytes) || maxResourceBytes < 1)
2657
+ throw new import_core23.TileConversionError(
2658
+ "I3S_COLLECTION_OPTIONS_INVALID",
2659
+ "I3S collections require 1\u201364 meshes and a positive resource byte limit"
2660
+ );
2661
+ const memory = (0, import_core23.createBoundedMemoryTileConversionSink)(options);
2662
+ const compressor = new import_compression.GZipCompressor({ useNative: false, gzip: { mtime: 0 } });
2663
+ const decompressor = new import_compression.GZipDecompressor({ useNative: false });
2664
+ const leaves = [];
2665
+ const identifiers = /* @__PURE__ */ new Set();
2666
+ const objectIdRanges = [];
2667
+ let layer;
2668
+ let schemaKey;
2669
+ let state = "open";
2670
+ let completed = false;
2671
+ async function readBytes(bytes) {
2672
+ const output = await decompressor.decompress(bytes);
2673
+ if (output.byteLength > maxResourceBytes)
2674
+ throw new import_core23.TileConversionError(
2675
+ "I3S_COLLECTION_RESOURCE_TOO_LARGE",
2676
+ "I3S metadata exceeds maxResourceBytes"
2677
+ );
2678
+ return output;
2679
+ }
2680
+ async function readJson(resource, name) {
2681
+ return JSON.parse(new TextDecoder().decode(await readBytes(resource.files[name])));
2682
+ }
2683
+ async function writeJson(resourceId, value) {
2684
+ const bytes = new TextEncoder().encode(JSON.stringify(value));
2685
+ if (bytes.byteLength > maxResourceBytes)
2686
+ throw new import_core23.TileConversionError(
2687
+ "I3S_COLLECTION_RESOURCE_TOO_LARGE",
2688
+ "I3S index exceeds maxResourceBytes"
2689
+ );
2690
+ await memory.write({
2691
+ resourceId,
2692
+ parts: [await compressor.compress(bytes.buffer)],
2693
+ contentType: "application/gzip"
2694
+ });
2695
+ }
2696
+ return {
2697
+ /** Exposes the whole collection atomically. */
2698
+ getFiles: () => completed ? memory.getFiles() : [],
2699
+ /** Retains one leaf's binary resources and validates its shared schema and IDs. */
2700
+ async write(resource, signal) {
2701
+ signal?.throwIfAborted();
2702
+ if (state !== "open" || identifiers.has(resource.id) || leaves.length >= maxMeshes)
2703
+ throw new import_core23.TileConversionError(
2704
+ "I3S_COLLECTION_UNAVAILABLE",
2705
+ "I3S collection is busy, closed, full, or repeats a placement"
2706
+ );
2707
+ state = "writing";
2708
+ const authored = await readJson(resource, "3dSceneLayer.json.gz");
2709
+ const key = JSON.stringify([
2710
+ authored.fields,
2711
+ authored.attributeStorageInfo,
2712
+ authored.description,
2713
+ authored.store.defaultGeometrySchema
2714
+ ]);
2715
+ if (schemaKey !== void 0 && key !== schemaKey)
2716
+ throw new import_core23.TileConversionError(
2717
+ "I3S_COLLECTION_SCHEMA_MISMATCH",
2718
+ "Mesh nodes must share feature fields and the legacy geometry layout"
2719
+ );
2720
+ const objectIds = new DataView(
2721
+ await readBytes(resource.files["nodes/1/attributes/f_0/0.bin.gz"])
2722
+ );
2723
+ const count = objectIds.getUint32(0, true);
2724
+ const first = objectIds.getUint32(4, true);
2725
+ 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(
2726
+ (value, index2) => value !== first + index2
2727
+ ))
2728
+ throw new import_core23.TileConversionError(
2729
+ "I3S_COLLECTION_IDS_INVALID",
2730
+ "Generated object IDs must be contiguous and disjoint across nodes"
2731
+ );
2732
+ const index = leaves.length + 1;
2733
+ const node = (await readJson(resource, "nodepages/0.json.gz")).nodes[1];
2734
+ const legacy = await readJson(
2735
+ resource,
2736
+ "nodes/1/3dNodeIndexDocument.json.gz"
2737
+ );
2738
+ 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))
2739
+ throw new import_core23.TileConversionError(
2740
+ "I3S_COLLECTION_BOUNDS_INVALID",
2741
+ "Expected finite axis-aligned bounds from the I3S mesh writer"
2742
+ );
2743
+ if (!layer) {
2744
+ layer = structuredClone(authored);
2745
+ layer.materialDefinitions = [];
2746
+ layer.geometryDefinitions = [];
2747
+ layer.textureSetDefinitions = [];
2748
+ schemaKey = key;
2749
+ }
2750
+ const material = structuredClone(authored.materialDefinitions[0]);
2751
+ const texture = material.pbrMetallicRoughness?.baseColorTexture;
2752
+ if (texture) {
2753
+ texture.textureSetDefinitionId = layer.textureSetDefinitions.length;
2754
+ layer.textureSetDefinitions.push(authored.textureSetDefinitions[0]);
2755
+ node.mesh.material.resource = index;
2756
+ }
2757
+ node.index = index;
2758
+ node.parentIndex = 0;
2759
+ node.mesh.material.definition = layer.materialDefinitions.length;
2760
+ node.mesh.geometry.definition = layer.geometryDefinitions.length;
2761
+ node.mesh.geometry.resource = index;
2762
+ node.mesh.attribute.resource = index;
2763
+ layer.materialDefinitions.push(material);
2764
+ layer.geometryDefinitions.push(authored.geometryDefinitions[0]);
2765
+ layer.store.resourcePattern = [
2766
+ .../* @__PURE__ */ new Set([...layer.store.resourcePattern, ...authored.store.resourcePattern])
2767
+ ];
2768
+ layer.store.extent = layer.store.extent.map(
2769
+ (value, axis) => axis < 2 ? Math.min(value, authored.store.extent[axis]) : Math.max(value, authored.store.extent[axis])
2770
+ );
2771
+ for (const [name, bytes] of Object.entries(resource.files)) {
2772
+ if (name.startsWith("nodes/1/") && !name.endsWith("3dNodeIndexDocument.json.gz"))
2773
+ await memory.write(
2774
+ {
2775
+ resourceId: name.replace("nodes/1/", `nodes/${index}/`),
2776
+ parts: [bytes],
2777
+ contentType: name.endsWith(".gz") ? "application/gzip" : name.endsWith(".png") ? "image/png" : "image/jpeg"
2778
+ },
2779
+ signal
2780
+ );
2781
+ }
2782
+ leaves.push({ node, legacy: { ...legacy, id: String(index) } });
2783
+ identifiers.add(resource.id);
2784
+ objectIdRanges.push([first, first + count]);
2785
+ state = "open";
2786
+ },
2787
+ /** Rebuilds both indices and layer bounds only after every leaf has completed. */
2788
+ async finalize(report) {
2789
+ if (state !== "open" || !layer || !leaves.length)
2790
+ throw new import_core23.TileConversionError(
2791
+ "I3S_COLLECTION_INCOMPLETE",
2792
+ "Finalization requires successfully written mesh nodes"
2793
+ );
2794
+ state = "writing";
2795
+ const minimum = [Infinity, Infinity, Infinity];
2796
+ const maximum = [-Infinity, -Infinity, -Infinity];
2797
+ for (const { node } of leaves) {
2798
+ const center2 = import_geospatial.Ellipsoid.WGS84.cartographicToCartesian(node.obb.center);
2799
+ for (let axis = 0; axis < 3; axis++) {
2800
+ minimum[axis] = Math.min(minimum[axis], center2[axis] - node.obb.halfSize[axis]);
2801
+ maximum[axis] = Math.max(maximum[axis], center2[axis] + node.obb.halfSize[axis]);
2802
+ }
2803
+ }
2804
+ const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
2805
+ const geographicCenter = Array.from(import_geospatial.Ellipsoid.WGS84.cartesianToCartographic(center));
2806
+ const reconstructedCenter = import_geospatial.Ellipsoid.WGS84.cartographicToCartesian(geographicCenter);
2807
+ const obb = {
2808
+ center: geographicCenter,
2809
+ halfSize: minimum.map(
2810
+ (value, axis) => maximum[axis] / 2 - value / 2 + Math.abs(center[axis] - reconstructedCenter[axis]) + 1e-7
2811
+ ),
2812
+ quaternion: [0, 0, 0, 1]
2813
+ };
2814
+ if ([...obb.center, ...obb.halfSize].some((value) => !Number.isFinite(value)))
2815
+ throw new import_core23.TileConversionError(
2816
+ "I3S_COLLECTION_BOUNDS_INVALID",
2817
+ "Collection bounds cannot be represented"
2818
+ );
2819
+ const mbs = [...obb.center, Math.hypot(...obb.halfSize)];
2820
+ const children = leaves.map(({ node }) => ({
2821
+ id: String(node.index),
2822
+ href: `../${node.index}`,
2823
+ obb: node.obb,
2824
+ mbs: [...node.obb.center, Math.hypot(...node.obb.halfSize)]
2825
+ }));
2826
+ await writeJson("nodes/root/3dNodeIndexDocument.json.gz", {
2827
+ id: "root",
2828
+ level: 0,
2829
+ version: "1",
2830
+ obb,
2831
+ mbs,
2832
+ lodSelection: [{ metricType: "maxScreenThresholdSQ", maxError: 0 }],
2833
+ children
2834
+ });
2835
+ for (const { node, legacy } of leaves)
2836
+ await writeJson(`nodes/${node.index}/3dNodeIndexDocument.json.gz`, {
2837
+ ...legacy,
2838
+ parentNode: { id: "root", href: "../root", obb, mbs }
2839
+ });
2840
+ const nodes = [
2841
+ { index: 0, obb, lodThreshold: 0, children: leaves.map(({ node }) => node.index) },
2842
+ ...leaves.map(({ node }) => node)
2843
+ ];
2844
+ for (let page = 0; page * 64 < nodes.length; page++)
2845
+ await writeJson(`nodepages/${page}.json.gz`, {
2846
+ nodes: nodes.slice(page * 64, (page + 1) * 64)
2847
+ });
2848
+ if (!layer.textureSetDefinitions?.length) delete layer.textureSetDefinitions;
2849
+ await writeJson("3dSceneLayer.json.gz", layer);
2850
+ await writeJson("metadata.json.gz", {
2851
+ folderPattern: "BASIC",
2852
+ archiveCompressionType: "STORE",
2853
+ resourceCompressionType: "GZIP",
2854
+ I3SVersion: "1.7",
2855
+ nodeCount: nodes.length
2856
+ });
2857
+ await memory.finalize(report);
2858
+ state = "closed";
2859
+ completed = true;
2860
+ },
2861
+ /** Discards binary resources and index metadata after any failure. */
2862
+ async abort(error) {
2863
+ completed = false;
2864
+ state = "closed";
2865
+ leaves.length = 0;
2866
+ identifiers.clear();
2867
+ objectIdRanges.length = 0;
2868
+ layer = void 0;
2869
+ await memory.abort(error);
2870
+ }
2871
+ };
2872
+ }
2873
+
2874
+ // src/v5/conversion-api.ts
2875
+ var TileConversionError17 = class extends Error {
2876
+ /** Stable machine-readable failure code. */
2877
+ code;
2878
+ /** Diagnostics associated with this failure. */
2879
+ diagnostics;
2880
+ /** Creates a typed conversion failure. */
2881
+ constructor(code, message, diagnostics = []) {
2882
+ super(message);
2883
+ this.name = "TileConversionError";
2884
+ this.code = code;
2885
+ this.diagnostics = diagnostics;
2886
+ }
2887
+ };
2888
+
2889
+ // src/v5/resource-fetcher.ts
2890
+ function createTileConversionResourceFetcher(options) {
2891
+ let consumed = options.initialInputBytes ?? 0;
2892
+ if (!Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(consumed) || consumed < 0 || consumed > options.maxInputBytes)
2893
+ throw new TileConversionError17(
2894
+ "INVALID_INPUT_MEMORY_LIMIT",
2895
+ "Resource fetch requires a positive aggregate byte budget and valid initial bytes"
2896
+ );
2897
+ const fetcher = options.fetcher ?? fetch;
2898
+ return async (input, requestOptions) => {
2899
+ options.signal?.throwIfAborted();
2900
+ const url = new URL(input instanceof Request ? input.url : String(input));
2901
+ if (!["http:", "https:", "blob:", "data:"].includes(url.protocol))
2902
+ throw new TileConversionError17(
2903
+ "EXTERNAL_RESOURCE_UNSUPPORTED",
2904
+ "Conversion transport requires HTTP(S), Blob or embedded data resources"
2905
+ );
2906
+ const requestSignal = requestOptions?.signal ?? (input instanceof Request ? input.signal : void 0);
2907
+ const signal = options.signal && requestSignal ? AbortSignal.any([options.signal, requestSignal]) : options.signal ?? requestSignal;
2908
+ const response = await fetcher(input, { ...requestOptions, signal });
2909
+ if (!response.ok) {
2910
+ await response.body?.cancel();
2911
+ throw new TileConversionError17(
2912
+ "BROWSER_SOURCE_FETCH_FAILED",
2913
+ `Could not fetch conversion resource (${response.status})`
2914
+ );
2915
+ }
2916
+ const reader = response.body?.getReader();
2917
+ if (!reader)
2918
+ throw new TileConversionError17(
2919
+ "BROWSER_SOURCE_STREAM_UNAVAILABLE",
2920
+ "Conversion resources require readable response streams"
2921
+ );
2922
+ const cancelRead = () => {
2923
+ void reader.cancel(signal?.reason).catch(() => {
2924
+ });
2925
+ };
2926
+ signal?.addEventListener("abort", cancelRead, { once: true });
2927
+ const parts = [];
2928
+ let byteLength = 0;
2929
+ try {
2930
+ while (true) {
2931
+ signal?.throwIfAborted();
2932
+ const { done, value } = await reader.read();
2933
+ signal?.throwIfAborted();
2934
+ if (done) break;
2935
+ if (value.byteLength > options.maxInputBytes - consumed)
2936
+ throw new TileConversionError17(
2937
+ "INPUT_RESOURCE_TOO_LARGE",
2938
+ "Selected contents and external dependencies exceed the aggregate input byte budget"
2939
+ );
2940
+ consumed += value.byteLength;
2941
+ byteLength += value.byteLength;
2942
+ parts.push(value);
2943
+ }
2944
+ } catch (error) {
2945
+ await reader.cancel(error);
2946
+ throw error;
2947
+ } finally {
2948
+ signal?.removeEventListener("abort", cancelRead);
2949
+ reader.releaseLock();
2950
+ }
2951
+ const bytes = new Uint8Array(byteLength);
2952
+ let offset = 0;
2953
+ for (const part of parts) {
2954
+ bytes.set(part, offset);
2955
+ offset += part.byteLength;
2956
+ }
2957
+ return new Response(bytes, {
2958
+ status: response.status,
2959
+ statusText: response.statusText,
2960
+ headers: response.headers
2961
+ });
2962
+ };
2963
+ }
2964
+
2965
+ // src/v5/point-cloud-tileset-sink.ts
2966
+ var import_core24 = require("@loaders.gl/tile-converter/v5/core");
2967
+ function createPointCloudTilesetSink(options) {
2968
+ const { maxTiles, geometricError } = options;
2969
+ if (!Number.isSafeInteger(maxTiles) || maxTiles < 1 || !Number.isFinite(geometricError) || geometricError < 0) {
2970
+ throw new import_core24.TileConversionError(
2971
+ "POINT_CLOUD_TILESET_OPTIONS_INVALID",
2972
+ "Positive maxTiles and finite nonnegative geometricError are required"
2973
+ );
2974
+ }
2975
+ const memory = (0, import_core24.createBoundedMemoryTileConversionSink)(options);
2976
+ const children = [];
2977
+ const identifiers = /* @__PURE__ */ new Set();
2978
+ const minimum = [Infinity, Infinity, Infinity];
2979
+ const maximum = [-Infinity, -Infinity, -Infinity];
2980
+ let state = "open";
2981
+ let completed = false;
2982
+ return {
2983
+ /** Exposes finalized files without exposing a partial package. */
2984
+ getFiles: () => completed ? memory.getFiles() : [],
2985
+ /** Captures one validated placement and encoded PNTS resource. */
2986
+ async write(resource, signal) {
2987
+ signal?.throwIfAborted();
2988
+ if (state !== "open")
2989
+ throw new import_core24.TileConversionError(
2990
+ "POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
2991
+ "Point sink is busy or closed"
2992
+ );
2993
+ if (!resource.id || identifiers.has(resource.id) || children.length >= maxTiles) {
2994
+ throw new import_core24.TileConversionError(
2995
+ "POINT_CLOUD_TILESET_PLACEMENT_INVALID",
2996
+ "Placements require unique nonempty identifiers within maxTiles"
2997
+ );
2998
+ }
2999
+ const [lower, upper] = getPointTileBounds(resource, geometricError);
3000
+ const resourceId = `points/${children.length}.pnts`;
3001
+ state = "writing";
3002
+ await memory.write(
3003
+ { resourceId, parts: [resource.pnts], contentType: "application/octet-stream" },
3004
+ signal
3005
+ );
3006
+ if (state !== "writing")
3007
+ throw new import_core24.TileConversionError(
3008
+ "POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
3009
+ "Point sink was aborted during writing"
3010
+ );
3011
+ children.push({
3012
+ boundingVolume: { box: createPointBoundsBox(lower, upper) },
3013
+ geometricError,
3014
+ content: { uri: resourceId }
3015
+ });
3016
+ identifiers.add(resource.id);
3017
+ for (let axis = 0; axis < 3; axis++) {
3018
+ minimum[axis] = Math.min(minimum[axis], lower[axis]);
3019
+ maximum[axis] = Math.max(maximum[axis], upper[axis]);
3020
+ }
3021
+ state = "open";
3022
+ },
3023
+ /** Serializes the enclosing root after all placements have completed. */
3024
+ async finalize(report) {
3025
+ if (state !== "open" || !children.length)
3026
+ throw new import_core24.TileConversionError(
3027
+ "POINT_CLOUD_TILESET_SINK_INCOMPLETE",
3028
+ "Finalization requires successfully written point tiles"
3029
+ );
3030
+ state = "writing";
3031
+ const rootError = Math.max(
3032
+ geometricError,
3033
+ Math.hypot(...minimum.map((value, axis) => maximum[axis] - value))
3034
+ );
3035
+ if (!Number.isFinite(rootError))
3036
+ throw new import_core24.TileConversionError(
3037
+ "POINT_CLOUD_TILESET_BOUNDS_INVALID",
3038
+ "Enclosing bounds must have a finite diagonal"
3039
+ );
3040
+ const json = JSON.stringify({
3041
+ asset: { version: "1.0" },
3042
+ geometricError: rootError,
3043
+ root: {
3044
+ boundingVolume: { box: createPointBoundsBox(minimum, maximum) },
3045
+ geometricError: rootError,
3046
+ refine: "ADD",
3047
+ children
3048
+ }
3049
+ });
3050
+ await memory.write({
3051
+ resourceId: "tileset.json",
3052
+ parts: [json],
3053
+ contentType: "application/json"
3054
+ });
3055
+ if (state !== "writing")
3056
+ throw new import_core24.TileConversionError(
3057
+ "POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
3058
+ "Point sink was aborted during finalization"
3059
+ );
3060
+ await memory.finalize(report);
3061
+ if (state !== "writing")
3062
+ throw new import_core24.TileConversionError(
3063
+ "POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
3064
+ "Point sink was aborted during finalization"
3065
+ );
3066
+ state = "closed";
3067
+ completed = true;
3068
+ },
3069
+ /** Clears resource and hierarchy storage on failure or cancellation. */
3070
+ async abort(error) {
3071
+ state = "closed";
3072
+ completed = false;
3073
+ children.length = 0;
3074
+ identifiers.clear();
3075
+ await memory.abort(error);
3076
+ }
3077
+ };
3078
+ }
3079
+ function getPointTileBounds(resource, geometricError) {
3080
+ const reference = resource.spatialReference;
3081
+ const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
3082
+ 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"))) {
3083
+ throw new import_core24.TileConversionError(
3084
+ "POINT_CLOUD_TILESET_FRAME_UNSUPPORTED",
3085
+ "Point tiles require declared EPSG:4978 xyz meters, ellipsoidal heights and no additional source placement"
3086
+ );
3087
+ }
3088
+ if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
3089
+ throw new import_core24.TileConversionError(
3090
+ "POINT_CLOUD_GEOMETRIC_ERROR_INVALID",
3091
+ "geometricError must include measured position reconstruction error"
3092
+ );
3093
+ }
3094
+ const [minimum, maximum] = resource.localBoundingBox;
3095
+ const center = resource.rtcCenter;
3096
+ 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])) {
3097
+ throw new import_core24.TileConversionError(
3098
+ "POINT_CLOUD_TILESET_BOUNDS_INVALID",
3099
+ "RTC center and encoded xyz bounds must be finite and ordered"
3100
+ );
3101
+ }
3102
+ const lower = minimum.map((value, axis) => value + center[axis]);
3103
+ const upper = maximum.map((value, axis) => value + center[axis]);
3104
+ if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
3105
+ throw new import_core24.TileConversionError(
3106
+ "POINT_CLOUD_TILESET_BOUNDS_INVALID",
3107
+ "Reconstructed ECEF bounds must be finite"
3108
+ );
3109
+ return [lower, upper];
3110
+ }
3111
+ function createPointBoundsBox(minimum, maximum) {
3112
+ const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
3113
+ const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
3114
+ return [center[0], center[1], center[2], halfSize[0], 0, 0, 0, halfSize[1], 0, 0, 0, halfSize[2]];
3115
+ }
3116
+
3117
+ // src/v5/i3s-mesh-source.ts
3118
+ var import_core27 = require("@loaders.gl/tile-converter/v5/core");
3119
+
3120
+ // src/v5/i3s-mesh-source-material.ts
3121
+ var import_images3 = require("@loaders.gl/images");
3122
+ var import_core25 = require("@loaders.gl/tile-converter/v5/core");
3123
+ async function mapI3SMeshSourceMaterial(content, header, readExternalResource, contentUri, signal) {
3124
+ const material = Object.fromEntries(
3125
+ Object.entries(content.material || {}).filter(([, value]) => value !== void 0)
3126
+ );
3127
+ const metallicRoughness = material.pbrMetallicRoughness ? Object.fromEntries(
3128
+ Object.entries(material.pbrMetallicRoughness).filter(([, value]) => value !== void 0)
3129
+ ) : {};
3130
+ material.pbrMetallicRoughness = metallicRoughness;
3131
+ if (material.cullFace !== void 0 && material.cullFace !== "back") {
3132
+ throw new import_core25.TileConversionError(
3133
+ "I3S_MESH_SOURCE_MATERIAL_UNSUPPORTED",
3134
+ "Only back-face culling is supported"
3135
+ );
3136
+ }
3137
+ delete material.cullFace;
3138
+ let textureInfo = metallicRoughness.baseColorTexture ? { ...metallicRoughness.baseColorTexture } : void 0;
3139
+ const resources = header.textureUrls?.length ? header.textureUrls : header.textureUrl ? [
3140
+ {
3141
+ textureSetDefinitionId: textureInfo?.textureSetDefinitionId ?? 0,
3142
+ textureUrl: header.textureUrl,
3143
+ textureFormat: header.textureFormat || "jpg"
3144
+ }
3145
+ ] : [];
3146
+ if (!textureInfo && !header.materialDefinition && resources.length === 1) {
3147
+ textureInfo = { textureSetDefinitionId: resources[0].textureSetDefinitionId };
3148
+ }
3149
+ const encodedTextures = content.textures || {};
3150
+ if (!textureInfo && (resources.length || content.texture || Object.keys(encodedTextures).length)) {
3151
+ throw new import_core25.TileConversionError(
3152
+ "I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
3153
+ "Texture resources require a base-color mapping"
3154
+ );
3155
+ }
3156
+ if (textureInfo) {
3157
+ const definitionId = textureInfo.textureSetDefinitionId;
3158
+ const wrappingModes = { none: 33071, repeat: 10497, mirror: 33648 };
3159
+ if (typeof definitionId !== "number" || !Number.isSafeInteger(definitionId) || definitionId < 0 || textureInfo.texCoord !== void 0 && textureInfo.texCoord !== 0 || Object.keys(textureInfo).some(
3160
+ (name) => !["textureSetDefinitionId", "texCoord", "wrapS", "wrapT", "texture"].includes(name)
3161
+ ) || ["wrapS", "wrapT"].some(
3162
+ (name) => textureInfo[name] !== void 0 && (typeof textureInfo[name] !== "string" || !Object.hasOwn(wrappingModes, textureInfo[name]))
3163
+ ) || resources.length > 1 || resources.some((resource) => resource.textureSetDefinitionId !== definitionId) || Object.keys(encodedTextures).some((key) => key !== String(definitionId))) {
3164
+ throw new import_core25.TileConversionError(
3165
+ "I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
3166
+ "Only one base-color texture set on TEXCOORD_0 with qualified wrapping is supported"
3167
+ );
3168
+ }
3169
+ let encoded = encodedTextures[String(definitionId)] || content.texture || textureInfo.texture?.source?.image;
3170
+ if (!encoded) {
3171
+ if (!resources.length || !readExternalResource) {
3172
+ throw new import_core25.TileConversionError(
3173
+ "I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
3174
+ "Encoded texture bytes or an explicit bounded resource reader are required"
3175
+ );
3176
+ }
3177
+ signal?.throwIfAborted();
3178
+ encoded = await readExternalResource(resources[0].textureUrl, contentUri, signal);
3179
+ signal?.throwIfAborted();
3180
+ }
3181
+ const data = encoded instanceof ArrayBuffer ? new Uint8Array(encoded) : encoded;
3182
+ let image;
3183
+ if (data instanceof Uint8Array) {
3184
+ try {
3185
+ image = (0, import_images3.getBinaryImageMetadata)(new DataView(data.buffer, data.byteOffset, data.byteLength));
3186
+ } catch {
3187
+ }
3188
+ }
3189
+ const declaredFormat = resources[0]?.textureFormat;
3190
+ if (!image || !["image/png", "image/jpeg"].includes(image.mimeType) || declaredFormat && (declaredFormat === "png" ? image.mimeType !== "image/png" : declaredFormat === "jpg" ? image.mimeType !== "image/jpeg" : true)) {
3191
+ throw new import_core25.TileConversionError(
3192
+ "I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
3193
+ "I3S base-color bytes must have matching PNG/JPEG headers"
3194
+ );
3195
+ }
3196
+ const sampler = Object.fromEntries(
3197
+ ["wrapS", "wrapT"].filter((name) => textureInfo[name] !== void 0).map((name) => [name, wrappingModes[textureInfo[name]]])
3198
+ );
3199
+ metallicRoughness.baseColorTexture = {
3200
+ texCoord: 0,
3201
+ texture: {
3202
+ source: { mimeType: image.mimeType, bufferView: { data } },
3203
+ ...Object.keys(sampler).length ? { sampler } : {}
3204
+ }
3205
+ };
3206
+ }
3207
+ const mapped = await mapMeshSourceMaterial(
3208
+ material,
3209
+ void 0,
3210
+ contentUri,
3211
+ signal
3212
+ );
3213
+ if (mapped?.baseColorTexture && !content.sourceAttributes?.uv0) {
3214
+ throw new import_core25.TileConversionError(
3215
+ "MESH_TEXCOORD_REQUIRED",
3216
+ "Base-color textures require TEXCOORD_0"
3217
+ );
3218
+ }
3219
+ return mapped;
3220
+ }
3221
+
3222
+ // src/v5/i3s-mesh-source-features.ts
3223
+ var import_i3s2 = require("@loaders.gl/i3s");
3224
+ var import_core26 = require("@loaders.gl/tile-converter/v5/core");
3225
+ async function extractI3SMeshFeatures(content, item, metadata, options, readExternalResource, contentUri, signal) {
3226
+ const maximumAttributeBytes = options.maxAttributeBytes;
3227
+ const storage = metadata.tileset.attributeStorageInfo;
3228
+ const attributeUrls = item.tile.header.attributeUrls;
3229
+ 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(
3230
+ (field) => field.domain || field.type === "esriFieldTypeDate" || !storage.some((attribute2) => attribute2.name === field.name)
3231
+ )) {
3232
+ throw new import_core26.TileConversionError(
3233
+ "I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
3234
+ "I3S attributes require an explicit scalar schema, object ID column, byte budget and resource reader; domains/dates/raw metadata need a custom mapper"
3235
+ );
3236
+ }
3237
+ const attributeLoader = await import_i3s2.I3SAttributeLoader.preload();
3238
+ const columns = /* @__PURE__ */ Object.create(null);
3239
+ let inputBytes = 0;
3240
+ let count;
3241
+ for (const [index, attribute2] of storage.entries()) {
3242
+ signal?.throwIfAborted();
3243
+ const values = attribute2.objectIds ?? attribute2.attributeValues;
3244
+ const type = values?.valueType;
3245
+ const expectedOrdering = attribute2.objectIds ? ["objectIds"] : type === "String" ? ["attributeByteCounts", "attributeValues"] : ["attributeValues"];
3246
+ const expectedHeader = type === "String" ? [
3247
+ { property: "count", valueType: "UInt32" },
3248
+ { property: "attributeValuesByteCount", valueType: "UInt32" }
3249
+ ] : [{ property: "count", valueType: "UInt32" }];
3250
+ if (!type || values?.valuesPerElement !== 1 || attribute2.objectIds && attribute2.attributeValues || attribute2.header?.length !== expectedHeader.length || expectedHeader.some(
3251
+ (value, headerIndex) => attribute2.header[headerIndex]?.property !== value.property || attribute2.header[headerIndex]?.valueType !== value.valueType
3252
+ ) || attribute2.ordering && JSON.stringify(
3253
+ attribute2.ordering.map((value) => value === "ObjectIds" ? "objectIds" : value)
3254
+ ) !== JSON.stringify(expectedOrdering) || type === "String" && (values.encoding !== "UTF-8" || attribute2.attributeByteCounts?.valueType !== "UInt32" || attribute2.attributeByteCounts.valuesPerElement !== 1) || !attributeUrls[index]) {
3255
+ throw new import_core26.TileConversionError(
3256
+ "I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
3257
+ `Attribute ${attribute2.name} requires a standard scalar/string resource layout`
3258
+ );
3259
+ }
3260
+ const bytes = await readExternalResource(attributeUrls[index], contentUri, signal);
3261
+ signal?.throwIfAborted();
3262
+ inputBytes += bytes instanceof Uint8Array ? bytes.byteLength : Infinity;
3263
+ if (!(bytes instanceof Uint8Array) || inputBytes > maximumAttributeBytes) {
3264
+ throw new import_core26.TileConversionError(
3265
+ "INPUT_RESOURCE_TOO_LARGE",
3266
+ "I3S node attributes exceed maxAttributeBytes"
3267
+ );
3268
+ }
3269
+ let parsed;
3270
+ try {
3271
+ parsed = await attributeLoader.parse(Uint8Array.from(bytes).buffer, {
3272
+ attributeName: attribute2.name,
3273
+ attributeType: type,
3274
+ i3s: { attributeValues: "exact" }
3275
+ });
3276
+ } catch (error) {
3277
+ throw new import_core26.TileConversionError(
3278
+ "I3S_MESH_SOURCE_ATTRIBUTE_INVALID",
3279
+ `Invalid attribute ${attribute2.name}: ${error instanceof Error ? error.message : String(error)}`
3280
+ );
3281
+ }
3282
+ const column = parsed[attribute2.name];
3283
+ if (!column || count !== void 0 && column.length !== count || !column.length) {
3284
+ throw new import_core26.TileConversionError(
3285
+ "I3S_MESH_SOURCE_ATTRIBUTE_COUNT_INVALID",
3286
+ "Every attribute must have the same nonzero row count"
3287
+ );
3288
+ }
3289
+ if (Array.from(column).some(
3290
+ (value) => typeof value === "number" && !Number.isFinite(value)
3291
+ )) {
3292
+ throw new import_core26.TileConversionError(
3293
+ "I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
3294
+ "Numeric noData values require an explicit custom mapper"
3295
+ );
3296
+ }
3297
+ count = column.length;
3298
+ columns[attribute2.name] = column;
3299
+ }
3300
+ const identifiers = columns[options.objectIdProperty];
3301
+ const attributeRows = /* @__PURE__ */ new Map();
3302
+ for (let index = 0; index < count; index++) {
3303
+ const identifier = identifiers[index];
3304
+ if (!(typeof identifier === "bigint" || typeof identifier === "number" && Number.isSafeInteger(identifier)) || attributeRows.has(BigInt(identifier))) {
3305
+ throw new import_core26.TileConversionError(
3306
+ "I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
3307
+ "Object IDs must be unique exact integers"
3308
+ );
3309
+ }
3310
+ attributeRows.set(BigInt(identifier), index);
3311
+ }
3312
+ const vertexCount = content.vertexCount;
3313
+ const geometryIdentifiers = content.featureIds;
3314
+ const attribute = content.sourceAttributes?.id;
3315
+ if (geometryIdentifiers.length !== vertexCount || attribute?.size !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(geometryIdentifiers).some((value, index) => value !== attribute.value[index])) {
3316
+ throw new import_core26.TileConversionError(
3317
+ "I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
3318
+ "Geometry requires one exact unnormalized object ID per vertex"
3319
+ );
3320
+ }
3321
+ const indices = content.indices;
3322
+ const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
3323
+ if (content.drawRanges?.length) {
3324
+ const covered = new Uint8Array(triangleFeatureIndices.length);
3325
+ for (const range of content.drawRanges) {
3326
+ if (!Number.isSafeInteger(range.firstPrimitive) || range.firstPrimitive < 0 || !Number.isSafeInteger(range.primitiveCount) || range.primitiveCount <= 0 || range.firstPrimitive + range.primitiveCount > covered.length) {
3327
+ throw new import_core26.TileConversionError(
3328
+ "I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
3329
+ "Feature draw ranges must cover valid triangles"
3330
+ );
3331
+ }
3332
+ for (let triangle = range.firstPrimitive; triangle < range.firstPrimitive + range.primitiveCount; triangle++) {
3333
+ const vertex = indices?.[triangle * 3] ?? triangle * 3;
3334
+ if (covered[triangle] || geometryIdentifiers[vertex] !== range.featureId) {
3335
+ throw new import_core26.TileConversionError(
3336
+ "I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
3337
+ "Feature draw ranges must agree with decoded ownership without overlapping"
3338
+ );
3339
+ }
3340
+ covered[triangle] = 1;
3341
+ }
3342
+ }
3343
+ if (covered.some((value) => value === 0)) {
3344
+ throw new import_core26.TileConversionError(
3345
+ "I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
3346
+ "Feature draw ranges must cover every triangle"
3347
+ );
3348
+ }
3349
+ }
3350
+ const usedRows = /* @__PURE__ */ new Set();
3351
+ for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
3352
+ const corners = [0, 1, 2].map(
3353
+ (corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
3354
+ );
3355
+ const featureIds = corners.map((corner) => geometryIdentifiers[corner]);
3356
+ const row = attributeRows.get(BigInt(featureIds[0]));
3357
+ if (row === void 0 || featureIds.some((identifier) => identifier !== featureIds[0])) {
3358
+ throw new import_core26.TileConversionError(
3359
+ "I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
3360
+ "Each triangle must reference one declared attribute row"
3361
+ );
3362
+ }
3363
+ triangleFeatureIndices[triangle] = row;
3364
+ usedRows.add(row);
3365
+ }
3366
+ if (usedRows.size !== count) {
3367
+ throw new import_core26.TileConversionError(
3368
+ "I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
3369
+ "Attribute rows without geometry require a separately qualified mapping"
3370
+ );
3371
+ }
3372
+ const stableProperty = options.sourceFeatureIdProperty ?? options.objectIdProperty;
3373
+ if (!Object.hasOwn(columns, stableProperty)) {
3374
+ throw new import_core26.TileConversionError(
3375
+ "I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
3376
+ "Stable identifier column is missing"
3377
+ );
3378
+ }
3379
+ const batches = convertFeatureAttributesToArrowBatches(
3380
+ Array.from({ length: count }, (_, index) => ({
3381
+ featureId: columns[stableProperty][index],
3382
+ metadataClass: options.metadataClass,
3383
+ properties: Object.fromEntries(
3384
+ Object.entries(columns).filter(([name]) => name !== options.objectIdProperty && name !== stableProperty).map(([name, values]) => [name, values[index]])
3385
+ )
3386
+ })),
3387
+ options
3388
+ );
3389
+ const stableIds = /* @__PURE__ */ new Set();
3390
+ for (const batch of batches) {
3391
+ const column = batch.data.getChild(options.featureIdField || "feature_id");
3392
+ for (let index = 0; index < column.length; index++) {
3393
+ const identifier = column.get(index);
3394
+ if (stableIds.has(identifier)) {
3395
+ throw new import_core26.TileConversionError(
3396
+ "I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
3397
+ "Stable IDs must be unique"
3398
+ );
3399
+ }
3400
+ stableIds.add(identifier);
3401
+ }
3402
+ }
3403
+ return {
3404
+ batches,
3405
+ triangleFeatureIndices,
3406
+ featureIdField: options.featureIdField || "feature_id",
3407
+ integer64Encoding: options.integer64Encoding
3408
+ };
3409
+ }
3410
+
3411
+ // src/v5/i3s-mesh-source.ts
3412
+ function createI3SMeshTilesetConversionSource(tileset, options = {}) {
3413
+ const source = (0, import_core27.createTilesetConversionSource)(tileset, options);
3414
+ return {
3415
+ /** Validates the layer profile and explicit source decode policy before reading. */
3416
+ async inspect(signal) {
3417
+ if (options.features && options.getFeatures) {
3418
+ throw new import_core27.TileConversionError(
3419
+ "I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
3420
+ "Choose explicit resource features or a custom getFeatures mapper, not both"
3421
+ );
3422
+ }
3423
+ const metadata = await source.inspect(signal);
3424
+ if (metadata.type !== "I3S" || !["3DObject", "IntegratedMesh"].includes(metadata.tileset.layerType) || tileset.options.i3s?.geometryMode !== "source") {
3425
+ throw new import_core27.TileConversionError(
3426
+ "I3S_MESH_SOURCE_PROFILE_UNSUPPORTED",
3427
+ "I3S mesh extraction requires a 3DObject/IntegratedMesh runtime with geometryMode=source"
3428
+ );
3429
+ }
3430
+ if (!metadata.spatialReference?.sourceCrs) {
3431
+ throw new import_core27.TileConversionError(
3432
+ "I3S_MESH_SOURCE_REFERENCE_REQUIRED",
3433
+ "I3S conversion requires an explicit source CRS"
3434
+ );
3435
+ }
3436
+ return metadata;
3437
+ },
3438
+ /** Converts each supported content entry before shared traversal releases its buffers. */
3439
+ async *read(metadata, signal) {
3440
+ for await (const item of source.read(metadata, signal)) {
3441
+ for (const entry of item.contents) {
3442
+ signal?.throwIfAborted();
3443
+ const content = entry.payload;
3444
+ const attributes = content?.sourceAttributes;
3445
+ if (content?.geometryMode !== "source" || content.topology !== "triangle-list" || !attributes || !(attributes.position?.value instanceof Float64Array) || content.spatialReference?.status !== "native" || [
3446
+ "sourceCrs",
3447
+ "verticalCrs",
3448
+ "axisOrder",
3449
+ "units",
3450
+ "verticalUnitScale",
3451
+ "heightReference",
3452
+ "elevationMode",
3453
+ "elevationOffset",
3454
+ "elevationUnitScale",
3455
+ "coordinateEpoch"
3456
+ ].some(
3457
+ (field) => JSON.stringify(content.spatialReference?.[field]) !== JSON.stringify(metadata.spatialReference?.[field])
3458
+ )) {
3459
+ throw new import_core27.TileConversionError(
3460
+ "I3S_MESH_SOURCE_GEOMETRY_UNSUPPORTED",
3461
+ "I3S extraction requires original source-frame Float64 triangle geometry"
3462
+ );
3463
+ }
3464
+ if (!["earth-centered", "vertex-reference-frame"].includes(
3465
+ content.normalReferenceFrame || ""
3466
+ ) || content.normalReferenceFrame !== (metadata.tileset.store?.normalReferenceFrame || "earth-centered")) {
3467
+ throw new import_core27.TileConversionError(
3468
+ "I3S_MESH_SOURCE_NORMAL_FRAME_UNSUPPORTED",
3469
+ "Unknown I3S vector basis"
3470
+ );
3471
+ }
3472
+ if (content.meshSegmentation || Object.entries(attributes).some(
3473
+ ([name, value]) => value && !["position", "normal", "uv0", "id", "faceRange"].includes(name)
3474
+ )) {
3475
+ throw new import_core27.TileConversionError(
3476
+ "I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
3477
+ "Only positions, normals, TEXCOORD_0 and explicitly mapped feature associations are supported"
3478
+ );
3479
+ }
3480
+ const hasFeatures = content.featureIds.length > 0 || metadata.tileset.fields?.length || metadata.tileset.attributeStorageInfo?.length;
3481
+ if (Array.from(content.featureIds).some((value) => !Number.isSafeInteger(value))) {
3482
+ throw new import_core27.TileConversionError(
3483
+ "I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
3484
+ "Decoded numeric feature IDs must be exact safe integers"
3485
+ );
3486
+ }
3487
+ if (hasFeatures && !options.getFeatures && !options.features) {
3488
+ throw new import_core27.TileConversionError(
3489
+ "I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
3490
+ "I3S feature data requires explicit resource features or a custom Arrow mapper"
3491
+ );
3492
+ }
3493
+ const mesh = validateMeshGeometry(
3494
+ {
3495
+ mode: 4,
3496
+ topology: "triangle-list",
3497
+ attributes: {
3498
+ POSITION: attributes.position,
3499
+ ...attributes.normal ? { NORMAL: attributes.normal } : {},
3500
+ ...attributes.uv0 ? { TEXCOORD_0: attributes.uv0 } : {}
3501
+ },
3502
+ ...content.indices ? { indices: { value: content.indices, size: 1 } } : {}
3503
+ },
3504
+ true
3505
+ );
3506
+ if (mesh.attributes.POSITION.value.length / 3 !== content.vertexCount) {
3507
+ throw new import_core27.TileConversionError(
3508
+ "I3S_MESH_SOURCE_COUNT_INVALID",
3509
+ "I3S vertexCount must match the decoded geometry"
3510
+ );
3511
+ }
3512
+ const features = options.getFeatures ? await options.getFeatures(content, item, metadata, signal) : options.features ? await extractI3SMeshFeatures(
3513
+ content,
3514
+ item,
3515
+ metadata,
3516
+ options.features,
3517
+ options.readExternalResource,
3518
+ entry.uri,
3519
+ signal
3520
+ ) : void 0;
3521
+ signal?.throwIfAborted();
3522
+ if (hasFeatures && !features) {
3523
+ throw new import_core27.TileConversionError(
3524
+ "I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
3525
+ "The feature mapper must return all declared feature data"
3526
+ );
3527
+ }
3528
+ const mappedMaterial = await mapI3SMeshSourceMaterial(
3529
+ content,
3530
+ item.tile.header,
3531
+ options.readExternalResource,
3532
+ entry.uri,
3533
+ signal
3534
+ );
3535
+ signal?.throwIfAborted();
3536
+ yield {
3537
+ id: `${item.tile.id}/${entry.index}`,
3538
+ origin: [0, 0, 0],
3539
+ mesh,
3540
+ normalReferenceFrame: content.normalReferenceFrame === "vertex-reference-frame" ? "vertex-reference-frame" : "earth-centered",
3541
+ features,
3542
+ material: mappedMaterial
3543
+ };
3544
+ }
3545
+ }
3546
+ }
3547
+ };
3548
+ }