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