@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.
- package/README.md +33 -0
- package/dist/converter.min.cjs +103 -103
- package/dist/i3s-server/bin/i3s-server.min.cjs +1 -1
- package/dist/index.cjs +651 -547
- package/dist/index.js +641 -537
- package/dist/v5/adapters.cjs +1756 -200
- package/dist/v5/adapters.d.ts +13 -2
- package/dist/v5/adapters.js +1755 -190
- package/dist/v5/browser.cjs +12 -0
- package/dist/v5/browser.d.ts +12 -2
- package/dist/v5/browser.js +19 -1
- package/dist/v5/core.cjs +54 -0
- package/dist/v5/core.js +54 -0
- package/dist/v5/i3s-mesh-conversion.d.ts +10 -4
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/index.cjs +10 -0
- package/dist/v5/index.d.ts +10 -2
- package/dist/v5/index.js +17 -1
- package/dist/v5/mesh-conversion.d.ts +11 -2
- package/dist/v5/mesh-features.d.ts +9 -0
- package/dist/v5/mesh-source-texture.d.ts +2 -0
- package/dist/v5/mesh-source.d.ts +8 -1
- package/dist/v5/mesh.d.ts +6 -1
- package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud.d.ts +26 -1
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/spatial-conversion.d.ts +3 -1
- package/package.json +23 -23
- package/src/v5/adapters.ts +21 -2
- package/src/v5/browser.ts +36 -2
- package/src/v5/i3s-mesh-conversion.ts +58 -18
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +227 -0
- package/src/v5/index.ts +34 -2
- package/src/v5/mesh-conversion.ts +41 -11
- package/src/v5/mesh-draco.ts +47 -3
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-source-features.ts +37 -6
- package/src/v5/mesh-source-texture.ts +47 -0
- package/src/v5/mesh-source.ts +83 -21
- package/src/v5/mesh.ts +16 -7
- package/src/v5/point-cloud-conversion.ts +9 -1
- package/src/v5/point-cloud-source-encoder.ts +31 -20
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud.ts +159 -32
- package/src/v5/resource-fetcher.ts +110 -0
- package/src/v5/spatial-conversion.ts +84 -0
package/dist/v5/adapters.js
CHANGED
|
@@ -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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
|
393
|
-
if (
|
|
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
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1187
|
+
const indices = validateMeshIndices(mesh.indices, positions.length / 3);
|
|
682
1188
|
if (!indices && positions.length % 9 !== 0) {
|
|
683
|
-
throw new
|
|
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
|
|
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
|
|
1210
|
+
throw new TileConversionError6("MESH_ATTRIBUTE_NONFINITE", `${name} must contain finite values`);
|
|
705
1211
|
}
|
|
706
1212
|
return attribute.value;
|
|
707
1213
|
}
|
|
708
|
-
function
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
1559
|
+
throw new TileConversionError8(
|
|
1033
1560
|
"MESH_ORIGIN_INVALID",
|
|
1034
1561
|
"Target origin must contain three finite coordinates"
|
|
1035
1562
|
);
|
|
1036
1563
|
}
|
|
1037
|
-
const
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2125
|
+
throw new TileConversionError13(
|
|
1548
2126
|
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
1549
2127
|
`Property ${name} has not been decoded`
|
|
1550
2128
|
);
|
|
1551
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1605
|
-
(corner) =>
|
|
2198
|
+
const corners = [0, 1, 2].map(
|
|
2199
|
+
(corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
|
|
1606
2200
|
);
|
|
1607
|
-
|
|
1608
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1684
|
-
const placement = new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1738
|
-
node.matrix ? new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
2416
|
+
material: await mapMeshSourceMaterial(
|
|
2417
|
+
primitive.material,
|
|
2418
|
+
readExternalResource,
|
|
2419
|
+
contentUri,
|
|
2420
|
+
signal
|
|
2421
|
+
)
|
|
1819
2422
|
};
|
|
1820
2423
|
}
|
|
1821
|
-
function
|
|
2424
|
+
async function mapMeshSourceMaterial(material, readExternalResource, contentUri, signal) {
|
|
1822
2425
|
if (!material) return void 0;
|
|
1823
2426
|
const pbr = material.pbrMetallicRoughness;
|
|
1824
|
-
if (Object.
|
|
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.
|
|
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
|
|
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 || {}) ? {
|
|
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
|
|
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
|
|
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
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
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
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
|
|
3534
|
+
encodePointCloudTileWithMetadata,
|
|
3535
|
+
encodeTileConversionArchiveInBatches,
|
|
3536
|
+
transformPointCloudSourceTile
|
|
1972
3537
|
};
|