@loaders.gl/tile-converter 5.0.0-alpha.10 → 5.0.0-alpha.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -2
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +162 -163
- package/dist/i3s-server/bin/i3s-server.min.cjs +73 -73
- package/dist/index.cjs +1933 -2232
- package/dist/index.js +1925 -2235
- package/dist/v5/adapters.cjs +3186 -208
- package/dist/v5/adapters.d.ts +29 -2
- package/dist/v5/adapters.js +3202 -196
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +30 -0
- package/dist/v5/browser.d.ts +26 -2
- package/dist/v5/browser.js +45 -1
- package/dist/v5/conversion-api.d.ts +9 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1214 -0
- package/dist/v5/core.cjs +515 -50
- package/dist/v5/core.d.ts +2 -0
- package/dist/v5/core.js +515 -50
- package/dist/v5/i3s-mesh-conversion.d.ts +14 -4
- package/dist/v5/i3s-mesh-sink.d.ts +25 -0
- package/dist/v5/i3s-mesh-source-features.d.ts +13 -0
- package/dist/v5/i3s-mesh-source-material.d.ts +5 -0
- package/dist/v5/i3s-mesh-source.d.ts +30 -0
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +20 -0
- package/dist/v5/index.d.ts +18 -2
- package/dist/v5/index.js +31 -1
- package/dist/v5/mesh-conversion.d.ts +29 -2
- package/dist/v5/mesh-features.d.ts +9 -0
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +2 -1
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +2 -0
- package/dist/v5/mesh-source.d.ts +13 -3
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +6 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +32 -0
- package/dist/v5/node-feature-schema.d.ts +3 -0
- package/dist/v5/node-i3s-source.d.ts +41 -0
- package/dist/v5/node-source.d.ts +47 -0
- package/dist/v5/node.cjs +900 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +877 -0
- package/dist/v5/point-cloud-file.d.ts +83 -0
- package/dist/v5/point-cloud-potree.d.ts +47 -0
- package/dist/v5/point-cloud-source-encoder.d.ts +4 -7
- package/dist/v5/point-cloud-spatial.d.ts +22 -0
- package/dist/v5/point-cloud-tileset-sink.d.ts +28 -0
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +26 -1
- package/dist/v5/resource-fetcher.d.ts +18 -0
- package/dist/v5/resource-manifest.d.ts +10 -1
- package/dist/v5/selected-mesh-source.d.ts +53 -0
- package/dist/v5/spatial-conversion.d.ts +3 -1
- package/package.json +33 -25
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +2 -4
- package/src/v4/converter-cli.ts +8 -2
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +3 -51
- package/src/v4/i3s-converter/helpers/node-index-document.ts +5 -2
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +1 -1
- package/src/v4/i3s-converter/json-templates/store.ts +2 -1
- package/src/v5/adapters.ts +69 -2
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +81 -2
- package/src/v5/conversion-api.ts +69 -9
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +6 -0
- package/src/v5/i3s-mesh-conversion.ts +97 -19
- package/src/v5/i3s-mesh-sink.ts +351 -0
- package/src/v5/i3s-mesh-source-features.ts +279 -0
- package/src/v5/i3s-mesh-source-material.ts +157 -0
- package/src/v5/i3s-mesh-source.ts +228 -0
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +60 -2
- package/src/v5/mesh-conversion.ts +61 -11
- package/src/v5/mesh-draco.ts +47 -3
- package/src/v5/mesh-features.ts +319 -0
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +73 -10
- package/src/v5/mesh-source-schema.ts +134 -0
- package/src/v5/mesh-source-texture.ts +47 -0
- package/src/v5/mesh-source.ts +99 -25
- package/src/v5/mesh-tileset-sink.ts +1 -1
- package/src/v5/mesh.ts +16 -7
- package/src/v5/node-cli.ts +197 -0
- package/src/v5/node-conversion.ts +345 -0
- package/src/v5/node-feature-schema.ts +155 -0
- package/src/v5/node-i3s-source.ts +181 -0
- package/src/v5/node-source.ts +328 -0
- package/src/v5/node.ts +16 -0
- package/src/v5/point-cloud-conversion.ts +9 -1
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +130 -0
- package/src/v5/point-cloud-source-encoder.ts +31 -20
- package/src/v5/point-cloud-spatial.ts +280 -0
- package/src/v5/point-cloud-tileset-sink.ts +234 -0
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +159 -32
- package/src/v5/resource-fetcher.ts +113 -0
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- package/src/v5/spatial-conversion.ts +161 -6
package/dist/v5/adapters.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++) {
|
|
@@ -1084,6 +1630,8 @@ function createMeshConversionCodec(options) {
|
|
|
1084
1630
|
}
|
|
1085
1631
|
yield {
|
|
1086
1632
|
id: resource.id,
|
|
1633
|
+
contentResourceId: resource.contentResourceId,
|
|
1634
|
+
contentPrimitive: resource.contentPrimitive,
|
|
1087
1635
|
glb: encoded.glb,
|
|
1088
1636
|
origin,
|
|
1089
1637
|
boundingBox: [minimum, maximum],
|
|
@@ -1107,12 +1655,12 @@ function createMeshConversionCodec(options) {
|
|
|
1107
1655
|
}
|
|
1108
1656
|
|
|
1109
1657
|
// src/v5/mesh-tileset-sink.ts
|
|
1110
|
-
import { TileConversionError as
|
|
1658
|
+
import { TileConversionError as TileConversionError9 } from "@loaders.gl/tile-converter/v5/core";
|
|
1111
1659
|
import { createBoundedMemoryTileConversionSink } from "@loaders.gl/tile-converter/v5/core";
|
|
1112
1660
|
function createSingleMeshTilesetSink(options) {
|
|
1113
1661
|
const { geometricError } = options;
|
|
1114
1662
|
if (!Number.isFinite(geometricError) || geometricError < 0) {
|
|
1115
|
-
throw new
|
|
1663
|
+
throw new TileConversionError9(
|
|
1116
1664
|
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1117
1665
|
"geometricError must be finite and nonnegative, in meters"
|
|
1118
1666
|
);
|
|
@@ -1127,7 +1675,7 @@ function createSingleMeshTilesetSink(options) {
|
|
|
1127
1675
|
async write(resource, signal) {
|
|
1128
1676
|
signal?.throwIfAborted();
|
|
1129
1677
|
if (state !== "empty") {
|
|
1130
|
-
throw new
|
|
1678
|
+
throw new TileConversionError9(
|
|
1131
1679
|
"SINGLE_MESH_SINK_UNAVAILABLE",
|
|
1132
1680
|
"The sink accepts exactly one mesh before finalization or abort"
|
|
1133
1681
|
);
|
|
@@ -1146,7 +1694,7 @@ function createSingleMeshTilesetSink(options) {
|
|
|
1146
1694
|
},
|
|
1147
1695
|
async finalize(report) {
|
|
1148
1696
|
if (state !== "written") {
|
|
1149
|
-
throw new
|
|
1697
|
+
throw new TileConversionError9(
|
|
1150
1698
|
"SINGLE_MESH_SINK_INCOMPLETE",
|
|
1151
1699
|
"Finalization requires exactly one successfully written mesh"
|
|
1152
1700
|
);
|
|
@@ -1167,13 +1715,13 @@ function createMeshTileset(resource, geometricError) {
|
|
|
1167
1715
|
const outputCrs = reference.targetCrs || reference.sourceCrs;
|
|
1168
1716
|
const heightReference = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
|
|
1169
1717
|
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
|
|
1718
|
+
throw new TileConversionError9(
|
|
1171
1719
|
"MESH_TILESET_FRAME_UNSUPPORTED",
|
|
1172
1720
|
"Single-mesh tilesets require resolved EPSG:4978 xyz coordinates in meters with ellipsoidal heights"
|
|
1173
1721
|
);
|
|
1174
1722
|
}
|
|
1175
1723
|
if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
|
|
1176
|
-
throw new
|
|
1724
|
+
throw new TileConversionError9(
|
|
1177
1725
|
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
1178
1726
|
"geometricError must include the measured position reconstruction error"
|
|
1179
1727
|
);
|
|
@@ -1181,7 +1729,7 @@ function createMeshTileset(resource, geometricError) {
|
|
|
1181
1729
|
const origin = [...resource.origin];
|
|
1182
1730
|
const [minimum, maximum] = resource.localBoundingBox.map((bound) => [...bound]);
|
|
1183
1731
|
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
|
|
1732
|
+
throw new TileConversionError9(
|
|
1185
1733
|
"MESH_TILESET_BOUNDS_INVALID",
|
|
1186
1734
|
"Origin and local bounds must be finite xyz coordinates with ordered bounds"
|
|
1187
1735
|
);
|
|
@@ -1220,7 +1768,7 @@ function createMeshTileset(resource, geometricError) {
|
|
|
1220
1768
|
function createMeshTilesetSink(options) {
|
|
1221
1769
|
const { geometricError, maxMeshes } = options;
|
|
1222
1770
|
if (!Number.isFinite(geometricError) || geometricError < 0 || !Number.isSafeInteger(maxMeshes) || maxMeshes < 1) {
|
|
1223
|
-
throw new
|
|
1771
|
+
throw new TileConversionError9(
|
|
1224
1772
|
"MESH_TILESET_OPTIONS_INVALID",
|
|
1225
1773
|
"A leaf collection requires finite nonnegative geometricError and positive maxMeshes"
|
|
1226
1774
|
);
|
|
@@ -1239,12 +1787,12 @@ function createMeshTilesetSink(options) {
|
|
|
1239
1787
|
async write(resource, signal) {
|
|
1240
1788
|
signal?.throwIfAborted();
|
|
1241
1789
|
if (state !== "open")
|
|
1242
|
-
throw new
|
|
1790
|
+
throw new TileConversionError9(
|
|
1243
1791
|
"MESH_TILESET_SINK_UNAVAILABLE",
|
|
1244
1792
|
"The mesh sink is busy or closed"
|
|
1245
1793
|
);
|
|
1246
1794
|
if (children.length >= maxMeshes || identifiers.has(resource.id))
|
|
1247
|
-
throw new
|
|
1795
|
+
throw new TileConversionError9(
|
|
1248
1796
|
"MESH_TILESET_PLACEMENT_INVALID",
|
|
1249
1797
|
"Mesh count exceeds maxMeshes or a placement identifier is repeated"
|
|
1250
1798
|
);
|
|
@@ -1256,7 +1804,7 @@ function createMeshTilesetSink(options) {
|
|
|
1256
1804
|
(value, axis) => value + resource.origin[axis]
|
|
1257
1805
|
);
|
|
1258
1806
|
if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
|
|
1259
|
-
throw new
|
|
1807
|
+
throw new TileConversionError9(
|
|
1260
1808
|
"MESH_TILESET_BOUNDS_INVALID",
|
|
1261
1809
|
"Absolute bounds must be finite"
|
|
1262
1810
|
);
|
|
@@ -1278,7 +1826,7 @@ function createMeshTilesetSink(options) {
|
|
|
1278
1826
|
/** Writes the enclosing root only when every leaf has completed. */
|
|
1279
1827
|
async finalize(report) {
|
|
1280
1828
|
if (state !== "open" || !children.length)
|
|
1281
|
-
throw new
|
|
1829
|
+
throw new TileConversionError9(
|
|
1282
1830
|
"MESH_TILESET_SINK_INCOMPLETE",
|
|
1283
1831
|
"Finalization requires written mesh leaves"
|
|
1284
1832
|
);
|
|
@@ -1287,7 +1835,7 @@ function createMeshTilesetSink(options) {
|
|
|
1287
1835
|
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
1288
1836
|
const rootGeometricError = Math.max(geometricError, 2 * Math.hypot(...halfSize));
|
|
1289
1837
|
if (!Number.isFinite(rootGeometricError))
|
|
1290
|
-
throw new
|
|
1838
|
+
throw new TileConversionError9(
|
|
1291
1839
|
"MESH_TILESET_BOUNDS_INVALID",
|
|
1292
1840
|
"Root bounds must have a finite diagonal"
|
|
1293
1841
|
);
|
|
@@ -1342,7 +1890,7 @@ import {
|
|
|
1342
1890
|
encodeTiles3DArchiveInBatches
|
|
1343
1891
|
} from "@loaders.gl/3d-tiles/3d-tiles-archive-writer";
|
|
1344
1892
|
import { SLPKWriter, encodeSLPKArchiveInBatches } from "@loaders.gl/i3s/i3s-slpk-writer";
|
|
1345
|
-
import { TileConversionError as
|
|
1893
|
+
import { TileConversionError as TileConversionError10 } from "@loaders.gl/tile-converter/v5/core";
|
|
1346
1894
|
async function createTileConversionArchive(files, options) {
|
|
1347
1895
|
const { resources, format, maxArchiveBytes, signal } = prepareArchiveInput(files, options);
|
|
1348
1896
|
const writer = format === "3tz" ? Tiles3DArchiveWriter : SLPKWriter;
|
|
@@ -1357,17 +1905,17 @@ async function* encodeTileConversionArchiveInBatches(files, options) {
|
|
|
1357
1905
|
function prepareArchiveInput(files, options) {
|
|
1358
1906
|
const { format, maxArchiveBytes, signal } = options;
|
|
1359
1907
|
if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
|
|
1360
|
-
throw new
|
|
1908
|
+
throw new TileConversionError10(
|
|
1361
1909
|
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
1362
1910
|
"Archive byte limit must be a non-negative safe integer"
|
|
1363
1911
|
);
|
|
1364
1912
|
}
|
|
1365
1913
|
if (format !== "3tz" && format !== "slpk") {
|
|
1366
|
-
throw new
|
|
1914
|
+
throw new TileConversionError10("INVALID_ARCHIVE_FORMAT", "Expected 3tz or slpk archive format");
|
|
1367
1915
|
}
|
|
1368
1916
|
const resources = Object.fromEntries(files.map((file) => [file.resourceId, file.blob]));
|
|
1369
1917
|
if (Object.keys(resources).length !== files.length) {
|
|
1370
|
-
throw new
|
|
1918
|
+
throw new TileConversionError10(
|
|
1371
1919
|
"DUPLICATE_ARCHIVE_RESOURCE",
|
|
1372
1920
|
"Archive resource identifiers must be unique"
|
|
1373
1921
|
);
|
|
@@ -1377,17 +1925,17 @@ function prepareArchiveInput(files, options) {
|
|
|
1377
1925
|
|
|
1378
1926
|
// src/v5/mesh-tileset-archive.ts
|
|
1379
1927
|
import { Tiles3DArchiveWriter as Tiles3DArchiveWriter2 } from "@loaders.gl/3d-tiles/3d-tiles-archive-writer";
|
|
1380
|
-
import { TileConversionError as
|
|
1928
|
+
import { TileConversionError as TileConversionError11 } from "@loaders.gl/tile-converter/v5/core";
|
|
1381
1929
|
async function createSingleMeshTilesetArchive(files, options) {
|
|
1382
1930
|
const { maxArchiveBytes, signal } = options;
|
|
1383
1931
|
if (!Number.isSafeInteger(maxArchiveBytes) || maxArchiveBytes < 0) {
|
|
1384
|
-
throw new
|
|
1932
|
+
throw new TileConversionError11(
|
|
1385
1933
|
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
1386
1934
|
"Archive byte limit must be a non-negative safe integer"
|
|
1387
1935
|
);
|
|
1388
1936
|
}
|
|
1389
1937
|
if (files.length !== 2 || !files.some((file) => file.resourceId === "mesh.glb") || !files.some((file) => file.resourceId === "tileset.json")) {
|
|
1390
|
-
throw new
|
|
1938
|
+
throw new TileConversionError11(
|
|
1391
1939
|
"INVALID_SINGLE_MESH_ARCHIVE",
|
|
1392
1940
|
"Expected mesh.glb and tileset.json from a finalized single-mesh sink"
|
|
1393
1941
|
);
|
|
@@ -1398,12 +1946,13 @@ async function createSingleMeshTilesetArchive(files, options) {
|
|
|
1398
1946
|
}
|
|
1399
1947
|
|
|
1400
1948
|
// src/v5/mesh-source.ts
|
|
1401
|
-
import { Matrix4 } from "@math.gl/core";
|
|
1402
|
-
import { TileConversionError as
|
|
1949
|
+
import { Matrix4 as Matrix42 } from "@math.gl/core";
|
|
1950
|
+
import { TileConversionError as TileConversionError15 } from "@loaders.gl/tile-converter/v5/core";
|
|
1403
1951
|
import { createTilesetConversionSource } from "@loaders.gl/tile-converter/v5/core";
|
|
1404
1952
|
|
|
1405
1953
|
// src/v5/mesh-source-texture.ts
|
|
1406
|
-
import {
|
|
1954
|
+
import { getBinaryImageMetadata as getBinaryImageMetadata2 } from "@loaders.gl/images";
|
|
1955
|
+
import { TileConversionError as TileConversionError12 } from "@loaders.gl/tile-converter/v5/core";
|
|
1407
1956
|
function mapMeshSourceTexture(textureInfo) {
|
|
1408
1957
|
const texture = textureInfo?.texture;
|
|
1409
1958
|
const image = texture?.source;
|
|
@@ -1430,7 +1979,7 @@ function mapMeshSourceTexture(textureInfo) {
|
|
|
1430
1979
|
"extras"
|
|
1431
1980
|
].includes(name)
|
|
1432
1981
|
))) {
|
|
1433
|
-
throw new
|
|
1982
|
+
throw new TileConversionError12(
|
|
1434
1983
|
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
1435
1984
|
"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
1985
|
);
|
|
@@ -1459,7 +2008,7 @@ function mapMeshSourceImage(image) {
|
|
|
1459
2008
|
}
|
|
1460
2009
|
const match = typeof image.uri === "string" ? /^data:(image\/(?:png|jpeg));base64,([A-Za-z0-9+/]+={0,2})$/.exec(image.uri) : null;
|
|
1461
2010
|
if (!match || image.mimeType !== void 0 && image.mimeType !== match[1]) {
|
|
1462
|
-
throw new
|
|
2011
|
+
throw new TileConversionError12(
|
|
1463
2012
|
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
1464
2013
|
"Inline images require a PNG/JPEG base64 data URI and matching optional mimeType"
|
|
1465
2014
|
);
|
|
@@ -1468,91 +2017,233 @@ function mapMeshSourceImage(image) {
|
|
|
1468
2017
|
try {
|
|
1469
2018
|
encodedBytes = atob(match[2]);
|
|
1470
2019
|
} catch {
|
|
1471
|
-
throw new
|
|
2020
|
+
throw new TileConversionError12("MESH_SOURCE_TEXTURE_UNSUPPORTED", "Invalid inline image base64");
|
|
1472
2021
|
}
|
|
1473
2022
|
return {
|
|
1474
2023
|
data: Uint8Array.from(encodedBytes, (character) => character.charCodeAt(0)),
|
|
1475
2024
|
mimeType: match[1]
|
|
1476
2025
|
};
|
|
1477
2026
|
}
|
|
2027
|
+
async function resolveMeshSourceTexture(textureInfo, readExternalResource, contentUri, signal) {
|
|
2028
|
+
const image = textureInfo?.texture?.source;
|
|
2029
|
+
if (!image || image.uri === void 0 || image.uri.startsWith("data:"))
|
|
2030
|
+
return mapMeshSourceTexture(textureInfo);
|
|
2031
|
+
if (!readExternalResource || image.bufferView !== void 0)
|
|
2032
|
+
throw new TileConversionError12(
|
|
2033
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
2034
|
+
"External images require an explicit bounded resource reader"
|
|
2035
|
+
);
|
|
2036
|
+
signal?.throwIfAborted();
|
|
2037
|
+
const data = await readExternalResource(image.uri, contentUri, signal);
|
|
2038
|
+
signal?.throwIfAborted();
|
|
2039
|
+
let metadata;
|
|
2040
|
+
try {
|
|
2041
|
+
metadata = getBinaryImageMetadata2(new DataView(data.buffer, data.byteOffset, data.byteLength));
|
|
2042
|
+
} catch {
|
|
2043
|
+
}
|
|
2044
|
+
if (!metadata || !["image/png", "image/jpeg"].includes(metadata.mimeType) || image.mimeType !== void 0 && image.mimeType !== metadata.mimeType)
|
|
2045
|
+
throw new TileConversionError12(
|
|
2046
|
+
"MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
2047
|
+
"External images require matching PNG/JPEG headers"
|
|
2048
|
+
);
|
|
2049
|
+
const resolved = {
|
|
2050
|
+
...textureInfo,
|
|
2051
|
+
texture: {
|
|
2052
|
+
...textureInfo.texture,
|
|
2053
|
+
source: { ...image, uri: void 0, mimeType: metadata.mimeType, bufferView: { data } }
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
return mapMeshSourceTexture(resolved);
|
|
2057
|
+
}
|
|
2058
|
+
|
|
2059
|
+
// src/v5/mesh-source-schema.ts
|
|
2060
|
+
import { TileConversionError as TileConversionError13 } from "@loaders.gl/tile-converter/v5/core";
|
|
2061
|
+
var SCALAR_TYPES = {
|
|
2062
|
+
INT8: "int8",
|
|
2063
|
+
UINT8: "uint8",
|
|
2064
|
+
INT16: "int16",
|
|
2065
|
+
UINT16: "uint16",
|
|
2066
|
+
INT32: "int32",
|
|
2067
|
+
UINT32: "uint32",
|
|
2068
|
+
INT64: "int64",
|
|
2069
|
+
UINT64: "uint64",
|
|
2070
|
+
FLOAT32: "float32",
|
|
2071
|
+
FLOAT64: "float64"
|
|
2072
|
+
};
|
|
2073
|
+
function createMeshSourceFeatureOptions(gltf, options) {
|
|
2074
|
+
const metadata = gltf.extensions?.EXT_structural_metadata;
|
|
2075
|
+
const table = metadata?.propertyTables?.[0];
|
|
2076
|
+
const properties = metadata?.schema?.classes?.[options.metadataClass]?.properties;
|
|
2077
|
+
if (!options.metadataClass || metadata?.propertyTables?.length !== 1 || table?.class !== options.metadataClass || !properties || !Number.isSafeInteger(table.count) || table.count < 1 || table.count > 4294967295 || metadata.propertyTextures?.length || metadata.propertyAttributes?.length) {
|
|
2078
|
+
throw new TileConversionError13(
|
|
2079
|
+
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
2080
|
+
"Schema mapping requires one inline class, one matching property table and a positive uint32 row count"
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
if (Object.keys(table.properties || {}).some((name) => !Object.hasOwn(properties, name))) {
|
|
2084
|
+
throw new TileConversionError13(
|
|
2085
|
+
"MESH_FEATURE_PROPERTY_UNMAPPED",
|
|
2086
|
+
"Property table columns must be declared in the selected class"
|
|
2087
|
+
);
|
|
2088
|
+
}
|
|
2089
|
+
const fields = Object.entries(properties).map(([name, property]) => {
|
|
2090
|
+
const declaration = property;
|
|
2091
|
+
const column = table.properties?.[name];
|
|
2092
|
+
if (declaration.array || !["SCALAR", "STRING"].includes(declaration.type) || declaration.normalized || declaration.offset !== void 0 || declaration.scale !== void 0 || column?.offset !== void 0 || column?.scale !== void 0 || declaration.required && (declaration.noData !== void 0 || declaration.default !== void 0)) {
|
|
2093
|
+
throw new TileConversionError13(
|
|
2094
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2095
|
+
`Property ${name} requires an explicit separately qualified mapping`
|
|
2096
|
+
);
|
|
2097
|
+
}
|
|
2098
|
+
const type = declaration.type === "STRING" ? "utf8" : Object.hasOwn(SCALAR_TYPES, declaration.componentType || "") ? SCALAR_TYPES[declaration.componentType] : void 0;
|
|
2099
|
+
if (!type) {
|
|
2100
|
+
throw new TileConversionError13(
|
|
2101
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2102
|
+
`Property ${name} has no supported declared scalar width`
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
return {
|
|
2106
|
+
name,
|
|
2107
|
+
type,
|
|
2108
|
+
nullable: declaration.required !== true,
|
|
2109
|
+
metadata: { "loaders.gl:structural-property": JSON.stringify(declaration) }
|
|
2110
|
+
};
|
|
2111
|
+
});
|
|
2112
|
+
const identifierName = options.featureIdField || "feature_id";
|
|
2113
|
+
const sourceIdentifier = options.sourceFeatureIdProperty;
|
|
2114
|
+
const sourceField = sourceIdentifier ? fields.find((field) => field.name === sourceIdentifier) : void 0;
|
|
2115
|
+
if (sourceIdentifier && !sourceField || fields.some((field) => field.name === identifierName && field !== sourceField) || sourceField && !["utf8", "int8", "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64"].includes(
|
|
2116
|
+
String(sourceField.type)
|
|
2117
|
+
)) {
|
|
2118
|
+
throw new TileConversionError13(
|
|
2119
|
+
"MESH_FEATURE_ID_PROPERTY_REQUIRED",
|
|
2120
|
+
"Select a declared string/integer identifier and a distinct output identifier name"
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
return {
|
|
2124
|
+
...options,
|
|
2125
|
+
schema: {
|
|
2126
|
+
fields: [
|
|
2127
|
+
{
|
|
2128
|
+
...sourceField,
|
|
2129
|
+
name: identifierName,
|
|
2130
|
+
type: sourceField?.type || "uint32",
|
|
2131
|
+
nullable: false
|
|
2132
|
+
},
|
|
2133
|
+
...fields.filter((field) => field !== sourceField)
|
|
2134
|
+
],
|
|
2135
|
+
metadata: { "loaders.gl:source-schema": JSON.stringify(metadata.schema) }
|
|
2136
|
+
}
|
|
2137
|
+
};
|
|
2138
|
+
}
|
|
1478
2139
|
|
|
1479
2140
|
// src/v5/mesh-source-features.ts
|
|
1480
|
-
import { TileConversionError as
|
|
2141
|
+
import { TileConversionError as TileConversionError14 } from "@loaders.gl/tile-converter/v5/core";
|
|
1481
2142
|
function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
1482
2143
|
const declarations = primitive.extensions?.EXT_mesh_features?.featureIds;
|
|
1483
2144
|
const metadata = gltf.extensions?.EXT_structural_metadata;
|
|
1484
2145
|
const legacy = primitive.attributes._BATCHID;
|
|
1485
2146
|
if (!declarations && !legacy && !metadata && !Object.keys(payload.batchTableJson || {}).length && !payload.header?.batchLength) {
|
|
1486
2147
|
if (Object.keys(primitive.attributes).some((name) => /^_FEATURE_ID_\d+$/.test(name)))
|
|
1487
|
-
throw new
|
|
2148
|
+
throw new TileConversionError14(
|
|
1488
2149
|
"MESH_FEATURE_MAPPING_REQUIRED",
|
|
1489
2150
|
"Feature attributes require an EXT_mesh_features declaration"
|
|
1490
2151
|
);
|
|
1491
2152
|
return void 0;
|
|
1492
2153
|
}
|
|
1493
2154
|
if (!options)
|
|
1494
|
-
throw new
|
|
2155
|
+
throw new TileConversionError14(
|
|
1495
2156
|
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
1496
|
-
"Feature extraction requires
|
|
2157
|
+
"Feature extraction requires a class selection and a declared Arrow or inline source schema"
|
|
1497
2158
|
);
|
|
1498
2159
|
if (!options.metadataClass)
|
|
1499
|
-
throw new
|
|
2160
|
+
throw new TileConversionError14(
|
|
1500
2161
|
"MESH_FEATURE_CLASS_REQUIRED",
|
|
1501
2162
|
"Feature metadata class must be explicit"
|
|
1502
2163
|
);
|
|
2164
|
+
const mapping = "schema" in options ? options : createMeshSourceFeatureOptions(gltf, options);
|
|
1503
2165
|
let count;
|
|
1504
2166
|
let attribute = legacy;
|
|
1505
2167
|
let properties;
|
|
1506
2168
|
if (declarations) {
|
|
1507
2169
|
if (legacy || declarations.length !== 1 || metadata?.propertyTables?.length !== 1 || metadata.propertyTextures?.length || metadata.propertyAttributes?.length)
|
|
1508
|
-
throw new
|
|
2170
|
+
throw new TileConversionError14(
|
|
1509
2171
|
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1510
2172
|
"Only one attribute-backed feature set and one property table are supported"
|
|
1511
2173
|
);
|
|
1512
2174
|
const declaration = declarations[0];
|
|
1513
2175
|
if (!Number.isInteger(declaration.attribute) || declaration.attribute < 0 || declaration.propertyTable !== 0 || declaration.texture || declaration.implicit || declaration.nullFeatureId !== void 0)
|
|
1514
|
-
throw new
|
|
2176
|
+
throw new TileConversionError14(
|
|
1515
2177
|
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1516
2178
|
"Features require an attribute and property table 0 without null/texture/implicit IDs"
|
|
1517
2179
|
);
|
|
1518
2180
|
attribute = primitive.attributes[`_FEATURE_ID_${declaration.attribute}`];
|
|
1519
2181
|
const table = metadata.propertyTables[0];
|
|
1520
2182
|
if (table.class !== options.metadataClass || table.count !== declaration.featureCount)
|
|
1521
|
-
throw new
|
|
2183
|
+
throw new TileConversionError14(
|
|
1522
2184
|
"MESH_FEATURE_CLASS_MISMATCH",
|
|
1523
2185
|
"Feature class and declared table counts must agree"
|
|
1524
2186
|
);
|
|
1525
2187
|
count = table.count;
|
|
2188
|
+
if (!Number.isSafeInteger(count) || count <= 0 || count > mesh.attributes.POSITION.value.length / 3) {
|
|
2189
|
+
throw new TileConversionError14(
|
|
2190
|
+
"MESH_FEATURE_IDS_INVALID",
|
|
2191
|
+
"Feature row count exceeds the available mesh vertices"
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
1526
2194
|
properties = {};
|
|
1527
2195
|
const classProperties = metadata.schema?.classes?.[table.class]?.properties;
|
|
1528
2196
|
if (!classProperties)
|
|
1529
|
-
throw new
|
|
2197
|
+
throw new TileConversionError14(
|
|
1530
2198
|
"MESH_FEATURE_SCHEMA_REQUIRED",
|
|
1531
2199
|
"A decoded inline structural metadata class is required"
|
|
1532
2200
|
);
|
|
1533
2201
|
if (Object.keys(table.properties || {}).some((name) => !Object.hasOwn(classProperties, name)))
|
|
1534
|
-
throw new
|
|
2202
|
+
throw new TileConversionError14(
|
|
1535
2203
|
"MESH_FEATURE_PROPERTY_UNMAPPED",
|
|
1536
2204
|
"Property table columns must be declared in the metadata class"
|
|
1537
2205
|
);
|
|
1538
2206
|
for (const [name, property] of Object.entries(classProperties)) {
|
|
1539
2207
|
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
|
|
2208
|
+
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))
|
|
2209
|
+
throw new TileConversionError14(
|
|
1542
2210
|
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
1543
2211
|
`Property ${name} requires a separately qualified semantic mapping`
|
|
1544
2212
|
);
|
|
1545
2213
|
const data = column?.data;
|
|
1546
|
-
if (data === void 0 && property.
|
|
1547
|
-
throw new
|
|
2214
|
+
if (column !== void 0 && (data === void 0 || data === null) || data === void 0 && property.required === true)
|
|
2215
|
+
throw new TileConversionError14(
|
|
1548
2216
|
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
1549
2217
|
`Property ${name} has not been decoded`
|
|
1550
2218
|
);
|
|
1551
|
-
|
|
2219
|
+
const integer64 = ["INT64", "UINT64"].includes(property.componentType || "");
|
|
2220
|
+
if (integer64 && property.default !== void 0 && (typeof property.default !== "number" || !Number.isSafeInteger(property.default))) {
|
|
2221
|
+
throw new TileConversionError14(
|
|
2222
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2223
|
+
`Property ${name} requires an exact 64-bit default`
|
|
2224
|
+
);
|
|
2225
|
+
}
|
|
2226
|
+
const defaultValue = integer64 && property.default !== void 0 ? BigInt(property.default) : property.default ?? null;
|
|
2227
|
+
const decoded = data ?? Array.from({ length: count }, () => defaultValue);
|
|
2228
|
+
if (property.noData === void 0 || data === void 0) {
|
|
2229
|
+
properties[name] = decoded;
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
const sentinel = property.noData;
|
|
2233
|
+
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)
|
|
2234
|
+
throw new TileConversionError14(
|
|
2235
|
+
"MESH_FEATURE_PROPERTY_UNSUPPORTED",
|
|
2236
|
+
`Property ${name} requires a valid decoded noData mapping`
|
|
2237
|
+
);
|
|
2238
|
+
const comparable = integer64 ? BigInt(sentinel) : sentinel;
|
|
2239
|
+
properties[name] = Array.from(
|
|
2240
|
+
decoded,
|
|
2241
|
+
(value) => value === comparable ? defaultValue : value
|
|
2242
|
+
);
|
|
1552
2243
|
}
|
|
1553
2244
|
} else {
|
|
1554
2245
|
if (metadata)
|
|
1555
|
-
throw new
|
|
2246
|
+
throw new TileConversionError14(
|
|
1556
2247
|
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1557
2248
|
"Structural metadata requires an attribute-backed feature declaration"
|
|
1558
2249
|
);
|
|
@@ -1563,7 +2254,7 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
|
1563
2254
|
if (!Number.isSafeInteger(count) || count <= 0 || !attribute || attribute.components !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(attribute.value).some(
|
|
1564
2255
|
(value) => !Number.isSafeInteger(value) || value < 0 || value >= count
|
|
1565
2256
|
))
|
|
1566
|
-
throw new
|
|
2257
|
+
throw new TileConversionError14(
|
|
1567
2258
|
"MESH_FEATURE_IDS_INVALID",
|
|
1568
2259
|
"Feature IDs must be one valid property-table row index per vertex"
|
|
1569
2260
|
);
|
|
@@ -1571,20 +2262,20 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
|
1571
2262
|
if (Object.keys(primitive.attributes).some(
|
|
1572
2263
|
(name) => (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) && name !== selectedAttribute
|
|
1573
2264
|
))
|
|
1574
|
-
throw new
|
|
2265
|
+
throw new TileConversionError14(
|
|
1575
2266
|
"MESH_FEATURE_MAPPING_UNSUPPORTED",
|
|
1576
2267
|
"Additional feature ID sets require separate mappings"
|
|
1577
2268
|
);
|
|
1578
2269
|
for (const [name, values] of Object.entries(properties)) {
|
|
1579
2270
|
if (!values || !Array.isArray(values) && !ArrayBuffer.isView(values) || values.length !== count)
|
|
1580
|
-
throw new
|
|
2271
|
+
throw new TileConversionError14(
|
|
1581
2272
|
"MESH_FEATURE_PROPERTY_UNAVAILABLE",
|
|
1582
2273
|
`Property ${name} must be a decoded column matching the table count`
|
|
1583
2274
|
);
|
|
1584
2275
|
}
|
|
1585
2276
|
const sourceIdentifier = options.sourceFeatureIdProperty;
|
|
1586
2277
|
if (sourceIdentifier && !properties[sourceIdentifier])
|
|
1587
|
-
throw new
|
|
2278
|
+
throw new TileConversionError14(
|
|
1588
2279
|
"MESH_FEATURE_ID_PROPERTY_REQUIRED",
|
|
1589
2280
|
"Stable source identifier property is missing"
|
|
1590
2281
|
);
|
|
@@ -1596,18 +2287,19 @@ function extractMeshFeatures(gltf, primitive, payload, mesh, options) {
|
|
|
1596
2287
|
Object.entries(properties).filter(([name]) => name !== sourceIdentifier).map(([name, values]) => [name, values[index]])
|
|
1597
2288
|
)
|
|
1598
2289
|
})),
|
|
1599
|
-
|
|
2290
|
+
mapping
|
|
1600
2291
|
);
|
|
1601
2292
|
const indices = mesh.indices?.value;
|
|
1602
2293
|
const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
|
|
1603
2294
|
for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
|
|
1604
|
-
const
|
|
1605
|
-
(corner) =>
|
|
2295
|
+
const corners = [0, 1, 2].map(
|
|
2296
|
+
(corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
|
|
1606
2297
|
);
|
|
1607
|
-
|
|
1608
|
-
|
|
2298
|
+
const rows = corners.map((corner) => attribute.value[corner]);
|
|
2299
|
+
if (rows.some((row) => row !== rows[0]) && new Set(corners).size === 3)
|
|
2300
|
+
throw new TileConversionError14(
|
|
1609
2301
|
"MESH_FEATURE_TRIANGLE_MIXED",
|
|
1610
|
-
"All vertices of a triangle must reference the same feature row"
|
|
2302
|
+
"All vertices of a nondegenerate triangle must reference the same feature row"
|
|
1611
2303
|
);
|
|
1612
2304
|
triangleFeatureIndices[triangle] = Number(rows[0]);
|
|
1613
2305
|
}
|
|
@@ -1628,13 +2320,13 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1628
2320
|
const metadata = await source.inspect(signal);
|
|
1629
2321
|
const reference = metadata.spatialReference;
|
|
1630
2322
|
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
|
|
2323
|
+
throw new TileConversionError15(
|
|
1632
2324
|
"MESH_SOURCE_FRAME_UNSUPPORTED",
|
|
1633
2325
|
"Mesh extraction requires native EPSG:4978 xyz coordinates in meters and ellipsoidal heights"
|
|
1634
2326
|
);
|
|
1635
2327
|
}
|
|
1636
2328
|
if (metadata.tileset.metadata || metadata.tileset.groups?.length) {
|
|
1637
|
-
throw new
|
|
2329
|
+
throw new TileConversionError15(
|
|
1638
2330
|
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1639
2331
|
"Tileset/group metadata requires a separate mapping"
|
|
1640
2332
|
);
|
|
@@ -1646,25 +2338,25 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1646
2338
|
for await (const item of source.read(inspection, signal)) {
|
|
1647
2339
|
const placementReference = item.tile.header._spatialReference || tileset.spatialReference;
|
|
1648
2340
|
if (placementReference && (placementReference.status !== "native" || placementReference.sourceCrs !== "EPSG:4978"))
|
|
1649
|
-
throw new
|
|
2341
|
+
throw new TileConversionError15(
|
|
1650
2342
|
"MESH_SOURCE_FRAME_UNSUPPORTED",
|
|
1651
2343
|
"Nested mesh placements require native ECEF coordinates"
|
|
1652
2344
|
);
|
|
1653
2345
|
if (item.tile.header.metadata || item.tile.header.implicitMetadata)
|
|
1654
|
-
throw new
|
|
2346
|
+
throw new TileConversionError15(
|
|
1655
2347
|
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1656
2348
|
"Tile metadata requires a separate mapping"
|
|
1657
2349
|
);
|
|
1658
2350
|
for (const content of item.contents) {
|
|
1659
2351
|
signal?.throwIfAborted();
|
|
1660
2352
|
if (content.metadata || content.group !== void 0)
|
|
1661
|
-
throw new
|
|
2353
|
+
throw new TileConversionError15(
|
|
1662
2354
|
"MESH_SOURCE_METADATA_UNSUPPORTED",
|
|
1663
2355
|
"Content metadata requires a separate mapping"
|
|
1664
2356
|
);
|
|
1665
2357
|
const payload = content.payload;
|
|
1666
2358
|
if (!payload?.gltf || !["glTF", "gltf", "b3dm"].includes(payload.type || content.type || "")) {
|
|
1667
|
-
throw new
|
|
2359
|
+
throw new TileConversionError15(
|
|
1668
2360
|
"MESH_SOURCE_CONTENT_UNSUPPORTED",
|
|
1669
2361
|
"Mesh extraction requires decoded GLB/glTF or B3DM content"
|
|
1670
2362
|
);
|
|
@@ -1673,40 +2365,47 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1673
2365
|
if (gltf.animations?.length || gltf.skins?.length || Object.keys(gltf.extensions || {}).some(
|
|
1674
2366
|
(name) => !["EXT_structural_metadata", "CESIUM_RTC"].includes(name)
|
|
1675
2367
|
)) {
|
|
1676
|
-
throw new
|
|
2368
|
+
throw new TileConversionError15(
|
|
1677
2369
|
"MESH_SOURCE_SCENE_UNSUPPORTED",
|
|
1678
2370
|
"Animated/skinned scenes and unmapped scene extensions are unsupported"
|
|
1679
2371
|
);
|
|
1680
2372
|
}
|
|
1681
2373
|
const axis = payload.gltfUpAxis || "Y";
|
|
1682
2374
|
if (!["X", "Y", "Z"].includes(axis))
|
|
1683
|
-
throw new
|
|
1684
|
-
const placement = new
|
|
2375
|
+
throw new TileConversionError15("MESH_SOURCE_AXIS_UNSUPPORTED", "Unknown glTF up axis");
|
|
2376
|
+
const placement = new Matrix42(item.tile.computedTransform);
|
|
1685
2377
|
const rtc = payload.rtcCenter || gltf.extensions?.CESIUM_RTC?.center;
|
|
1686
2378
|
if (rtc) {
|
|
1687
2379
|
if (rtc.length !== 3 || rtc.some((value) => !Number.isFinite(value)))
|
|
1688
|
-
throw new
|
|
2380
|
+
throw new TileConversionError15(
|
|
1689
2381
|
"MESH_SOURCE_TRANSFORM_INVALID",
|
|
1690
2382
|
"RTC center must contain finite xyz coordinates"
|
|
1691
2383
|
);
|
|
1692
2384
|
placement.translate(rtc);
|
|
1693
2385
|
}
|
|
1694
2386
|
if (axis === "Y")
|
|
1695
|
-
placement.multiplyRight(new
|
|
2387
|
+
placement.multiplyRight(new Matrix42([1, 0, 0, 0, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 1]));
|
|
1696
2388
|
if (axis === "X")
|
|
1697
|
-
placement.multiplyRight(new
|
|
2389
|
+
placement.multiplyRight(new Matrix42([0, 0, 1, 0, 0, 1, 0, 0, -1, 0, 0, 0, 0, 0, 0, 1]));
|
|
1698
2390
|
const roots = gltf.scene?.nodes || (gltf.scenes?.length === 1 ? gltf.scenes[0].nodes : void 0);
|
|
1699
2391
|
if (!roots?.length)
|
|
1700
|
-
throw new
|
|
2392
|
+
throw new TileConversionError15(
|
|
1701
2393
|
"MESH_SOURCE_SCENE_UNSUPPORTED",
|
|
1702
2394
|
"A default scene or exactly one scene is required"
|
|
1703
2395
|
);
|
|
1704
2396
|
let primitiveIndex = 0;
|
|
1705
|
-
|
|
1706
|
-
|
|
2397
|
+
const primitives = traverseMeshNodes(roots, placement);
|
|
2398
|
+
let current = primitives.next();
|
|
2399
|
+
while (!current.done) {
|
|
2400
|
+
const selected = current.value;
|
|
2401
|
+
const next = primitives.next();
|
|
2402
|
+
const resource = await extractPrimitive(
|
|
1707
2403
|
selected.primitive,
|
|
1708
2404
|
selected.transform,
|
|
1709
|
-
`${item.tile.id}/${content.index}/${primitiveIndex++}
|
|
2405
|
+
`${item.tile.id}/${content.index}/${primitiveIndex++}`,
|
|
2406
|
+
options.readExternalResource,
|
|
2407
|
+
content.uri,
|
|
2408
|
+
signal
|
|
1710
2409
|
);
|
|
1711
2410
|
const features = extractMeshFeatures(
|
|
1712
2411
|
gltf,
|
|
@@ -1715,10 +2414,15 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1715
2414
|
resource.mesh,
|
|
1716
2415
|
options.features
|
|
1717
2416
|
);
|
|
1718
|
-
yield {
|
|
2417
|
+
yield {
|
|
2418
|
+
...resource,
|
|
2419
|
+
features,
|
|
2420
|
+
contentPrimitive: { index: primitiveIndex - 1, last: Boolean(next.done) }
|
|
2421
|
+
};
|
|
2422
|
+
current = next;
|
|
1719
2423
|
}
|
|
1720
2424
|
if (!primitiveIndex)
|
|
1721
|
-
throw new
|
|
2425
|
+
throw new TileConversionError15(
|
|
1722
2426
|
"MESH_SOURCE_CONTENT_EMPTY",
|
|
1723
2427
|
"Declared content contains no supported mesh primitive"
|
|
1724
2428
|
);
|
|
@@ -1730,16 +2434,16 @@ function createMeshTilesetConversionSource(tileset, options = {}) {
|
|
|
1730
2434
|
function* traverseMeshNodes(nodes, parent, ancestors = /* @__PURE__ */ new Set()) {
|
|
1731
2435
|
for (const node of nodes) {
|
|
1732
2436
|
if (ancestors.has(node) || node.skin || node.weights || Object.keys(node.extensions || {}).length)
|
|
1733
|
-
throw new
|
|
2437
|
+
throw new TileConversionError15(
|
|
1734
2438
|
"MESH_SOURCE_NODE_UNSUPPORTED",
|
|
1735
2439
|
"Cyclic, skinned, morphed, or extended nodes are unsupported"
|
|
1736
2440
|
);
|
|
1737
|
-
const transform = new
|
|
1738
|
-
node.matrix ? new
|
|
2441
|
+
const transform = new Matrix42(parent).multiplyRight(
|
|
2442
|
+
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
2443
|
);
|
|
1740
2444
|
if (node.mesh) {
|
|
1741
2445
|
if (node.mesh.weights || Object.keys(node.mesh.extensions || {}).length)
|
|
1742
|
-
throw new
|
|
2446
|
+
throw new TileConversionError15(
|
|
1743
2447
|
"MESH_SOURCE_NODE_UNSUPPORTED",
|
|
1744
2448
|
"Morph weights and mesh extensions are unsupported"
|
|
1745
2449
|
);
|
|
@@ -1748,14 +2452,14 @@ function* traverseMeshNodes(nodes, parent, ancestors = /* @__PURE__ */ new Set()
|
|
|
1748
2452
|
yield* traverseMeshNodes(node.children || [], transform, /* @__PURE__ */ new Set([...ancestors, node]));
|
|
1749
2453
|
}
|
|
1750
2454
|
}
|
|
1751
|
-
function extractPrimitive(primitive, transform, id) {
|
|
2455
|
+
async function extractPrimitive(primitive, transform, id, readExternalResource, contentUri, signal) {
|
|
1752
2456
|
if (primitive.targets?.length || Object.keys(primitive.extensions || {}).some((name) => name !== "EXT_mesh_features"))
|
|
1753
|
-
throw new
|
|
2457
|
+
throw new TileConversionError15(
|
|
1754
2458
|
"MESH_SOURCE_PRIMITIVE_UNSUPPORTED",
|
|
1755
2459
|
"Morph targets and unmapped primitive extensions are unsupported"
|
|
1756
2460
|
);
|
|
1757
2461
|
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
|
|
2462
|
+
throw new TileConversionError15(
|
|
1759
2463
|
"MESH_SOURCE_TRANSFORM_INVALID",
|
|
1760
2464
|
"Mesh placement requires a finite invertible affine transform with positive determinant"
|
|
1761
2465
|
);
|
|
@@ -1763,7 +2467,7 @@ function extractPrimitive(primitive, transform, id) {
|
|
|
1763
2467
|
for (const [name, accessor] of Object.entries(primitive.attributes)) {
|
|
1764
2468
|
if (name === "_BATCHID" || /^_FEATURE_ID_\d+$/.test(name)) continue;
|
|
1765
2469
|
if (!["POSITION", "NORMAL", "COLOR_0", "TEXCOORD_0"].includes(name))
|
|
1766
|
-
throw new
|
|
2470
|
+
throw new TileConversionError15(
|
|
1767
2471
|
"MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
1768
2472
|
`Unsupported mesh attribute ${name}`
|
|
1769
2473
|
);
|
|
@@ -1776,9 +2480,9 @@ function extractPrimitive(primitive, transform, id) {
|
|
|
1776
2480
|
const mesh = validateMeshGeometry(
|
|
1777
2481
|
{
|
|
1778
2482
|
topology: "triangle-list",
|
|
1779
|
-
mode: primitive.mode ?? 4,
|
|
2483
|
+
mode: primitive.mode === 5 ? 4 : primitive.mode ?? 4,
|
|
1780
2484
|
attributes,
|
|
1781
|
-
indices: primitive.
|
|
2485
|
+
indices: createTriangleIndices(primitive, (attributes.POSITION?.value.length ?? 0) / 3)
|
|
1782
2486
|
},
|
|
1783
2487
|
true
|
|
1784
2488
|
);
|
|
@@ -1799,7 +2503,7 @@ function extractPrimitive(primitive, transform, id) {
|
|
|
1799
2503
|
mesh.attributes.POSITION = { value: positions, size: 3 };
|
|
1800
2504
|
if (mesh.attributes.NORMAL) {
|
|
1801
2505
|
const values = mesh.attributes.NORMAL.value;
|
|
1802
|
-
const normalMatrix = new
|
|
2506
|
+
const normalMatrix = new Matrix42(transform).invert().transpose();
|
|
1803
2507
|
const normals = new Float32Array(values.length);
|
|
1804
2508
|
for (let index = 0; index < values.length; index += 3) {
|
|
1805
2509
|
const normal = normalMatrix.transformAsVector(Array.from(values.subarray(index, index + 3)));
|
|
@@ -1815,14 +2519,19 @@ function extractPrimitive(primitive, transform, id) {
|
|
|
1815
2519
|
id,
|
|
1816
2520
|
mesh,
|
|
1817
2521
|
origin: minimum.map((value, axis) => value / 2 + maximum[axis] / 2),
|
|
1818
|
-
material:
|
|
2522
|
+
material: await mapMeshSourceMaterial(
|
|
2523
|
+
primitive.material,
|
|
2524
|
+
readExternalResource,
|
|
2525
|
+
contentUri,
|
|
2526
|
+
signal
|
|
2527
|
+
)
|
|
1819
2528
|
};
|
|
1820
2529
|
}
|
|
1821
|
-
function
|
|
2530
|
+
async function mapMeshSourceMaterial(material, readExternalResource, contentUri, signal) {
|
|
1822
2531
|
if (!material) return void 0;
|
|
1823
2532
|
const pbr = material.pbrMetallicRoughness;
|
|
1824
|
-
if (Object.
|
|
1825
|
-
(name) => ![
|
|
2533
|
+
if (Object.entries(material).some(
|
|
2534
|
+
([name, value]) => value !== void 0 && ![
|
|
1826
2535
|
"id",
|
|
1827
2536
|
"name",
|
|
1828
2537
|
"pbrMetallicRoughness",
|
|
@@ -1832,16 +2541,23 @@ function mapMaterial(material) {
|
|
|
1832
2541
|
"emissiveFactor",
|
|
1833
2542
|
"extras"
|
|
1834
2543
|
].includes(name)
|
|
1835
|
-
) || Object.
|
|
1836
|
-
(name) => !["baseColorFactor", "baseColorTexture", "metallicFactor", "roughnessFactor"].includes(name)
|
|
2544
|
+
) || Object.entries(pbr || {}).some(
|
|
2545
|
+
([name, value]) => value !== void 0 && !["baseColorFactor", "baseColorTexture", "metallicFactor", "roughnessFactor"].includes(name)
|
|
1837
2546
|
) || material.emissiveFactor?.some((value) => value !== 0))
|
|
1838
|
-
throw new
|
|
2547
|
+
throw new TileConversionError15(
|
|
1839
2548
|
"MESH_SOURCE_MATERIAL_UNSUPPORTED",
|
|
1840
2549
|
"Only metallic-roughness factors and one embedded base-color image are supported"
|
|
1841
2550
|
);
|
|
1842
2551
|
return {
|
|
1843
2552
|
baseColorFactor: pbr?.baseColorFactor,
|
|
1844
|
-
..."baseColorTexture" in (pbr || {}) ? {
|
|
2553
|
+
..."baseColorTexture" in (pbr || {}) ? {
|
|
2554
|
+
baseColorTexture: await resolveMeshSourceTexture(
|
|
2555
|
+
pbr.baseColorTexture,
|
|
2556
|
+
readExternalResource,
|
|
2557
|
+
contentUri,
|
|
2558
|
+
signal
|
|
2559
|
+
)
|
|
2560
|
+
} : {},
|
|
1845
2561
|
metallicFactor: pbr?.metallicFactor,
|
|
1846
2562
|
roughnessFactor: pbr?.roughnessFactor,
|
|
1847
2563
|
alphaMode: material.alphaMode,
|
|
@@ -1849,53 +2565,111 @@ function mapMaterial(material) {
|
|
|
1849
2565
|
doubleSided: material.doubleSided
|
|
1850
2566
|
};
|
|
1851
2567
|
}
|
|
2568
|
+
function createTriangleIndices(primitive, vertexCount) {
|
|
2569
|
+
const attribute = primitive.indices ? {
|
|
2570
|
+
value: primitive.indices.value,
|
|
2571
|
+
size: primitive.indices.components,
|
|
2572
|
+
normalized: primitive.indices.normalized
|
|
2573
|
+
} : void 0;
|
|
2574
|
+
if (primitive.mode !== 5) return attribute;
|
|
2575
|
+
const sourceIndices = validateMeshIndices(attribute, vertexCount, 5);
|
|
2576
|
+
const indexCount = sourceIndices?.length ?? vertexCount;
|
|
2577
|
+
if (!Number.isSafeInteger(indexCount) || indexCount < 3) {
|
|
2578
|
+
throw new TileConversionError15(
|
|
2579
|
+
"MESH_TRIANGLE_COUNT_INVALID",
|
|
2580
|
+
"Triangle strips require at least three vertices"
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
const IndexArray = vertexCount < 65536 ? Uint16Array : Uint32Array;
|
|
2584
|
+
const indices = new IndexArray((indexCount - 2) * 3);
|
|
2585
|
+
for (let triangle = 0; triangle < indexCount - 2; triangle++) {
|
|
2586
|
+
const second = triangle + 1 + triangle % 2;
|
|
2587
|
+
const third = triangle + 2 - triangle % 2;
|
|
2588
|
+
indices[triangle * 3] = sourceIndices?.[triangle] ?? triangle;
|
|
2589
|
+
indices[triangle * 3 + 1] = sourceIndices?.[second] ?? second;
|
|
2590
|
+
indices[triangle * 3 + 2] = sourceIndices?.[third] ?? third;
|
|
2591
|
+
}
|
|
2592
|
+
return { value: indices, size: 1 };
|
|
2593
|
+
}
|
|
1852
2594
|
|
|
1853
2595
|
// src/v5/i3s-mesh-conversion.ts
|
|
1854
|
-
import {
|
|
1855
|
-
|
|
2596
|
+
import {
|
|
2597
|
+
encodeI3SMeshLayer,
|
|
2598
|
+
encodeI3SMeshLayerWithDraco,
|
|
2599
|
+
preloadI3SMeshCompression
|
|
2600
|
+
} from "@loaders.gl/i3s";
|
|
2601
|
+
import { TileConversionError as TileConversionError16 } from "@loaders.gl/tile-converter/v5/core";
|
|
1856
2602
|
import { createBoundedMemoryTileConversionSink as createBoundedMemoryTileConversionSink2 } from "@loaders.gl/tile-converter/v5/core";
|
|
1857
2603
|
function createI3SMeshConversionCodec(options) {
|
|
2604
|
+
if (options.integratedMeshPolicy !== void 0 && options.integratedMeshPolicy !== "as-3d-object") {
|
|
2605
|
+
throw new TileConversionError16(
|
|
2606
|
+
"I3S_LAYER_TYPE_POLICY_INVALID",
|
|
2607
|
+
"Unknown IntegratedMesh conversion policy"
|
|
2608
|
+
);
|
|
2609
|
+
}
|
|
1858
2610
|
const reference = options.spatialContext.spatialReference;
|
|
1859
2611
|
const height = reference.targetHeightReference === "native" ? reference.heightReference : reference.targetHeightReference;
|
|
1860
2612
|
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
|
|
2613
|
+
throw new TileConversionError16(
|
|
1862
2614
|
"I3S_MESH_FRAME_UNSUPPORTED",
|
|
1863
2615
|
"I3S mesh authoring requires resolved ECEF coordinates and ellipsoidal heights"
|
|
1864
2616
|
);
|
|
2617
|
+
let objectIdOffset = 0;
|
|
1865
2618
|
return {
|
|
1866
2619
|
/** Places a source mesh in ECEF and authors one layer without modifying source arrays. */
|
|
1867
2620
|
async *convert(resource, _inspection, signal) {
|
|
1868
2621
|
signal?.throwIfAborted();
|
|
2622
|
+
if (resource.sourceLayerType === "IntegratedMesh" && options.integratedMeshPolicy !== "as-3d-object") {
|
|
2623
|
+
throw new TileConversionError16(
|
|
2624
|
+
"I3S_LAYER_TYPE_CHANGE_REQUIRED",
|
|
2625
|
+
"IntegratedMesh to 3DObject conversion requires integratedMeshPolicy=as-3d-object"
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
1869
2628
|
const mesh = validateMeshGeometry(resource.mesh, true);
|
|
1870
2629
|
const positions = mesh.attributes.POSITION.value;
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
2630
|
+
const context = options.spatialContext;
|
|
2631
|
+
const transformed = "transformGeometryAsync" in context ? await context.transformGeometryAsync(
|
|
2632
|
+
positions,
|
|
2633
|
+
mesh.attributes.NORMAL?.value,
|
|
2634
|
+
resource.normalReferenceFrame ?? options.normalReferenceFrame
|
|
2635
|
+
) : {
|
|
2636
|
+
positions: context.transformPositions(positions),
|
|
2637
|
+
normals: mesh.attributes.NORMAL ? context.transformNormals(mesh.attributes.NORMAL.value, positions) : void 0
|
|
1874
2638
|
};
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
size: 3
|
|
1879
|
-
};
|
|
2639
|
+
signal?.throwIfAborted();
|
|
2640
|
+
mesh.attributes.POSITION = { value: transformed.positions, size: 3 };
|
|
2641
|
+
if (mesh.attributes.NORMAL) mesh.attributes.NORMAL = { value: transformed.normals, size: 3 };
|
|
1880
2642
|
let layer;
|
|
1881
2643
|
try {
|
|
1882
|
-
|
|
2644
|
+
await preloadI3SMeshCompression();
|
|
2645
|
+
const writerOptions = {
|
|
1883
2646
|
...options,
|
|
1884
2647
|
material: resource.material,
|
|
1885
|
-
features: resource.features
|
|
1886
|
-
|
|
2648
|
+
features: resource.features,
|
|
2649
|
+
objectIdOffset
|
|
2650
|
+
};
|
|
2651
|
+
layer = options.draco === false ? encodeI3SMeshLayer(mesh, writerOptions) : await encodeI3SMeshLayerWithDraco(mesh, writerOptions, options.dracoLibraryOptions);
|
|
1887
2652
|
} catch (error) {
|
|
1888
|
-
throw new
|
|
2653
|
+
throw new TileConversionError16(
|
|
1889
2654
|
"I3S_MESH_PROFILE_UNSUPPORTED",
|
|
1890
2655
|
error instanceof Error ? error.message : String(error)
|
|
1891
2656
|
);
|
|
1892
2657
|
}
|
|
1893
2658
|
signal?.throwIfAborted();
|
|
1894
|
-
|
|
2659
|
+
objectIdOffset += resource.features?.batches.reduce((total, batch) => total + batch.data.numRows, 0) ?? 1;
|
|
2660
|
+
yield { id: resource.id, ...layer, sourceLayerType: resource.sourceLayerType };
|
|
1895
2661
|
},
|
|
1896
2662
|
/** Reports measured precision and explicitly authorized integer representation changes. */
|
|
1897
2663
|
async validateOutput(resource) {
|
|
1898
2664
|
return [
|
|
2665
|
+
...resource.sourceLayerType === "IntegratedMesh" ? [
|
|
2666
|
+
{
|
|
2667
|
+
code: "I3S_LAYER_TYPE_CHANGED",
|
|
2668
|
+
message: "Explicitly authored IntegratedMesh geometry as I3S 1.7 3DObject; integrated surface layer semantics and source LOD are not preserved",
|
|
2669
|
+
severity: "warning",
|
|
2670
|
+
resourceId: resource.id
|
|
2671
|
+
}
|
|
2672
|
+
] : [],
|
|
1899
2673
|
...resource.maximumPositionError > 0 ? [
|
|
1900
2674
|
{
|
|
1901
2675
|
code: "I3S_MESH_POSITION_ROUNDING",
|
|
@@ -1925,19 +2699,26 @@ function createSingleMeshI3SSink(options) {
|
|
|
1925
2699
|
async write(resource, signal) {
|
|
1926
2700
|
signal?.throwIfAborted();
|
|
1927
2701
|
if (state !== "empty")
|
|
1928
|
-
throw new
|
|
2702
|
+
throw new TileConversionError16(
|
|
1929
2703
|
"SINGLE_I3S_MESH_UNAVAILABLE",
|
|
1930
2704
|
"The initial I3S sink accepts exactly one mesh layer"
|
|
1931
2705
|
);
|
|
1932
2706
|
state = "writing";
|
|
1933
2707
|
for (const [resourceId, bytes] of Object.entries(resource.files))
|
|
1934
|
-
await memory.write(
|
|
2708
|
+
await memory.write(
|
|
2709
|
+
{
|
|
2710
|
+
resourceId,
|
|
2711
|
+
parts: [bytes],
|
|
2712
|
+
contentType: resourceId.endsWith(".png") ? "image/png" : resourceId.endsWith(".jpg") ? "image/jpeg" : "application/gzip"
|
|
2713
|
+
},
|
|
2714
|
+
signal
|
|
2715
|
+
);
|
|
1935
2716
|
state = "written";
|
|
1936
2717
|
},
|
|
1937
2718
|
/** Commits a complete single-layer result. */
|
|
1938
2719
|
async finalize(report) {
|
|
1939
2720
|
if (state !== "written")
|
|
1940
|
-
throw new
|
|
2721
|
+
throw new TileConversionError16(
|
|
1941
2722
|
"SINGLE_I3S_MESH_INCOMPLETE",
|
|
1942
2723
|
"Finalization requires exactly one successfully written layer"
|
|
1943
2724
|
);
|
|
@@ -1953,20 +2734,2245 @@ function createSingleMeshI3SSink(options) {
|
|
|
1953
2734
|
}
|
|
1954
2735
|
};
|
|
1955
2736
|
}
|
|
2737
|
+
|
|
2738
|
+
// src/v5/i3s-mesh-sink.ts
|
|
2739
|
+
import { Ellipsoid } from "@math.gl/geospatial";
|
|
2740
|
+
import { GZipDecompressor, GZipCompressor } from "@loaders.gl/compression";
|
|
2741
|
+
import {
|
|
2742
|
+
TileConversionError as TileConversionError17,
|
|
2743
|
+
createBoundedMemoryTileConversionSink as createBoundedMemoryTileConversionSink3
|
|
2744
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
2745
|
+
function createI3SMeshSink(options) {
|
|
2746
|
+
const { maxMeshes, maxResourceBytes } = options;
|
|
2747
|
+
if (!Number.isInteger(maxMeshes) || maxMeshes < 1 || maxMeshes > 64 || !Number.isSafeInteger(maxResourceBytes) || maxResourceBytes < 1)
|
|
2748
|
+
throw new TileConversionError17(
|
|
2749
|
+
"I3S_COLLECTION_OPTIONS_INVALID",
|
|
2750
|
+
"I3S collections require 1\u201364 meshes and a positive resource byte limit"
|
|
2751
|
+
);
|
|
2752
|
+
const memory = createBoundedMemoryTileConversionSink3(options);
|
|
2753
|
+
const compressor = new GZipCompressor({ useNative: false });
|
|
2754
|
+
const decompressor = new GZipDecompressor({ useNative: false });
|
|
2755
|
+
const leaves = [];
|
|
2756
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
2757
|
+
const objectIdRanges = [];
|
|
2758
|
+
let layer;
|
|
2759
|
+
let schemaKey;
|
|
2760
|
+
let state = "open";
|
|
2761
|
+
let completed = false;
|
|
2762
|
+
async function readBytes(bytes) {
|
|
2763
|
+
const output = await decompressor.decompress(bytes);
|
|
2764
|
+
if (output.byteLength > maxResourceBytes)
|
|
2765
|
+
throw new TileConversionError17(
|
|
2766
|
+
"I3S_COLLECTION_RESOURCE_TOO_LARGE",
|
|
2767
|
+
"I3S metadata exceeds maxResourceBytes"
|
|
2768
|
+
);
|
|
2769
|
+
return output;
|
|
2770
|
+
}
|
|
2771
|
+
async function readJson(resource, name) {
|
|
2772
|
+
return JSON.parse(new TextDecoder().decode(await readBytes(resource.files[name])));
|
|
2773
|
+
}
|
|
2774
|
+
async function writeJson(resourceId, value) {
|
|
2775
|
+
const bytes = new TextEncoder().encode(JSON.stringify(value));
|
|
2776
|
+
if (bytes.byteLength > maxResourceBytes)
|
|
2777
|
+
throw new TileConversionError17(
|
|
2778
|
+
"I3S_COLLECTION_RESOURCE_TOO_LARGE",
|
|
2779
|
+
"I3S index exceeds maxResourceBytes"
|
|
2780
|
+
);
|
|
2781
|
+
await memory.write({
|
|
2782
|
+
resourceId,
|
|
2783
|
+
parts: [await compressor.compress(bytes.buffer)],
|
|
2784
|
+
contentType: "application/gzip"
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
return {
|
|
2788
|
+
/** Exposes the whole collection atomically. */
|
|
2789
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
2790
|
+
/** Retains one leaf's binary resources and validates its shared schema and IDs. */
|
|
2791
|
+
async write(resource, signal) {
|
|
2792
|
+
signal?.throwIfAborted();
|
|
2793
|
+
if (state !== "open" || identifiers.has(resource.id) || leaves.length >= maxMeshes)
|
|
2794
|
+
throw new TileConversionError17(
|
|
2795
|
+
"I3S_COLLECTION_UNAVAILABLE",
|
|
2796
|
+
"I3S collection is busy, closed, full, or repeats a placement"
|
|
2797
|
+
);
|
|
2798
|
+
state = "writing";
|
|
2799
|
+
const authored = await readJson(resource, "3dSceneLayer.json.gz");
|
|
2800
|
+
const key = JSON.stringify([
|
|
2801
|
+
authored.fields,
|
|
2802
|
+
authored.attributeStorageInfo,
|
|
2803
|
+
authored.description,
|
|
2804
|
+
authored.store.defaultGeometrySchema
|
|
2805
|
+
]);
|
|
2806
|
+
if (schemaKey !== void 0 && key !== schemaKey)
|
|
2807
|
+
throw new TileConversionError17(
|
|
2808
|
+
"I3S_COLLECTION_SCHEMA_MISMATCH",
|
|
2809
|
+
"Mesh nodes must share feature fields and the legacy geometry layout"
|
|
2810
|
+
);
|
|
2811
|
+
const objectIds = new DataView(
|
|
2812
|
+
await readBytes(resource.files["nodes/1/attributes/f_0/0.bin.gz"])
|
|
2813
|
+
);
|
|
2814
|
+
const count = objectIds.getUint32(0, true);
|
|
2815
|
+
const first = objectIds.getUint32(4, true);
|
|
2816
|
+
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(
|
|
2817
|
+
(value, index2) => value !== first + index2
|
|
2818
|
+
))
|
|
2819
|
+
throw new TileConversionError17(
|
|
2820
|
+
"I3S_COLLECTION_IDS_INVALID",
|
|
2821
|
+
"Generated object IDs must be contiguous and disjoint across nodes"
|
|
2822
|
+
);
|
|
2823
|
+
const index = leaves.length + 1;
|
|
2824
|
+
const node = (await readJson(resource, "nodepages/0.json.gz")).nodes[1];
|
|
2825
|
+
const legacy = await readJson(
|
|
2826
|
+
resource,
|
|
2827
|
+
"nodes/1/3dNodeIndexDocument.json.gz"
|
|
2828
|
+
);
|
|
2829
|
+
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))
|
|
2830
|
+
throw new TileConversionError17(
|
|
2831
|
+
"I3S_COLLECTION_BOUNDS_INVALID",
|
|
2832
|
+
"Expected finite axis-aligned bounds from the I3S mesh writer"
|
|
2833
|
+
);
|
|
2834
|
+
if (!layer) {
|
|
2835
|
+
layer = structuredClone(authored);
|
|
2836
|
+
layer.materialDefinitions = [];
|
|
2837
|
+
layer.geometryDefinitions = [];
|
|
2838
|
+
layer.textureSetDefinitions = [];
|
|
2839
|
+
schemaKey = key;
|
|
2840
|
+
}
|
|
2841
|
+
const material = structuredClone(authored.materialDefinitions[0]);
|
|
2842
|
+
const texture = material.pbrMetallicRoughness?.baseColorTexture;
|
|
2843
|
+
if (texture) {
|
|
2844
|
+
texture.textureSetDefinitionId = layer.textureSetDefinitions.length;
|
|
2845
|
+
layer.textureSetDefinitions.push(authored.textureSetDefinitions[0]);
|
|
2846
|
+
node.mesh.material.resource = index;
|
|
2847
|
+
}
|
|
2848
|
+
node.index = index;
|
|
2849
|
+
node.parentIndex = 0;
|
|
2850
|
+
node.mesh.material.definition = layer.materialDefinitions.length;
|
|
2851
|
+
node.mesh.geometry.definition = layer.geometryDefinitions.length;
|
|
2852
|
+
node.mesh.geometry.resource = index;
|
|
2853
|
+
node.mesh.attribute.resource = index;
|
|
2854
|
+
layer.materialDefinitions.push(material);
|
|
2855
|
+
layer.geometryDefinitions.push(authored.geometryDefinitions[0]);
|
|
2856
|
+
layer.store.resourcePattern = [
|
|
2857
|
+
.../* @__PURE__ */ new Set([...layer.store.resourcePattern, ...authored.store.resourcePattern])
|
|
2858
|
+
];
|
|
2859
|
+
layer.store.extent = layer.store.extent.map(
|
|
2860
|
+
(value, axis) => axis < 2 ? Math.min(value, authored.store.extent[axis]) : Math.max(value, authored.store.extent[axis])
|
|
2861
|
+
);
|
|
2862
|
+
for (const [name, bytes] of Object.entries(resource.files)) {
|
|
2863
|
+
if (name.startsWith("nodes/1/") && !name.endsWith("3dNodeIndexDocument.json.gz"))
|
|
2864
|
+
await memory.write(
|
|
2865
|
+
{
|
|
2866
|
+
resourceId: name.replace("nodes/1/", `nodes/${index}/`),
|
|
2867
|
+
parts: [bytes],
|
|
2868
|
+
contentType: name.endsWith(".gz") ? "application/gzip" : name.endsWith(".png") ? "image/png" : "image/jpeg"
|
|
2869
|
+
},
|
|
2870
|
+
signal
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
leaves.push({ node, legacy: { ...legacy, id: String(index) } });
|
|
2874
|
+
identifiers.add(resource.id);
|
|
2875
|
+
objectIdRanges.push([first, first + count]);
|
|
2876
|
+
state = "open";
|
|
2877
|
+
},
|
|
2878
|
+
/** Rebuilds both indices and layer bounds only after every leaf has completed. */
|
|
2879
|
+
async finalize(report) {
|
|
2880
|
+
if (state !== "open" || !layer || !leaves.length)
|
|
2881
|
+
throw new TileConversionError17(
|
|
2882
|
+
"I3S_COLLECTION_INCOMPLETE",
|
|
2883
|
+
"Finalization requires successfully written mesh nodes"
|
|
2884
|
+
);
|
|
2885
|
+
state = "writing";
|
|
2886
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
2887
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
2888
|
+
for (const { node } of leaves) {
|
|
2889
|
+
const center2 = Ellipsoid.WGS84.cartographicToCartesian(node.obb.center);
|
|
2890
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2891
|
+
minimum[axis] = Math.min(minimum[axis], center2[axis] - node.obb.halfSize[axis]);
|
|
2892
|
+
maximum[axis] = Math.max(maximum[axis], center2[axis] + node.obb.halfSize[axis]);
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2895
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
2896
|
+
const geographicCenter = Array.from(Ellipsoid.WGS84.cartesianToCartographic(center));
|
|
2897
|
+
const reconstructedCenter = Ellipsoid.WGS84.cartographicToCartesian(geographicCenter);
|
|
2898
|
+
const obb = {
|
|
2899
|
+
center: geographicCenter,
|
|
2900
|
+
halfSize: minimum.map(
|
|
2901
|
+
(value, axis) => maximum[axis] / 2 - value / 2 + Math.abs(center[axis] - reconstructedCenter[axis]) + 1e-7
|
|
2902
|
+
),
|
|
2903
|
+
quaternion: [0, 0, 0, 1]
|
|
2904
|
+
};
|
|
2905
|
+
if ([...obb.center, ...obb.halfSize].some((value) => !Number.isFinite(value)))
|
|
2906
|
+
throw new TileConversionError17(
|
|
2907
|
+
"I3S_COLLECTION_BOUNDS_INVALID",
|
|
2908
|
+
"Collection bounds cannot be represented"
|
|
2909
|
+
);
|
|
2910
|
+
const mbs = [...obb.center, Math.hypot(...obb.halfSize)];
|
|
2911
|
+
const children = leaves.map(({ node }) => ({
|
|
2912
|
+
id: String(node.index),
|
|
2913
|
+
href: `../${node.index}`,
|
|
2914
|
+
obb: node.obb,
|
|
2915
|
+
mbs: [...node.obb.center, Math.hypot(...node.obb.halfSize)]
|
|
2916
|
+
}));
|
|
2917
|
+
await writeJson("nodes/root/3dNodeIndexDocument.json.gz", {
|
|
2918
|
+
id: "root",
|
|
2919
|
+
level: 0,
|
|
2920
|
+
version: "1",
|
|
2921
|
+
obb,
|
|
2922
|
+
mbs,
|
|
2923
|
+
lodSelection: [{ metricType: "maxScreenThresholdSQ", maxError: 0 }],
|
|
2924
|
+
children
|
|
2925
|
+
});
|
|
2926
|
+
for (const { node, legacy } of leaves)
|
|
2927
|
+
await writeJson(`nodes/${node.index}/3dNodeIndexDocument.json.gz`, {
|
|
2928
|
+
...legacy,
|
|
2929
|
+
parentNode: { id: "root", href: "../root", obb, mbs }
|
|
2930
|
+
});
|
|
2931
|
+
const nodes = [
|
|
2932
|
+
{ index: 0, obb, lodThreshold: 0, children: leaves.map(({ node }) => node.index) },
|
|
2933
|
+
...leaves.map(({ node }) => node)
|
|
2934
|
+
];
|
|
2935
|
+
for (let page = 0; page * 64 < nodes.length; page++)
|
|
2936
|
+
await writeJson(`nodepages/${page}.json.gz`, {
|
|
2937
|
+
nodes: nodes.slice(page * 64, (page + 1) * 64)
|
|
2938
|
+
});
|
|
2939
|
+
if (!layer.textureSetDefinitions?.length) delete layer.textureSetDefinitions;
|
|
2940
|
+
await writeJson("3dSceneLayer.json.gz", layer);
|
|
2941
|
+
await writeJson("metadata.json.gz", {
|
|
2942
|
+
folderPattern: "BASIC",
|
|
2943
|
+
archiveCompressionType: "STORE",
|
|
2944
|
+
resourceCompressionType: "GZIP",
|
|
2945
|
+
I3SVersion: "1.7",
|
|
2946
|
+
nodeCount: nodes.length
|
|
2947
|
+
});
|
|
2948
|
+
await memory.finalize(report);
|
|
2949
|
+
state = "closed";
|
|
2950
|
+
completed = true;
|
|
2951
|
+
},
|
|
2952
|
+
/** Discards binary resources and index metadata after any failure. */
|
|
2953
|
+
async abort(error) {
|
|
2954
|
+
completed = false;
|
|
2955
|
+
state = "closed";
|
|
2956
|
+
leaves.length = 0;
|
|
2957
|
+
identifiers.clear();
|
|
2958
|
+
objectIdRanges.length = 0;
|
|
2959
|
+
layer = void 0;
|
|
2960
|
+
await memory.abort(error);
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2963
|
+
}
|
|
2964
|
+
|
|
2965
|
+
// src/v5/conversion-api.ts
|
|
2966
|
+
var TileConversionError18 = class extends Error {
|
|
2967
|
+
/** Stable machine-readable failure code. */
|
|
2968
|
+
code;
|
|
2969
|
+
/** Diagnostics associated with this failure. */
|
|
2970
|
+
diagnostics;
|
|
2971
|
+
/** Creates a typed conversion failure. */
|
|
2972
|
+
constructor(code, message, diagnostics = []) {
|
|
2973
|
+
super(message);
|
|
2974
|
+
this.name = "TileConversionError";
|
|
2975
|
+
this.code = code;
|
|
2976
|
+
this.diagnostics = diagnostics;
|
|
2977
|
+
}
|
|
2978
|
+
};
|
|
2979
|
+
|
|
2980
|
+
// src/v5/resource-fetcher.ts
|
|
2981
|
+
function createTileConversionResourceFetcher(options) {
|
|
2982
|
+
let consumed = options.initialInputBytes ?? 0;
|
|
2983
|
+
if (!Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(consumed) || consumed < 0 || consumed > options.maxInputBytes)
|
|
2984
|
+
throw new TileConversionError18(
|
|
2985
|
+
"INVALID_INPUT_MEMORY_LIMIT",
|
|
2986
|
+
"Resource fetch requires a positive aggregate byte budget and valid initial bytes"
|
|
2987
|
+
);
|
|
2988
|
+
const fetcher = options.fetcher ?? fetch;
|
|
2989
|
+
return async (input, requestOptions) => {
|
|
2990
|
+
options.signal?.throwIfAborted();
|
|
2991
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
2992
|
+
if (!["http:", "https:", "blob:", "data:"].includes(url.protocol))
|
|
2993
|
+
throw new TileConversionError18(
|
|
2994
|
+
"EXTERNAL_RESOURCE_UNSUPPORTED",
|
|
2995
|
+
"Conversion transport requires HTTP(S), Blob or embedded data resources"
|
|
2996
|
+
);
|
|
2997
|
+
const requestSignal = requestOptions?.signal ?? (input instanceof Request ? input.signal : void 0);
|
|
2998
|
+
const signal = options.signal && requestSignal ? AbortSignal.any([options.signal, requestSignal]) : options.signal ?? requestSignal;
|
|
2999
|
+
const response = await fetcher(input, { ...requestOptions, signal });
|
|
3000
|
+
if (!response.ok) {
|
|
3001
|
+
await response.body?.cancel();
|
|
3002
|
+
throw new TileConversionError18(
|
|
3003
|
+
"BROWSER_SOURCE_FETCH_FAILED",
|
|
3004
|
+
`Could not fetch conversion resource (${response.status})`
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
const reader = response.body?.getReader();
|
|
3008
|
+
if (!reader)
|
|
3009
|
+
throw new TileConversionError18(
|
|
3010
|
+
"BROWSER_SOURCE_STREAM_UNAVAILABLE",
|
|
3011
|
+
"Conversion resources require readable response streams"
|
|
3012
|
+
);
|
|
3013
|
+
const cancelRead = () => {
|
|
3014
|
+
void reader.cancel(signal?.reason).catch(() => {
|
|
3015
|
+
});
|
|
3016
|
+
};
|
|
3017
|
+
signal?.addEventListener("abort", cancelRead, { once: true });
|
|
3018
|
+
const parts = [];
|
|
3019
|
+
let byteLength = 0;
|
|
3020
|
+
try {
|
|
3021
|
+
while (true) {
|
|
3022
|
+
signal?.throwIfAborted();
|
|
3023
|
+
const { done, value } = await reader.read();
|
|
3024
|
+
signal?.throwIfAborted();
|
|
3025
|
+
if (done) break;
|
|
3026
|
+
if (value.byteLength > options.maxInputBytes - consumed)
|
|
3027
|
+
throw new TileConversionError18(
|
|
3028
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
3029
|
+
"Selected contents and external dependencies exceed the aggregate input byte budget"
|
|
3030
|
+
);
|
|
3031
|
+
consumed += value.byteLength;
|
|
3032
|
+
byteLength += value.byteLength;
|
|
3033
|
+
parts.push(value);
|
|
3034
|
+
}
|
|
3035
|
+
} catch (error) {
|
|
3036
|
+
await reader.cancel(error);
|
|
3037
|
+
throw error;
|
|
3038
|
+
} finally {
|
|
3039
|
+
signal?.removeEventListener("abort", cancelRead);
|
|
3040
|
+
reader.releaseLock();
|
|
3041
|
+
}
|
|
3042
|
+
const bytes = new Uint8Array(byteLength);
|
|
3043
|
+
let offset = 0;
|
|
3044
|
+
for (const part of parts) {
|
|
3045
|
+
bytes.set(part, offset);
|
|
3046
|
+
offset += part.byteLength;
|
|
3047
|
+
}
|
|
3048
|
+
const boundedResponse = new Response(bytes, {
|
|
3049
|
+
status: response.status,
|
|
3050
|
+
statusText: response.statusText,
|
|
3051
|
+
headers: response.headers
|
|
3052
|
+
});
|
|
3053
|
+
Object.defineProperty(boundedResponse, "url", { value: response.url });
|
|
3054
|
+
return boundedResponse;
|
|
3055
|
+
};
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
// src/v5/point-cloud-tileset-sink.ts
|
|
3059
|
+
import {
|
|
3060
|
+
createBoundedMemoryTileConversionSink as createBoundedMemoryTileConversionSink4,
|
|
3061
|
+
TileConversionError as TileConversionError19
|
|
3062
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
3063
|
+
function createPointCloudTilesetSink(options) {
|
|
3064
|
+
const { maxTiles, geometricError } = options;
|
|
3065
|
+
if (!Number.isSafeInteger(maxTiles) || maxTiles < 1 || !Number.isFinite(geometricError) || geometricError < 0) {
|
|
3066
|
+
throw new TileConversionError19(
|
|
3067
|
+
"POINT_CLOUD_TILESET_OPTIONS_INVALID",
|
|
3068
|
+
"Positive maxTiles and finite nonnegative geometricError are required"
|
|
3069
|
+
);
|
|
3070
|
+
}
|
|
3071
|
+
const memory = createBoundedMemoryTileConversionSink4(options);
|
|
3072
|
+
const children = [];
|
|
3073
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
3074
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
3075
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
3076
|
+
let state = "open";
|
|
3077
|
+
let completed = false;
|
|
3078
|
+
return {
|
|
3079
|
+
/** Exposes finalized files without exposing a partial package. */
|
|
3080
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
3081
|
+
/** Captures one validated placement and encoded PNTS resource. */
|
|
3082
|
+
async write(resource, signal) {
|
|
3083
|
+
signal?.throwIfAborted();
|
|
3084
|
+
if (state !== "open")
|
|
3085
|
+
throw new TileConversionError19(
|
|
3086
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3087
|
+
"Point sink is busy or closed"
|
|
3088
|
+
);
|
|
3089
|
+
if (!resource.id || identifiers.has(resource.id) || children.length >= maxTiles) {
|
|
3090
|
+
throw new TileConversionError19(
|
|
3091
|
+
"POINT_CLOUD_TILESET_PLACEMENT_INVALID",
|
|
3092
|
+
"Placements require unique nonempty identifiers within maxTiles"
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
const [lower, upper] = getPointTileBounds(resource, geometricError);
|
|
3096
|
+
const resourceId = `points/${children.length}.pnts`;
|
|
3097
|
+
state = "writing";
|
|
3098
|
+
await memory.write(
|
|
3099
|
+
{ resourceId, parts: [resource.pnts], contentType: "application/octet-stream" },
|
|
3100
|
+
signal
|
|
3101
|
+
);
|
|
3102
|
+
if (state !== "writing")
|
|
3103
|
+
throw new TileConversionError19(
|
|
3104
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3105
|
+
"Point sink was aborted during writing"
|
|
3106
|
+
);
|
|
3107
|
+
children.push({
|
|
3108
|
+
boundingVolume: { box: createPointBoundsBox(lower, upper) },
|
|
3109
|
+
geometricError,
|
|
3110
|
+
content: { uri: resourceId }
|
|
3111
|
+
});
|
|
3112
|
+
identifiers.add(resource.id);
|
|
3113
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
3114
|
+
minimum[axis] = Math.min(minimum[axis], lower[axis]);
|
|
3115
|
+
maximum[axis] = Math.max(maximum[axis], upper[axis]);
|
|
3116
|
+
}
|
|
3117
|
+
state = "open";
|
|
3118
|
+
},
|
|
3119
|
+
/** Serializes the enclosing root after all placements have completed. */
|
|
3120
|
+
async finalize(report) {
|
|
3121
|
+
if (state !== "open" || !children.length)
|
|
3122
|
+
throw new TileConversionError19(
|
|
3123
|
+
"POINT_CLOUD_TILESET_SINK_INCOMPLETE",
|
|
3124
|
+
"Finalization requires successfully written point tiles"
|
|
3125
|
+
);
|
|
3126
|
+
state = "writing";
|
|
3127
|
+
const rootError = Math.max(
|
|
3128
|
+
geometricError,
|
|
3129
|
+
Math.hypot(...minimum.map((value, axis) => maximum[axis] - value))
|
|
3130
|
+
);
|
|
3131
|
+
if (!Number.isFinite(rootError))
|
|
3132
|
+
throw new TileConversionError19(
|
|
3133
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3134
|
+
"Enclosing bounds must have a finite diagonal"
|
|
3135
|
+
);
|
|
3136
|
+
const json = JSON.stringify({
|
|
3137
|
+
asset: { version: "1.0" },
|
|
3138
|
+
geometricError: rootError,
|
|
3139
|
+
root: {
|
|
3140
|
+
boundingVolume: { box: createPointBoundsBox(minimum, maximum) },
|
|
3141
|
+
geometricError: rootError,
|
|
3142
|
+
refine: "ADD",
|
|
3143
|
+
children
|
|
3144
|
+
}
|
|
3145
|
+
});
|
|
3146
|
+
await memory.write({
|
|
3147
|
+
resourceId: "tileset.json",
|
|
3148
|
+
parts: [json],
|
|
3149
|
+
contentType: "application/json"
|
|
3150
|
+
});
|
|
3151
|
+
if (state !== "writing")
|
|
3152
|
+
throw new TileConversionError19(
|
|
3153
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3154
|
+
"Point sink was aborted during finalization"
|
|
3155
|
+
);
|
|
3156
|
+
await memory.finalize(report);
|
|
3157
|
+
if (state !== "writing")
|
|
3158
|
+
throw new TileConversionError19(
|
|
3159
|
+
"POINT_CLOUD_TILESET_SINK_UNAVAILABLE",
|
|
3160
|
+
"Point sink was aborted during finalization"
|
|
3161
|
+
);
|
|
3162
|
+
state = "closed";
|
|
3163
|
+
completed = true;
|
|
3164
|
+
},
|
|
3165
|
+
/** Clears resource and hierarchy storage on failure or cancellation. */
|
|
3166
|
+
async abort(error) {
|
|
3167
|
+
state = "closed";
|
|
3168
|
+
completed = false;
|
|
3169
|
+
children.length = 0;
|
|
3170
|
+
identifiers.clear();
|
|
3171
|
+
await memory.abort(error);
|
|
3172
|
+
}
|
|
3173
|
+
};
|
|
3174
|
+
}
|
|
3175
|
+
function getPointTileBounds(resource, geometricError) {
|
|
3176
|
+
const reference = resource.spatialReference;
|
|
3177
|
+
const identity = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
3178
|
+
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"))) {
|
|
3179
|
+
throw new TileConversionError19(
|
|
3180
|
+
"POINT_CLOUD_TILESET_FRAME_UNSUPPORTED",
|
|
3181
|
+
"Point tiles require declared EPSG:4978 xyz meters, ellipsoidal heights and no additional source placement"
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3184
|
+
if (!Number.isFinite(resource.maximumPositionError) || resource.maximumPositionError < 0 || resource.maximumPositionError > geometricError) {
|
|
3185
|
+
throw new TileConversionError19(
|
|
3186
|
+
"POINT_CLOUD_GEOMETRIC_ERROR_INVALID",
|
|
3187
|
+
"geometricError must include measured position reconstruction error"
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
const [minimum, maximum] = resource.localBoundingBox;
|
|
3191
|
+
const center = resource.rtcCenter;
|
|
3192
|
+
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])) {
|
|
3193
|
+
throw new TileConversionError19(
|
|
3194
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3195
|
+
"RTC center and encoded xyz bounds must be finite and ordered"
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
const lower = minimum.map((value, axis) => value + center[axis]);
|
|
3199
|
+
const upper = maximum.map((value, axis) => value + center[axis]);
|
|
3200
|
+
if ([...lower, ...upper].some((value) => !Number.isFinite(value)))
|
|
3201
|
+
throw new TileConversionError19(
|
|
3202
|
+
"POINT_CLOUD_TILESET_BOUNDS_INVALID",
|
|
3203
|
+
"Reconstructed ECEF bounds must be finite"
|
|
3204
|
+
);
|
|
3205
|
+
return [lower, upper];
|
|
3206
|
+
}
|
|
3207
|
+
function createPointBoundsBox(minimum, maximum) {
|
|
3208
|
+
const center = minimum.map((value, axis) => value / 2 + maximum[axis] / 2);
|
|
3209
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
3210
|
+
return [center[0], center[1], center[2], halfSize[0], 0, 0, 0, halfSize[1], 0, 0, 0, halfSize[2]];
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
// src/v5/i3s-mesh-source.ts
|
|
3214
|
+
import {
|
|
3215
|
+
TileConversionError as TileConversionError22,
|
|
3216
|
+
createTilesetConversionSource as createTilesetConversionSource2
|
|
3217
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
3218
|
+
|
|
3219
|
+
// src/v5/i3s-mesh-source-material.ts
|
|
3220
|
+
import { getBinaryImageMetadata as getBinaryImageMetadata3 } from "@loaders.gl/images";
|
|
3221
|
+
import { TileConversionError as TileConversionError20 } from "@loaders.gl/tile-converter/v5/core";
|
|
3222
|
+
async function mapI3SMeshSourceMaterial(content, header, readExternalResource, contentUri, signal) {
|
|
3223
|
+
const material = Object.fromEntries(
|
|
3224
|
+
Object.entries(content.material || {}).filter(([, value]) => value !== void 0)
|
|
3225
|
+
);
|
|
3226
|
+
const metallicRoughness = material.pbrMetallicRoughness ? Object.fromEntries(
|
|
3227
|
+
Object.entries(material.pbrMetallicRoughness).filter(([, value]) => value !== void 0)
|
|
3228
|
+
) : {};
|
|
3229
|
+
material.pbrMetallicRoughness = metallicRoughness;
|
|
3230
|
+
if (material.cullFace !== void 0 && material.cullFace !== "back") {
|
|
3231
|
+
throw new TileConversionError20(
|
|
3232
|
+
"I3S_MESH_SOURCE_MATERIAL_UNSUPPORTED",
|
|
3233
|
+
"Only back-face culling is supported"
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
delete material.cullFace;
|
|
3237
|
+
let textureInfo = metallicRoughness.baseColorTexture ? { ...metallicRoughness.baseColorTexture } : void 0;
|
|
3238
|
+
const resources = header.textureUrls?.length ? header.textureUrls : header.textureUrl ? [
|
|
3239
|
+
{
|
|
3240
|
+
textureSetDefinitionId: textureInfo?.textureSetDefinitionId ?? 0,
|
|
3241
|
+
textureUrl: header.textureUrl,
|
|
3242
|
+
textureFormat: header.textureFormat || "jpg"
|
|
3243
|
+
}
|
|
3244
|
+
] : [];
|
|
3245
|
+
if (!textureInfo && !header.materialDefinition && resources.length === 1) {
|
|
3246
|
+
textureInfo = { textureSetDefinitionId: resources[0].textureSetDefinitionId };
|
|
3247
|
+
}
|
|
3248
|
+
const encodedTextures = content.textures || {};
|
|
3249
|
+
if (!textureInfo && (resources.length || content.texture || Object.keys(encodedTextures).length)) {
|
|
3250
|
+
throw new TileConversionError20(
|
|
3251
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3252
|
+
"Texture resources require a base-color mapping"
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
if (textureInfo) {
|
|
3256
|
+
const definitionId = textureInfo.textureSetDefinitionId;
|
|
3257
|
+
const wrappingModes = { none: 33071, repeat: 10497, mirror: 33648 };
|
|
3258
|
+
if (typeof definitionId !== "number" || !Number.isSafeInteger(definitionId) || definitionId < 0 || textureInfo.texCoord !== void 0 && textureInfo.texCoord !== 0 || Object.keys(textureInfo).some(
|
|
3259
|
+
(name) => !["textureSetDefinitionId", "texCoord", "wrapS", "wrapT", "texture"].includes(name)
|
|
3260
|
+
) || ["wrapS", "wrapT"].some(
|
|
3261
|
+
(name) => textureInfo[name] !== void 0 && (typeof textureInfo[name] !== "string" || !Object.hasOwn(wrappingModes, textureInfo[name]))
|
|
3262
|
+
) || resources.length > 1 || resources.some((resource) => resource.textureSetDefinitionId !== definitionId) || Object.keys(encodedTextures).some((key) => key !== String(definitionId))) {
|
|
3263
|
+
throw new TileConversionError20(
|
|
3264
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3265
|
+
"Only one base-color texture set on TEXCOORD_0 with qualified wrapping is supported"
|
|
3266
|
+
);
|
|
3267
|
+
}
|
|
3268
|
+
let encoded = encodedTextures[String(definitionId)] || content.texture || textureInfo.texture?.source?.image;
|
|
3269
|
+
if (!encoded) {
|
|
3270
|
+
if (!resources.length || !readExternalResource) {
|
|
3271
|
+
throw new TileConversionError20(
|
|
3272
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3273
|
+
"Encoded texture bytes or an explicit bounded resource reader are required"
|
|
3274
|
+
);
|
|
3275
|
+
}
|
|
3276
|
+
signal?.throwIfAborted();
|
|
3277
|
+
encoded = await readExternalResource(resources[0].textureUrl, contentUri, signal);
|
|
3278
|
+
signal?.throwIfAborted();
|
|
3279
|
+
}
|
|
3280
|
+
const data = encoded instanceof ArrayBuffer ? new Uint8Array(encoded) : encoded;
|
|
3281
|
+
let image;
|
|
3282
|
+
if (data instanceof Uint8Array) {
|
|
3283
|
+
try {
|
|
3284
|
+
image = getBinaryImageMetadata3(new DataView(data.buffer, data.byteOffset, data.byteLength));
|
|
3285
|
+
} catch {
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
const declaredFormat = resources[0]?.textureFormat;
|
|
3289
|
+
if (!image || !["image/png", "image/jpeg"].includes(image.mimeType) || declaredFormat && (declaredFormat === "png" ? image.mimeType !== "image/png" : declaredFormat === "jpg" ? image.mimeType !== "image/jpeg" : true)) {
|
|
3290
|
+
throw new TileConversionError20(
|
|
3291
|
+
"I3S_MESH_SOURCE_TEXTURE_UNSUPPORTED",
|
|
3292
|
+
"I3S base-color bytes must have matching PNG/JPEG headers"
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
const sampler = Object.fromEntries(
|
|
3296
|
+
["wrapS", "wrapT"].filter((name) => textureInfo[name] !== void 0).map((name) => [name, wrappingModes[textureInfo[name]]])
|
|
3297
|
+
);
|
|
3298
|
+
metallicRoughness.baseColorTexture = {
|
|
3299
|
+
texCoord: 0,
|
|
3300
|
+
texture: {
|
|
3301
|
+
source: { mimeType: image.mimeType, bufferView: { data } },
|
|
3302
|
+
...Object.keys(sampler).length ? { sampler } : {}
|
|
3303
|
+
}
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
const mapped = await mapMeshSourceMaterial(
|
|
3307
|
+
material,
|
|
3308
|
+
void 0,
|
|
3309
|
+
contentUri,
|
|
3310
|
+
signal
|
|
3311
|
+
);
|
|
3312
|
+
if (mapped?.baseColorTexture && !content.sourceAttributes?.uv0) {
|
|
3313
|
+
throw new TileConversionError20(
|
|
3314
|
+
"MESH_TEXCOORD_REQUIRED",
|
|
3315
|
+
"Base-color textures require TEXCOORD_0"
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
return mapped;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
// src/v5/i3s-mesh-source-features.ts
|
|
3322
|
+
import { I3SAttributeLoader } from "@loaders.gl/i3s";
|
|
3323
|
+
import { TileConversionError as TileConversionError21 } from "@loaders.gl/tile-converter/v5/core";
|
|
3324
|
+
async function extractI3SMeshFeatures(content, item, metadata, options, readExternalResource, contentUri, signal) {
|
|
3325
|
+
const maximumAttributeBytes = options.maxAttributeBytes;
|
|
3326
|
+
const storage = metadata.tileset.attributeStorageInfo;
|
|
3327
|
+
const attributeUrls = item.tile.header.attributeUrls;
|
|
3328
|
+
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(
|
|
3329
|
+
(field) => field.domain || field.type === "esriFieldTypeDate" || !storage.some((attribute2) => attribute2.name === field.name)
|
|
3330
|
+
)) {
|
|
3331
|
+
throw new TileConversionError21(
|
|
3332
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3333
|
+
"I3S attributes require an explicit scalar schema, object ID column, byte budget and resource reader; domains/dates/raw metadata need a custom mapper"
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
3336
|
+
const attributeLoader = await I3SAttributeLoader.preload();
|
|
3337
|
+
const columns = /* @__PURE__ */ Object.create(null);
|
|
3338
|
+
let inputBytes = 0;
|
|
3339
|
+
let count;
|
|
3340
|
+
for (const [index, attribute2] of storage.entries()) {
|
|
3341
|
+
signal?.throwIfAborted();
|
|
3342
|
+
const values = attribute2.objectIds ?? attribute2.attributeValues;
|
|
3343
|
+
const type = values?.valueType;
|
|
3344
|
+
const expectedOrdering = attribute2.objectIds ? ["objectIds"] : type === "String" ? ["attributeByteCounts", "attributeValues"] : ["attributeValues"];
|
|
3345
|
+
const expectedHeader = type === "String" ? [
|
|
3346
|
+
{ property: "count", valueType: "UInt32" },
|
|
3347
|
+
{ property: "attributeValuesByteCount", valueType: "UInt32" }
|
|
3348
|
+
] : [{ property: "count", valueType: "UInt32" }];
|
|
3349
|
+
if (!type || values?.valuesPerElement !== 1 || attribute2.objectIds && attribute2.attributeValues || attribute2.header?.length !== expectedHeader.length || expectedHeader.some(
|
|
3350
|
+
(value, headerIndex) => attribute2.header[headerIndex]?.property !== value.property || attribute2.header[headerIndex]?.valueType !== value.valueType
|
|
3351
|
+
) || attribute2.ordering && JSON.stringify(
|
|
3352
|
+
attribute2.ordering.map((value) => value === "ObjectIds" ? "objectIds" : value)
|
|
3353
|
+
) !== JSON.stringify(expectedOrdering) || type === "String" && (values.encoding !== "UTF-8" || attribute2.attributeByteCounts?.valueType !== "UInt32" || attribute2.attributeByteCounts.valuesPerElement !== 1) || !attributeUrls[index]) {
|
|
3354
|
+
throw new TileConversionError21(
|
|
3355
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3356
|
+
`Attribute ${attribute2.name} requires a standard scalar/string resource layout`
|
|
3357
|
+
);
|
|
3358
|
+
}
|
|
3359
|
+
const bytes = await readExternalResource(attributeUrls[index], contentUri, signal);
|
|
3360
|
+
signal?.throwIfAborted();
|
|
3361
|
+
inputBytes += bytes instanceof Uint8Array ? bytes.byteLength : Infinity;
|
|
3362
|
+
if (!(bytes instanceof Uint8Array) || inputBytes > maximumAttributeBytes) {
|
|
3363
|
+
throw new TileConversionError21(
|
|
3364
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
3365
|
+
"I3S node attributes exceed maxAttributeBytes"
|
|
3366
|
+
);
|
|
3367
|
+
}
|
|
3368
|
+
let parsed;
|
|
3369
|
+
try {
|
|
3370
|
+
parsed = await attributeLoader.parse(Uint8Array.from(bytes).buffer, {
|
|
3371
|
+
attributeName: attribute2.name,
|
|
3372
|
+
attributeType: type,
|
|
3373
|
+
i3s: { attributeValues: "exact" }
|
|
3374
|
+
});
|
|
3375
|
+
} catch (error) {
|
|
3376
|
+
throw new TileConversionError21(
|
|
3377
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_INVALID",
|
|
3378
|
+
`Invalid attribute ${attribute2.name}: ${error instanceof Error ? error.message : String(error)}`
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
const column = parsed[attribute2.name];
|
|
3382
|
+
if (!column || count !== void 0 && column.length !== count || !column.length) {
|
|
3383
|
+
throw new TileConversionError21(
|
|
3384
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_COUNT_INVALID",
|
|
3385
|
+
"Every attribute must have the same nonzero row count"
|
|
3386
|
+
);
|
|
3387
|
+
}
|
|
3388
|
+
if (Array.from(column).some(
|
|
3389
|
+
(value) => typeof value === "number" && !Number.isFinite(value)
|
|
3390
|
+
)) {
|
|
3391
|
+
throw new TileConversionError21(
|
|
3392
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3393
|
+
"Numeric noData values require an explicit custom mapper"
|
|
3394
|
+
);
|
|
3395
|
+
}
|
|
3396
|
+
count = column.length;
|
|
3397
|
+
columns[attribute2.name] = column;
|
|
3398
|
+
}
|
|
3399
|
+
const identifiers = columns[options.objectIdProperty];
|
|
3400
|
+
const attributeRows = /* @__PURE__ */ new Map();
|
|
3401
|
+
for (let index = 0; index < count; index++) {
|
|
3402
|
+
const identifier = identifiers[index];
|
|
3403
|
+
if (!(typeof identifier === "bigint" || typeof identifier === "number" && Number.isSafeInteger(identifier)) || attributeRows.has(BigInt(identifier))) {
|
|
3404
|
+
throw new TileConversionError21(
|
|
3405
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3406
|
+
"Object IDs must be unique exact integers"
|
|
3407
|
+
);
|
|
3408
|
+
}
|
|
3409
|
+
attributeRows.set(BigInt(identifier), index);
|
|
3410
|
+
}
|
|
3411
|
+
const vertexCount = content.vertexCount;
|
|
3412
|
+
const geometryIdentifiers = content.featureIds;
|
|
3413
|
+
const attribute = content.sourceAttributes?.id;
|
|
3414
|
+
if (geometryIdentifiers.length !== vertexCount || attribute?.size !== 1 || attribute.normalized || attribute.value.length !== vertexCount || Array.from(geometryIdentifiers).some((value, index) => value !== attribute.value[index])) {
|
|
3415
|
+
throw new TileConversionError21(
|
|
3416
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3417
|
+
"Geometry requires one exact unnormalized object ID per vertex"
|
|
3418
|
+
);
|
|
3419
|
+
}
|
|
3420
|
+
const indices = content.indices;
|
|
3421
|
+
const triangleFeatureIndices = new Uint32Array((indices?.length ?? vertexCount) / 3);
|
|
3422
|
+
if (content.drawRanges?.length) {
|
|
3423
|
+
const covered = new Uint8Array(triangleFeatureIndices.length);
|
|
3424
|
+
for (const range of content.drawRanges) {
|
|
3425
|
+
if (!Number.isSafeInteger(range.firstPrimitive) || range.firstPrimitive < 0 || !Number.isSafeInteger(range.primitiveCount) || range.primitiveCount <= 0 || range.firstPrimitive + range.primitiveCount > covered.length) {
|
|
3426
|
+
throw new TileConversionError21(
|
|
3427
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3428
|
+
"Feature draw ranges must cover valid triangles"
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
for (let triangle = range.firstPrimitive; triangle < range.firstPrimitive + range.primitiveCount; triangle++) {
|
|
3432
|
+
const vertex = indices?.[triangle * 3] ?? triangle * 3;
|
|
3433
|
+
if (covered[triangle] || geometryIdentifiers[vertex] !== range.featureId) {
|
|
3434
|
+
throw new TileConversionError21(
|
|
3435
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3436
|
+
"Feature draw ranges must agree with decoded ownership without overlapping"
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
covered[triangle] = 1;
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
if (covered.some((value) => value === 0)) {
|
|
3443
|
+
throw new TileConversionError21(
|
|
3444
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3445
|
+
"Feature draw ranges must cover every triangle"
|
|
3446
|
+
);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
const usedRows = /* @__PURE__ */ new Set();
|
|
3450
|
+
for (let triangle = 0; triangle < triangleFeatureIndices.length; triangle++) {
|
|
3451
|
+
const corners = [0, 1, 2].map(
|
|
3452
|
+
(corner) => indices?.[triangle * 3 + corner] ?? triangle * 3 + corner
|
|
3453
|
+
);
|
|
3454
|
+
const featureIds = corners.map((corner) => geometryIdentifiers[corner]);
|
|
3455
|
+
const row = attributeRows.get(BigInt(featureIds[0]));
|
|
3456
|
+
if (row === void 0 || featureIds.some((identifier) => identifier !== featureIds[0])) {
|
|
3457
|
+
throw new TileConversionError21(
|
|
3458
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3459
|
+
"Each triangle must reference one declared attribute row"
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
triangleFeatureIndices[triangle] = row;
|
|
3463
|
+
usedRows.add(row);
|
|
3464
|
+
}
|
|
3465
|
+
if (usedRows.size !== count) {
|
|
3466
|
+
throw new TileConversionError21(
|
|
3467
|
+
"I3S_MESH_SOURCE_FEATURE_ASSOCIATION_INVALID",
|
|
3468
|
+
"Attribute rows without geometry require a separately qualified mapping"
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
const stableProperty = options.sourceFeatureIdProperty ?? options.objectIdProperty;
|
|
3472
|
+
if (!Object.hasOwn(columns, stableProperty)) {
|
|
3473
|
+
throw new TileConversionError21(
|
|
3474
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3475
|
+
"Stable identifier column is missing"
|
|
3476
|
+
);
|
|
3477
|
+
}
|
|
3478
|
+
const batches = convertFeatureAttributesToArrowBatches(
|
|
3479
|
+
Array.from({ length: count }, (_, index) => ({
|
|
3480
|
+
featureId: columns[stableProperty][index],
|
|
3481
|
+
metadataClass: options.metadataClass,
|
|
3482
|
+
properties: Object.fromEntries(
|
|
3483
|
+
Object.entries(columns).filter(([name]) => name !== options.objectIdProperty && name !== stableProperty).map(([name, values]) => [name, values[index]])
|
|
3484
|
+
)
|
|
3485
|
+
})),
|
|
3486
|
+
options
|
|
3487
|
+
);
|
|
3488
|
+
const stableIds = /* @__PURE__ */ new Set();
|
|
3489
|
+
for (const batch of batches) {
|
|
3490
|
+
const column = batch.data.getChild(options.featureIdField || "feature_id");
|
|
3491
|
+
for (let index = 0; index < column.length; index++) {
|
|
3492
|
+
const identifier = column.get(index);
|
|
3493
|
+
if (stableIds.has(identifier)) {
|
|
3494
|
+
throw new TileConversionError21(
|
|
3495
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3496
|
+
"Stable IDs must be unique"
|
|
3497
|
+
);
|
|
3498
|
+
}
|
|
3499
|
+
stableIds.add(identifier);
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
return {
|
|
3503
|
+
batches,
|
|
3504
|
+
triangleFeatureIndices,
|
|
3505
|
+
featureIdField: options.featureIdField || "feature_id",
|
|
3506
|
+
integer64Encoding: options.integer64Encoding
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
// src/v5/i3s-mesh-source.ts
|
|
3511
|
+
function createI3SMeshTilesetConversionSource(tileset, options = {}) {
|
|
3512
|
+
const source = createTilesetConversionSource2(tileset, options);
|
|
3513
|
+
return {
|
|
3514
|
+
/** Validates the layer profile and explicit source decode policy before reading. */
|
|
3515
|
+
async inspect(signal) {
|
|
3516
|
+
if (options.features && options.getFeatures) {
|
|
3517
|
+
throw new TileConversionError22(
|
|
3518
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3519
|
+
"Choose explicit resource features or a custom getFeatures mapper, not both"
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
const metadata = await source.inspect(signal);
|
|
3523
|
+
if (metadata.type !== "I3S" || !["3DObject", "IntegratedMesh"].includes(metadata.tileset.layerType) || tileset.options.i3s?.geometryMode !== "source") {
|
|
3524
|
+
throw new TileConversionError22(
|
|
3525
|
+
"I3S_MESH_SOURCE_PROFILE_UNSUPPORTED",
|
|
3526
|
+
"I3S mesh extraction requires a 3DObject/IntegratedMesh runtime with geometryMode=source"
|
|
3527
|
+
);
|
|
3528
|
+
}
|
|
3529
|
+
if (!metadata.spatialReference?.sourceCrs) {
|
|
3530
|
+
throw new TileConversionError22(
|
|
3531
|
+
"I3S_MESH_SOURCE_REFERENCE_REQUIRED",
|
|
3532
|
+
"I3S conversion requires an explicit source CRS"
|
|
3533
|
+
);
|
|
3534
|
+
}
|
|
3535
|
+
return metadata;
|
|
3536
|
+
},
|
|
3537
|
+
/** Converts each supported content entry before shared traversal releases its buffers. */
|
|
3538
|
+
async *read(metadata, signal) {
|
|
3539
|
+
for await (const item of source.read(metadata, signal)) {
|
|
3540
|
+
for (const entry of item.contents) {
|
|
3541
|
+
signal?.throwIfAborted();
|
|
3542
|
+
const content = entry.payload;
|
|
3543
|
+
const attributes = content?.sourceAttributes;
|
|
3544
|
+
if (content?.geometryMode !== "source" || content.topology !== "triangle-list" || !attributes || !(attributes.position?.value instanceof Float64Array) || content.spatialReference?.status !== "native" || [
|
|
3545
|
+
"sourceCrs",
|
|
3546
|
+
"verticalCrs",
|
|
3547
|
+
"axisOrder",
|
|
3548
|
+
"units",
|
|
3549
|
+
"verticalUnitScale",
|
|
3550
|
+
"heightReference",
|
|
3551
|
+
"elevationMode",
|
|
3552
|
+
"elevationOffset",
|
|
3553
|
+
"elevationUnitScale",
|
|
3554
|
+
"coordinateEpoch"
|
|
3555
|
+
].some(
|
|
3556
|
+
(field) => JSON.stringify(content.spatialReference?.[field]) !== JSON.stringify(metadata.spatialReference?.[field])
|
|
3557
|
+
)) {
|
|
3558
|
+
throw new TileConversionError22(
|
|
3559
|
+
"I3S_MESH_SOURCE_GEOMETRY_UNSUPPORTED",
|
|
3560
|
+
"I3S extraction requires original source-frame Float64 triangle geometry"
|
|
3561
|
+
);
|
|
3562
|
+
}
|
|
3563
|
+
if (!["earth-centered", "vertex-reference-frame"].includes(
|
|
3564
|
+
content.normalReferenceFrame || ""
|
|
3565
|
+
) || content.normalReferenceFrame !== (metadata.tileset.store?.normalReferenceFrame || "earth-centered")) {
|
|
3566
|
+
throw new TileConversionError22(
|
|
3567
|
+
"I3S_MESH_SOURCE_NORMAL_FRAME_UNSUPPORTED",
|
|
3568
|
+
"Unknown I3S vector basis"
|
|
3569
|
+
);
|
|
3570
|
+
}
|
|
3571
|
+
if (content.meshSegmentation || Object.entries(attributes).some(
|
|
3572
|
+
([name, value]) => value && !["position", "normal", "uv0", "id", "faceRange"].includes(name)
|
|
3573
|
+
)) {
|
|
3574
|
+
throw new TileConversionError22(
|
|
3575
|
+
"I3S_MESH_SOURCE_ATTRIBUTE_UNSUPPORTED",
|
|
3576
|
+
"Only positions, normals, TEXCOORD_0 and explicitly mapped feature associations are supported"
|
|
3577
|
+
);
|
|
3578
|
+
}
|
|
3579
|
+
const hasFeatures = content.featureIds.length > 0 || metadata.tileset.fields?.length || metadata.tileset.attributeStorageInfo?.length;
|
|
3580
|
+
if (Array.from(content.featureIds).some((value) => !Number.isSafeInteger(value))) {
|
|
3581
|
+
throw new TileConversionError22(
|
|
3582
|
+
"I3S_MESH_SOURCE_FEATURE_ID_UNSUPPORTED",
|
|
3583
|
+
"Decoded numeric feature IDs must be exact safe integers"
|
|
3584
|
+
);
|
|
3585
|
+
}
|
|
3586
|
+
if (hasFeatures && !options.getFeatures && !options.features) {
|
|
3587
|
+
throw new TileConversionError22(
|
|
3588
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3589
|
+
"I3S feature data requires explicit resource features or a custom Arrow mapper"
|
|
3590
|
+
);
|
|
3591
|
+
}
|
|
3592
|
+
const mesh = validateMeshGeometry(
|
|
3593
|
+
{
|
|
3594
|
+
mode: 4,
|
|
3595
|
+
topology: "triangle-list",
|
|
3596
|
+
attributes: {
|
|
3597
|
+
POSITION: attributes.position,
|
|
3598
|
+
...attributes.normal ? { NORMAL: attributes.normal } : {},
|
|
3599
|
+
...attributes.uv0 ? { TEXCOORD_0: attributes.uv0 } : {}
|
|
3600
|
+
},
|
|
3601
|
+
...content.indices ? { indices: { value: content.indices, size: 1 } } : {}
|
|
3602
|
+
},
|
|
3603
|
+
true
|
|
3604
|
+
);
|
|
3605
|
+
if (mesh.attributes.POSITION.value.length / 3 !== content.vertexCount) {
|
|
3606
|
+
throw new TileConversionError22(
|
|
3607
|
+
"I3S_MESH_SOURCE_COUNT_INVALID",
|
|
3608
|
+
"I3S vertexCount must match the decoded geometry"
|
|
3609
|
+
);
|
|
3610
|
+
}
|
|
3611
|
+
const features = options.getFeatures ? await options.getFeatures(content, item, metadata, signal) : options.features ? await extractI3SMeshFeatures(
|
|
3612
|
+
content,
|
|
3613
|
+
item,
|
|
3614
|
+
metadata,
|
|
3615
|
+
options.features,
|
|
3616
|
+
options.readExternalResource,
|
|
3617
|
+
entry.uri,
|
|
3618
|
+
signal
|
|
3619
|
+
) : void 0;
|
|
3620
|
+
signal?.throwIfAborted();
|
|
3621
|
+
if (hasFeatures && !features) {
|
|
3622
|
+
throw new TileConversionError22(
|
|
3623
|
+
"I3S_MESH_SOURCE_FEATURE_MAPPING_REQUIRED",
|
|
3624
|
+
"The feature mapper must return all declared feature data"
|
|
3625
|
+
);
|
|
3626
|
+
}
|
|
3627
|
+
const mappedMaterial = await mapI3SMeshSourceMaterial(
|
|
3628
|
+
content,
|
|
3629
|
+
item.tile.header,
|
|
3630
|
+
options.readExternalResource,
|
|
3631
|
+
entry.uri,
|
|
3632
|
+
signal
|
|
3633
|
+
);
|
|
3634
|
+
signal?.throwIfAborted();
|
|
3635
|
+
yield {
|
|
3636
|
+
id: `${item.tile.id}/${entry.index}`,
|
|
3637
|
+
origin: [0, 0, 0],
|
|
3638
|
+
sourceLayerType: metadata.tileset.layerType,
|
|
3639
|
+
mesh,
|
|
3640
|
+
normalReferenceFrame: content.normalReferenceFrame === "vertex-reference-frame" ? "vertex-reference-frame" : "earth-centered",
|
|
3641
|
+
features,
|
|
3642
|
+
material: mappedMaterial
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
3645
|
+
}
|
|
3646
|
+
}
|
|
3647
|
+
};
|
|
3648
|
+
}
|
|
3649
|
+
|
|
3650
|
+
// src/v5/point-cloud-tileset.ts
|
|
3651
|
+
import { PointCloudTileSource } from "@loaders.gl/tiles/point-cloud-tile-source";
|
|
3652
|
+
import { TileConversionError as TileConversionError23 } from "@loaders.gl/tile-converter/v5/core";
|
|
3653
|
+
async function convertPointCloudToTileset(input, options) {
|
|
3654
|
+
const reference = options.spatialReference;
|
|
3655
|
+
if (!reference || reference.status !== "native" || reference.sourceCrs !== "EPSG:4978" || reference.targetCrs && reference.targetCrs !== "EPSG:4978" || reference.coordinateFrame !== "geocentric" || reference.axisOrder !== "xyz" || reference.heightReference !== "ellipsoidal" || reference.verticalUnitScale !== 1 || reference.units?.some((unit) => unit !== "meter")) {
|
|
3656
|
+
throw new TileConversionError23(
|
|
3657
|
+
"POINT_CLOUD_TILESET_FRAME_UNSUPPORTED",
|
|
3658
|
+
"Point authoring requires explicit native EPSG:4978 xyz meters and ellipsoidal heights"
|
|
3659
|
+
);
|
|
3660
|
+
}
|
|
3661
|
+
if (options.archive && (!Number.isSafeInteger(options.archive.maxArchiveBytes) || options.archive.maxArchiveBytes < 0)) {
|
|
3662
|
+
throw new TileConversionError23(
|
|
3663
|
+
"INVALID_ARCHIVE_BYTE_LIMIT",
|
|
3664
|
+
"Archive byte limit must be a non-negative safe integer"
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
options.signal?.throwIfAborted();
|
|
3668
|
+
const sink = createPointCloudTilesetSink(options);
|
|
3669
|
+
let source;
|
|
3670
|
+
try {
|
|
3671
|
+
source = new PointCloudTileSource(input, {
|
|
3672
|
+
pointCloudTiler: { ...options.tiling, signal: options.signal, spatialReference: reference }
|
|
3673
|
+
});
|
|
3674
|
+
const { pointCount } = await source.getMetadata();
|
|
3675
|
+
const report = await convertPointCloudSource(source, {
|
|
3676
|
+
sink,
|
|
3677
|
+
signal: options.signal,
|
|
3678
|
+
onProgress: options.onProgress,
|
|
3679
|
+
measureInputBytes: (tile) => {
|
|
3680
|
+
const table = tile.content?.data.data;
|
|
3681
|
+
return table ? table.schema.fields.reduce(
|
|
3682
|
+
(bytes, _field, index) => bytes + (table.getChildAt(index)?.byteLength ?? 0),
|
|
3683
|
+
0
|
|
3684
|
+
) : 0;
|
|
3685
|
+
},
|
|
3686
|
+
getTileEncodingOptions: (tile) => ({
|
|
3687
|
+
rtcCenter: [
|
|
3688
|
+
tile.header.boundingVolume.center[0],
|
|
3689
|
+
tile.header.boundingVolume.center[1],
|
|
3690
|
+
tile.header.boundingVolume.center[2]
|
|
3691
|
+
],
|
|
3692
|
+
maxPositionError: options.geometricError
|
|
3693
|
+
})
|
|
3694
|
+
});
|
|
3695
|
+
options.signal?.throwIfAborted();
|
|
3696
|
+
const files = sink.getFiles();
|
|
3697
|
+
const archive = options.archive ? await createTileConversionArchive(files, {
|
|
3698
|
+
format: "3tz",
|
|
3699
|
+
maxArchiveBytes: options.archive.maxArchiveBytes,
|
|
3700
|
+
signal: options.signal
|
|
3701
|
+
}) : void 0;
|
|
3702
|
+
options.signal?.throwIfAborted();
|
|
3703
|
+
return { pointCount, report, files, archive };
|
|
3704
|
+
} catch (error) {
|
|
3705
|
+
await sink.abort(error);
|
|
3706
|
+
throw error;
|
|
3707
|
+
} finally {
|
|
3708
|
+
source?.close();
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
// src/v5/point-cloud-file.ts
|
|
3713
|
+
import { COPCSourceLoader } from "@loaders.gl/copc";
|
|
3714
|
+
import { PotreeSourceLoader, encodePotreeDataset } from "@loaders.gl/potree";
|
|
3715
|
+
import { deduceMeshSchema } from "@loaders.gl/schema-utils";
|
|
3716
|
+
import { encodeZip, openZipArchive } from "@loaders.gl/zip";
|
|
3717
|
+
async function encodePotreeArchive(input, options) {
|
|
3718
|
+
validateLimit(options.maxArchiveBytes);
|
|
3719
|
+
const dataset = await encodePotreeDataset(input, { potree: options.potree });
|
|
3720
|
+
return encodePotreeDatasetArchive(dataset, options.maxArchiveBytes, options.potree?.signal);
|
|
3721
|
+
}
|
|
3722
|
+
async function encodePotreeDatasetArchive(dataset, maxArchiveBytes, signal) {
|
|
3723
|
+
validateLimit(maxArchiveBytes);
|
|
3724
|
+
for (const path of dataset.files.keys()) validatePath(path);
|
|
3725
|
+
signal?.throwIfAborted();
|
|
3726
|
+
const archive = await encodeZip(Object.fromEntries(dataset.files), {
|
|
3727
|
+
compression: "store",
|
|
3728
|
+
zip64: "never",
|
|
3729
|
+
maxEntries: 65535,
|
|
3730
|
+
maxArchiveBytes: BigInt(maxArchiveBytes),
|
|
3731
|
+
signal
|
|
3732
|
+
});
|
|
3733
|
+
signal?.throwIfAborted();
|
|
3734
|
+
return new Blob([archive], { type: "application/zip" });
|
|
3735
|
+
}
|
|
3736
|
+
async function openPointCloudFile(input, options) {
|
|
3737
|
+
validateLimit(options.maxFileBytes);
|
|
3738
|
+
if (input.size > options.maxFileBytes) throw new RangeError("Point file input budget exceeded");
|
|
3739
|
+
options.signal?.throwIfAborted();
|
|
3740
|
+
if (options.format === "copc") {
|
|
3741
|
+
const source2 = COPCSourceLoader.createDataSource(input, options.sourceOptions || {});
|
|
3742
|
+
try {
|
|
3743
|
+
await source2.metadata;
|
|
3744
|
+
options.signal?.throwIfAborted();
|
|
3745
|
+
return { source: source2, close: () => source2.close() };
|
|
3746
|
+
} catch (error) {
|
|
3747
|
+
await source2.close();
|
|
3748
|
+
throw error;
|
|
3749
|
+
}
|
|
3750
|
+
}
|
|
3751
|
+
if (options.format !== "potree-zip") throw new TypeError("Unsupported point file format");
|
|
3752
|
+
const maxEntries = options.maxEntries ?? 1e5;
|
|
3753
|
+
const maxResourceBytes = options.maxResourceBytes ?? 64 * 1024 * 1024;
|
|
3754
|
+
validateLimit(maxEntries);
|
|
3755
|
+
validateLimit(maxResourceBytes);
|
|
3756
|
+
const controller = new AbortController();
|
|
3757
|
+
const signals = [controller.signal, options.signal, options.sourceOptions?.potree?.signal].filter(
|
|
3758
|
+
(value) => Boolean(value)
|
|
3759
|
+
);
|
|
3760
|
+
const signal = AbortSignal.any(signals);
|
|
3761
|
+
let archive;
|
|
3762
|
+
const entries = /* @__PURE__ */ new Map();
|
|
3763
|
+
let source;
|
|
3764
|
+
try {
|
|
3765
|
+
archive = await openZipArchive(input, {
|
|
3766
|
+
maxArchiveBytes: BigInt(options.maxFileBytes),
|
|
3767
|
+
maxEntries,
|
|
3768
|
+
maxReadBytes: Math.max(65557, maxResourceBytes),
|
|
3769
|
+
maxCompressedBytes: BigInt(maxResourceBytes),
|
|
3770
|
+
maxDecodedBytes: BigInt(maxResourceBytes),
|
|
3771
|
+
signal
|
|
3772
|
+
});
|
|
3773
|
+
for await (const entry of archive.entries()) {
|
|
3774
|
+
if (entry.directory) continue;
|
|
3775
|
+
validatePath(entry.path);
|
|
3776
|
+
if (entry.compression !== "store")
|
|
3777
|
+
throw new Error("Potree bundles require unencrypted STORE entries");
|
|
3778
|
+
entries.set(entry.path, entry);
|
|
3779
|
+
}
|
|
3780
|
+
const candidates = ["metadata.json", "cloud.js"].filter((path) => entries.has(path));
|
|
3781
|
+
const metadataPath = options.metadataPath || (candidates.length === 1 ? candidates[0] : "");
|
|
3782
|
+
validatePath(metadataPath);
|
|
3783
|
+
if (!entries.has(metadataPath) || !/(^|\/)(metadata\.json|cloud\.js)$/.test(metadataPath))
|
|
3784
|
+
throw new Error("Potree ZIP requires one metadata entry or an explicit metadataPath");
|
|
3785
|
+
const prefix = "https://point-bundle.loaders.gl.invalid/";
|
|
3786
|
+
const fetchResource = async (resource, request = {}) => {
|
|
3787
|
+
signal.throwIfAborted();
|
|
3788
|
+
request.signal?.throwIfAborted();
|
|
3789
|
+
const url = new URL(resource);
|
|
3790
|
+
if (url.origin !== new URL(prefix).origin || url.search || url.hash)
|
|
3791
|
+
throw new Error("Resource outside the Potree bundle");
|
|
3792
|
+
const path = decodeURIComponent(url.pathname.slice(1));
|
|
3793
|
+
validatePath(path);
|
|
3794
|
+
const entry = entries.get(path);
|
|
3795
|
+
if (!entry) throw new Error(`Missing Potree bundle resource: ${path}`);
|
|
3796
|
+
const range = new Headers(request.headers).get("range");
|
|
3797
|
+
const match = range?.match(/^bytes=(\d+)-(\d+)$/);
|
|
3798
|
+
if (range && !match) throw new RangeError("Unsupported Potree bundle byte range");
|
|
3799
|
+
const begin = match ? Number(match[1]) : 0;
|
|
3800
|
+
const end = match ? Number(match[2]) + 1 : Number(entry.size);
|
|
3801
|
+
if (!Number.isSafeInteger(begin) || !Number.isSafeInteger(end) || begin < 0 || end < begin || BigInt(end) > entry.size || end - begin > maxResourceBytes)
|
|
3802
|
+
throw new RangeError("Potree bundle resource/range budget exceeded");
|
|
3803
|
+
const headers = new Headers({
|
|
3804
|
+
"content-length": String(end - begin),
|
|
3805
|
+
"accept-ranges": "bytes"
|
|
3806
|
+
});
|
|
3807
|
+
if (match) headers.set("content-range", `bytes ${begin}-${end - 1}/${entry.size}`);
|
|
3808
|
+
const bytes = match ? await archive.readRange(entry, {
|
|
3809
|
+
start: BigInt(begin),
|
|
3810
|
+
end: BigInt(end),
|
|
3811
|
+
signal: request.signal ?? void 0
|
|
3812
|
+
}) : await archive.read(entry, { signal: request.signal ?? void 0 });
|
|
3813
|
+
const response = new Response(bytes, {
|
|
3814
|
+
status: match ? 206 : 200,
|
|
3815
|
+
headers
|
|
3816
|
+
});
|
|
3817
|
+
Object.defineProperty(response, "url", { value: url.href });
|
|
3818
|
+
signal.throwIfAborted();
|
|
3819
|
+
return response;
|
|
3820
|
+
};
|
|
3821
|
+
source = PotreeSourceLoader.createDataSource(`${prefix}${metadataPath}`, {
|
|
3822
|
+
...options.sourceOptions,
|
|
3823
|
+
core: { ...options.sourceOptions?.core, fetch: fetchResource },
|
|
3824
|
+
potree: { ...options.sourceOptions?.potree, signal }
|
|
3825
|
+
});
|
|
3826
|
+
await source.initialize();
|
|
3827
|
+
signal.throwIfAborted();
|
|
3828
|
+
return {
|
|
3829
|
+
source,
|
|
3830
|
+
async close() {
|
|
3831
|
+
controller.abort();
|
|
3832
|
+
source?.close();
|
|
3833
|
+
await archive?.close();
|
|
3834
|
+
}
|
|
3835
|
+
};
|
|
3836
|
+
} catch (error) {
|
|
3837
|
+
controller.abort();
|
|
3838
|
+
source?.close();
|
|
3839
|
+
await archive?.close();
|
|
3840
|
+
throw error;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
function extractMeshVertices(mesh, options) {
|
|
3844
|
+
validateLimit(options.maxPoints);
|
|
3845
|
+
const position = mesh.attributes.POSITION;
|
|
3846
|
+
const pointCount = position?.value.length / 3;
|
|
3847
|
+
if (mesh.topology !== "triangle-list" || mesh.mode !== 4 || !position || position.size !== 3 || position.normalized || position.transform || position.byteStride || position.byteOffset || !(position.value instanceof Float32Array || position.value instanceof Float64Array) || !Number.isSafeInteger(pointCount) || pointCount > options.maxPoints || position.value.some((value) => !Number.isFinite(value)))
|
|
3848
|
+
throw new Error(
|
|
3849
|
+
"Mesh vertex harvesting requires packed finite triangle XYZ within the point budget"
|
|
3850
|
+
);
|
|
3851
|
+
const attributes = { POSITION: position };
|
|
3852
|
+
for (const name of options.attributes) {
|
|
3853
|
+
const attribute = mesh.attributes[name];
|
|
3854
|
+
if (!attribute || attribute.transform || attribute.byteStride || attribute.byteOffset || !Number.isSafeInteger(attribute.size) || attribute.size < 1 || attribute.value.length !== pointCount * attribute.size)
|
|
3855
|
+
throw new Error(`Invalid selected mesh vertex attribute: ${name}`);
|
|
3856
|
+
attributes[name] = attribute;
|
|
3857
|
+
}
|
|
3858
|
+
return { topology: "point-list", mode: 0, attributes, schema: deduceMeshSchema(attributes) };
|
|
3859
|
+
}
|
|
3860
|
+
function validatePath(path) {
|
|
3861
|
+
if (!path || !/^[A-Za-z0-9_.\/-]+$/.test(path) || path.split("/").some((part) => !part || part === "." || part === ".."))
|
|
3862
|
+
throw new Error("Potree bundle paths must be canonical relative ASCII paths");
|
|
3863
|
+
}
|
|
3864
|
+
function validateLimit(value) {
|
|
3865
|
+
if (!Number.isSafeInteger(value) || value < 1)
|
|
3866
|
+
throw new RangeError("Point file limits must be positive safe integers");
|
|
3867
|
+
}
|
|
3868
|
+
|
|
3869
|
+
// src/v5/point-cloud-potree.ts
|
|
3870
|
+
import { PointCloudTiler } from "@loaders.gl/schema-utils";
|
|
3871
|
+
import { encodePotreeDataset as encodePotreeDataset2 } from "@loaders.gl/potree";
|
|
3872
|
+
async function convertPointCloudToPotree(input, options) {
|
|
3873
|
+
if (typeof options.projection !== "string" || !options.projection.trim()) {
|
|
3874
|
+
throw new Error("Potree authoring requires an explicit native CRS declaration");
|
|
3875
|
+
}
|
|
3876
|
+
for (const limit of [
|
|
3877
|
+
options.maxGatheredBytes,
|
|
3878
|
+
options.maxOutputBytes,
|
|
3879
|
+
options.archive?.maxArchiveBytes
|
|
3880
|
+
]) {
|
|
3881
|
+
if (limit !== void 0 && (!Number.isSafeInteger(limit) || limit < 1)) {
|
|
3882
|
+
throw new RangeError("Potree authoring byte limits must be positive safe integers");
|
|
3883
|
+
}
|
|
3884
|
+
}
|
|
3885
|
+
if (options.maxGatheredBytes === void 0 || options.maxOutputBytes === void 0) {
|
|
3886
|
+
throw new RangeError("Potree authoring requires gathered and encoded byte limits");
|
|
3887
|
+
}
|
|
3888
|
+
options.signal?.throwIfAborted();
|
|
3889
|
+
const tiler = new PointCloudTiler(input, {
|
|
3890
|
+
...options.tiling,
|
|
3891
|
+
maxNodes: Math.min(options.tiling?.maxNodes ?? 1e5, options.potree?.maxNodes ?? 1e5),
|
|
3892
|
+
maxTileBytes: Math.min(
|
|
3893
|
+
options.tiling?.maxTileBytes ?? 64 * 1024 * 1024,
|
|
3894
|
+
options.maxGatheredBytes
|
|
3895
|
+
),
|
|
3896
|
+
signal: options.signal
|
|
3897
|
+
});
|
|
3898
|
+
const nodes = [];
|
|
3899
|
+
let gatheredBytes = 0;
|
|
3900
|
+
let pointByteLength = 0;
|
|
3901
|
+
let pointCount;
|
|
3902
|
+
try {
|
|
3903
|
+
const pending = [await tiler.getRootNode()];
|
|
3904
|
+
pointCount = tiler.pointCount;
|
|
3905
|
+
while (pending.length) {
|
|
3906
|
+
options.signal?.throwIfAborted();
|
|
3907
|
+
const node = pending.pop();
|
|
3908
|
+
if (node.pointCount * pointByteLength > options.maxGatheredBytes - gatheredBytes) {
|
|
3909
|
+
throw new RangeError("Potree gathered node byte budget exceeded");
|
|
3910
|
+
}
|
|
3911
|
+
const mesh = await tiler.getTileMesh(node.id);
|
|
3912
|
+
const nodeBytes = Array.from(
|
|
3913
|
+
new Set(Object.values(mesh.attributes).map((attribute) => attribute.value.buffer))
|
|
3914
|
+
).reduce((bytes, buffer) => bytes + buffer.byteLength, 0);
|
|
3915
|
+
if (nodeBytes > options.maxGatheredBytes - gatheredBytes) {
|
|
3916
|
+
throw new RangeError("Potree gathered node byte budget exceeded");
|
|
3917
|
+
}
|
|
3918
|
+
if (node.pointCount) pointByteLength = nodeBytes / node.pointCount;
|
|
3919
|
+
gatheredBytes += nodeBytes;
|
|
3920
|
+
nodes.push({ id: node.id, mesh });
|
|
3921
|
+
const children = await tiler.getChildNodes(node.id);
|
|
3922
|
+
for (let index = children.length - 1; index >= 0; index--) pending.push(children[index]);
|
|
3923
|
+
}
|
|
3924
|
+
} finally {
|
|
3925
|
+
tiler.close();
|
|
3926
|
+
}
|
|
3927
|
+
const dataset = await encodePotreeDataset2(nodes, {
|
|
3928
|
+
potree: {
|
|
3929
|
+
...options.potree,
|
|
3930
|
+
projection: options.projection,
|
|
3931
|
+
maxOutputBytes: options.maxOutputBytes,
|
|
3932
|
+
signal: options.signal
|
|
3933
|
+
}
|
|
3934
|
+
});
|
|
3935
|
+
const archive = options.archive ? await encodePotreeDatasetArchive(dataset, options.archive.maxArchiveBytes, options.signal) : void 0;
|
|
3936
|
+
options.signal?.throwIfAborted();
|
|
3937
|
+
return { pointCount, nodeCount: nodes.length, dataset, archive };
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3940
|
+
// src/v5/mesh-hierarchy.ts
|
|
3941
|
+
import { Matrix4 as Matrix43 } from "@math.gl/core";
|
|
3942
|
+
import { get3DTilesSpatialReference } from "@loaders.gl/tiles";
|
|
3943
|
+
import {
|
|
3944
|
+
TileConversionError as TileConversionError24,
|
|
3945
|
+
createBoundedMemoryTileConversionSink as createBoundedMemoryTileConversionSink5,
|
|
3946
|
+
createTiles3DConversionSpatialContext
|
|
3947
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
3948
|
+
function prepareMeshHierarchy(inspection, resourceIds) {
|
|
3949
|
+
const rejectHierarchy = (message) => {
|
|
3950
|
+
throw new TileConversionError24("MESH_HIERARCHY_UNSUPPORTED", message);
|
|
3951
|
+
};
|
|
3952
|
+
if (!inspection.documents?.length || inspection.documents.length > 32)
|
|
3953
|
+
rejectHierarchy(
|
|
3954
|
+
"Hierarchy preservation requires expanded metadata inspection of at most 32 explicit document placements"
|
|
3955
|
+
);
|
|
3956
|
+
if (!resourceIds.length || resourceIds.length > 64 || new Set(resourceIds).size !== resourceIds.length || resourceIds.length !== inspection.resources.length || inspection.resources.some((resource) => !resourceIds.includes(resource.resourceId)))
|
|
3957
|
+
rejectHierarchy(
|
|
3958
|
+
"Hierarchy preservation requires every content placement, without duplicates (at most 64)"
|
|
3959
|
+
);
|
|
3960
|
+
const entryTransform = new Matrix43();
|
|
3961
|
+
if (inspection.documents[0].parentResourceId !== void 0 || !Array.isArray(inspection.documents[0].parentTransform) || inspection.documents[0].parentTransform.length !== 16 || inspection.documents[0].parentTransform.some((value, index) => value !== entryTransform[index]))
|
|
3962
|
+
rejectHierarchy("The entry document must have identity parent placement");
|
|
3963
|
+
const documents = inspection.documents.map((placement) => placement.tileset);
|
|
3964
|
+
const document = documents[0];
|
|
3965
|
+
const externalDocuments = /* @__PURE__ */ new Map();
|
|
3966
|
+
for (let index = 1; index < inspection.documents.length; index++) {
|
|
3967
|
+
const identifier = inspection.documents[index].parentResourceId;
|
|
3968
|
+
if (!identifier || externalDocuments.has(identifier))
|
|
3969
|
+
rejectHierarchy(
|
|
3970
|
+
"Every external document requires a distinct inspected parent content placement"
|
|
3971
|
+
);
|
|
3972
|
+
externalDocuments.set(identifier, index);
|
|
3973
|
+
}
|
|
3974
|
+
for (const document2 of documents) {
|
|
3975
|
+
if (document2.metadata || document2.groups || document2.schema || document2.schemaUri || Object.keys(document2.extensions ?? {}).some((name) => name !== "EXT_geospatial_crs") || (document2.extensionsRequired ?? []).some((name) => name !== "EXT_geospatial_crs"))
|
|
3976
|
+
rejectHierarchy(
|
|
3977
|
+
"Dataset metadata and unmapped extensions require a separate hierarchy profile"
|
|
3978
|
+
);
|
|
3979
|
+
if (!["1.0", "1.1"].includes(document2.asset?.version ?? ""))
|
|
3980
|
+
rejectHierarchy("Hierarchy preservation requires a 3D Tiles 1.0 or 1.1 document");
|
|
3981
|
+
if (document2.geometricError !== void 0 && (typeof document2.geometricError !== "number" || !Number.isFinite(document2.geometricError) || document2.geometricError < 0))
|
|
3982
|
+
rejectHierarchy("Dataset geometricError must be finite nonnegative meters");
|
|
3983
|
+
}
|
|
3984
|
+
const spatialReferences = documents.map(
|
|
3985
|
+
(document2) => get3DTilesSpatialReference(document2)
|
|
3986
|
+
);
|
|
3987
|
+
const spatialReference = createTiles3DConversionSpatialContext(
|
|
3988
|
+
spatialReferences[0]
|
|
3989
|
+
).spatialReference;
|
|
3990
|
+
if (spatialReference.status !== "native" || spatialReference.sourceCrs !== "EPSG:4978" || spatialReference.heightReference !== "ellipsoidal" || spatialReference.axisOrder !== "xyz" || spatialReference.verticalUnitScale !== 1 || spatialReference.units?.some((unit) => unit !== "meter"))
|
|
3991
|
+
rejectHierarchy(
|
|
3992
|
+
"Hierarchy preservation requires native EPSG:4978 xyz meters and ellipsoidal heights"
|
|
3993
|
+
);
|
|
3994
|
+
for (const reference of spatialReferences.slice(1)) {
|
|
3995
|
+
const inherited = reference.crs.state === "absent" && reference.coordinateEpoch === void 0 && !reference.verticalCrs && !reference.warnings.length;
|
|
3996
|
+
const context = createTiles3DConversionSpatialContext(inherited ? spatialReference : reference);
|
|
3997
|
+
if (context.spatialReference.status !== "native" || context.spatialReference.sourceCrs !== spatialReference.sourceCrs || context.spatialReference.heightReference !== spatialReference.heightReference || context.spatialReference.axisOrder !== spatialReference.axisOrder || context.spatialReference.verticalUnitScale !== 1 || context.spatialReference.units?.some((unit) => unit !== "meter") || context.spatialReference.coordinateEpoch !== spatialReference.coordinateEpoch)
|
|
3998
|
+
rejectHierarchy("External document CRS and coordinate epoch must match the entry frame");
|
|
3999
|
+
}
|
|
4000
|
+
const nodes = [];
|
|
4001
|
+
const visitedDocuments = /* @__PURE__ */ new Set([0]);
|
|
4002
|
+
const stack = [
|
|
4003
|
+
{
|
|
4004
|
+
tile: document.root,
|
|
4005
|
+
documentIndex: 0,
|
|
4006
|
+
prefix: "",
|
|
4007
|
+
path: [],
|
|
4008
|
+
parent: -1,
|
|
4009
|
+
transform: new Matrix43(),
|
|
4010
|
+
refine: void 0
|
|
4011
|
+
}
|
|
4012
|
+
];
|
|
4013
|
+
while (stack.length) {
|
|
4014
|
+
const {
|
|
4015
|
+
tile,
|
|
4016
|
+
path,
|
|
4017
|
+
parent,
|
|
4018
|
+
documentIndex,
|
|
4019
|
+
prefix,
|
|
4020
|
+
transform: parentTransform,
|
|
4021
|
+
refine: inheritedRefine
|
|
4022
|
+
} = stack.pop();
|
|
4023
|
+
const document2 = documents[documentIndex];
|
|
4024
|
+
if (nodes.length >= 256) rejectHierarchy("Hierarchy preservation supports at most 256 tiles");
|
|
4025
|
+
if (!tile || typeof tile !== "object" || Array.isArray(tile) || tile.implicitTiling !== void 0 || tile.extensions !== void 0 || tile.metadata !== void 0 || tile.viewerRequestVolume !== void 0 || tile.content !== void 0 && tile.contents !== void 0 || tile.contents !== void 0 && (!Array.isArray(tile.contents) || !tile.contents.length || document2.asset?.version !== "1.1") || tile.children !== void 0 && !Array.isArray(tile.children))
|
|
4026
|
+
rejectHierarchy(
|
|
4027
|
+
"Hierarchy tiles require explicit children, exclusive content/contents declarations and no unmapped metadata, extensions or request volumes"
|
|
4028
|
+
);
|
|
4029
|
+
const refine = tile.refine ?? inheritedRefine;
|
|
4030
|
+
if (refine !== "ADD" && refine !== "REPLACE")
|
|
4031
|
+
rejectHierarchy("Root refinement must be explicit ADD or REPLACE; children may inherit");
|
|
4032
|
+
const matrix = tile.transform;
|
|
4033
|
+
if (matrix !== void 0 && (!Array.isArray(matrix) || matrix.length !== 16 || matrix.some((value) => typeof value !== "number" || !Number.isFinite(value)) || matrix[3] !== 0 || matrix[7] !== 0 || matrix[11] !== 0 || matrix[15] !== 1))
|
|
4034
|
+
rejectHierarchy("Hierarchy transforms must be finite affine matrices");
|
|
4035
|
+
const transform = new Matrix43(parentTransform).multiplyRight(matrix ?? new Matrix43());
|
|
4036
|
+
const columns = [0, 1, 2].map(
|
|
4037
|
+
(column) => [0, 1, 2].reduce((sum, row) => sum + Math.abs(transform[column * 4 + row]), 0)
|
|
4038
|
+
);
|
|
4039
|
+
const rows = [0, 1, 2].map(
|
|
4040
|
+
(row) => [0, 1, 2].reduce((sum, column) => sum + Math.abs(transform[column * 4 + row]), 0)
|
|
4041
|
+
);
|
|
4042
|
+
const scale = document2.asset?.version === "1.0" ? 1 : Math.sqrt(Math.max(...columns)) * Math.sqrt(Math.max(...rows));
|
|
4043
|
+
const geometricError2 = (tile.geometricError ?? NaN) * scale;
|
|
4044
|
+
if (typeof tile.geometricError !== "number" || (tile.geometricError ?? -1) < 0 || !Number.isFinite(geometricError2) || Array.from(transform).some((value) => !Number.isFinite(value)))
|
|
4045
|
+
rejectHierarchy("Every hierarchy tile must declare a finite nonnegative metric error");
|
|
4046
|
+
const contents = tile.contents ?? (tile.content ? [tile.content] : []);
|
|
4047
|
+
const identifiers = contents.map(
|
|
4048
|
+
(_, contentIndex) => `${prefix}tile-${path.length ? path.join("-") : "root"}-content-${contentIndex}`
|
|
4049
|
+
);
|
|
4050
|
+
const externalIndexes = identifiers.map((identifier) => externalDocuments.get(identifier));
|
|
4051
|
+
const externalIndex = externalIndexes.find((index2) => index2 !== void 0);
|
|
4052
|
+
for (const [contentIndex, content] of contents.entries()) {
|
|
4053
|
+
if (!content || typeof content !== "object" || Array.isArray(content) || content.metadata !== void 0 || content.group !== void 0 || content.extensions !== void 0)
|
|
4054
|
+
rejectHierarchy("Content metadata, groups and extensions require a separate mapping");
|
|
4055
|
+
if (externalIndexes[contentIndex] === void 0 && !inspection.resources.some(
|
|
4056
|
+
(resource) => resource.resourceId === identifiers[contentIndex] && resource.documentIndex === documentIndex && resource.uri === (content.uri ?? content.url) && resource.tilePath.length === path.length && resource.tilePath.every((value, index2) => value === path[index2])
|
|
4057
|
+
))
|
|
4058
|
+
rejectHierarchy("Hierarchy content must match the inspected placement");
|
|
4059
|
+
}
|
|
4060
|
+
const resourceId = identifiers[0];
|
|
4061
|
+
if (externalIndex !== void 0) {
|
|
4062
|
+
if (contents.length !== 1 || tile.contents !== void 0 || tile.children?.length || visitedDocuments.has(externalIndex))
|
|
4063
|
+
rejectHierarchy(
|
|
4064
|
+
"External references require a single content, no source children and a unique document placement"
|
|
4065
|
+
);
|
|
4066
|
+
visitedDocuments.add(externalIndex);
|
|
4067
|
+
const placement = inspection.documents[externalIndex];
|
|
4068
|
+
if (!Array.isArray(placement.parentTransform) || placement.parentTransform.length !== 16 || placement.parentTransform.some((value, index2) => value !== transform[index2]))
|
|
4069
|
+
rejectHierarchy("External document placement must match its referencing tile transform");
|
|
4070
|
+
const contentUri = tile.content?.uri ?? tile.content?.url;
|
|
4071
|
+
if (!contentUri || !new URL(contentUri, inspection.documents[documentIndex].url).pathname.toLowerCase().endsWith(".json"))
|
|
4072
|
+
rejectHierarchy("External documents require an inspected .json content reference");
|
|
4073
|
+
}
|
|
4074
|
+
const index = nodes.length;
|
|
4075
|
+
nodes.push({
|
|
4076
|
+
children: [],
|
|
4077
|
+
resourceIds: externalIndex === void 0 ? identifiers : [],
|
|
4078
|
+
geometricError: Math.max(
|
|
4079
|
+
geometricError2,
|
|
4080
|
+
externalIndex === void 0 ? 0 : documents[externalIndex].geometricError ?? 0
|
|
4081
|
+
),
|
|
4082
|
+
refine
|
|
4083
|
+
});
|
|
4084
|
+
if (parent !== -1) nodes[parent].children.push(index);
|
|
4085
|
+
if (externalIndex !== void 0) {
|
|
4086
|
+
stack.push({
|
|
4087
|
+
tile: documents[externalIndex].root,
|
|
4088
|
+
path: [],
|
|
4089
|
+
parent: index,
|
|
4090
|
+
documentIndex: externalIndex,
|
|
4091
|
+
prefix: resourceId + "/",
|
|
4092
|
+
transform,
|
|
4093
|
+
refine: void 0
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
4096
|
+
if (nodes.length + stack.length + (tile.children?.length ?? 0) > 256)
|
|
4097
|
+
rejectHierarchy("Hierarchy preservation supports at most 256 tiles");
|
|
4098
|
+
for (let child = (tile.children?.length ?? 0) - 1; child >= 0; child--)
|
|
4099
|
+
stack.push({
|
|
4100
|
+
tile: tile.children[child],
|
|
4101
|
+
documentIndex,
|
|
4102
|
+
prefix,
|
|
4103
|
+
path: [...path, child],
|
|
4104
|
+
parent: index,
|
|
4105
|
+
transform,
|
|
4106
|
+
refine
|
|
4107
|
+
});
|
|
4108
|
+
}
|
|
4109
|
+
if (visitedDocuments.size !== documents.length || nodes.reduce((count, node) => count + node.resourceIds.length, 0) !== resourceIds.length || nodes.some((node) => !node.resourceIds.length && !node.children.length))
|
|
4110
|
+
rejectHierarchy("Contentless leaves or unmatched content placements cannot be preserved");
|
|
4111
|
+
const geometricError = document.geometricError ?? nodes[0].geometricError;
|
|
4112
|
+
if (typeof geometricError !== "number" || !Number.isFinite(geometricError) || geometricError < 0)
|
|
4113
|
+
rejectHierarchy("Dataset geometricError must be finite nonnegative meters");
|
|
4114
|
+
return {
|
|
4115
|
+
nodes,
|
|
4116
|
+
geometricError: Math.max(geometricError, nodes[0].geometricError),
|
|
4117
|
+
coordinateEpoch: spatialReference.coordinateEpoch
|
|
4118
|
+
};
|
|
4119
|
+
}
|
|
4120
|
+
function createMeshHierarchyTilesetSink(options) {
|
|
4121
|
+
const hierarchy = prepareMeshHierarchy(options.inspection, options.resourceIds);
|
|
4122
|
+
const extensions = structuredClone(
|
|
4123
|
+
options.inspection.tileset.extensions
|
|
4124
|
+
);
|
|
4125
|
+
const extensionsRequired = structuredClone(
|
|
4126
|
+
options.inspection.tileset.extensionsRequired
|
|
4127
|
+
);
|
|
4128
|
+
const { maxPositionError } = options;
|
|
4129
|
+
if (!Number.isFinite(maxPositionError) || maxPositionError < 0 || hierarchy.nodes.some((node) => !Number.isFinite(node.geometricError + maxPositionError)) || !Number.isFinite(hierarchy.geometricError + maxPositionError))
|
|
4130
|
+
throw new TileConversionError24(
|
|
4131
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
4132
|
+
"Hierarchy position allowance and resulting errors must be finite nonnegative meters"
|
|
4133
|
+
);
|
|
4134
|
+
const memory = createBoundedMemoryTileConversionSink5(options);
|
|
4135
|
+
const placements = /* @__PURE__ */ new Map();
|
|
4136
|
+
const completedContents = /* @__PURE__ */ new Set();
|
|
4137
|
+
const primitiveIdentifiers = /* @__PURE__ */ new Set();
|
|
4138
|
+
let state = "open";
|
|
4139
|
+
let completed = false;
|
|
4140
|
+
return {
|
|
4141
|
+
/** Exposes files only after complete finalization. */
|
|
4142
|
+
getFiles: () => completed ? memory.getFiles() : [],
|
|
4143
|
+
/** Retains ordered codec outputs and requires a terminal primitive for each content. */
|
|
4144
|
+
async write(resource, signal) {
|
|
4145
|
+
signal?.throwIfAborted();
|
|
4146
|
+
const identifier = resource.contentResourceId;
|
|
4147
|
+
const { index: primitiveIndex, last: lastPrimitive } = resource.contentPrimitive ?? {
|
|
4148
|
+
index: 0,
|
|
4149
|
+
last: true
|
|
4150
|
+
};
|
|
4151
|
+
if (state !== "open" || !identifier || completedContents.has(identifier) || primitiveIdentifiers.has(resource.id) || primitiveIdentifiers.size >= 64 || !Number.isSafeInteger(primitiveIndex) || primitiveIndex !== (placements.get(identifier)?.length ?? 0) || typeof lastPrimitive !== "boolean" || !hierarchy.nodes.some((node2) => node2.resourceIds.includes(identifier)))
|
|
4152
|
+
throw new TileConversionError24(
|
|
4153
|
+
"MESH_HIERARCHY_PLACEMENT_INVALID",
|
|
4154
|
+
"Hierarchy writes require ordered, distinct primitives for declared contents before close (at most 64)"
|
|
4155
|
+
);
|
|
4156
|
+
if (resource.spatialReference.coordinateEpoch !== hierarchy.coordinateEpoch)
|
|
4157
|
+
throw new TileConversionError24(
|
|
4158
|
+
"MESH_SOURCE_EPOCH_MISMATCH",
|
|
4159
|
+
"Hierarchy output coordinate epoch must match its source"
|
|
4160
|
+
);
|
|
4161
|
+
const tile = createMeshTileset(resource, maxPositionError).root;
|
|
4162
|
+
const nodeIndex = hierarchy.nodes.findIndex((node2) => node2.resourceIds.includes(identifier));
|
|
4163
|
+
const node = hierarchy.nodes[nodeIndex];
|
|
4164
|
+
const sibling = node.resourceIds.map((identifier2) => placements.get(identifier2)?.[0]).find(Boolean);
|
|
4165
|
+
if (sibling && sibling.origin.some((value, axis) => value !== resource.origin[axis]))
|
|
4166
|
+
throw new TileConversionError24(
|
|
4167
|
+
"MESH_HIERARCHY_PLACEMENT_INVALID",
|
|
4168
|
+
"Contents on one tile must share an encoded origin"
|
|
4169
|
+
);
|
|
4170
|
+
const suffix = node.resourceIds.length > 1 ? "-" + node.resourceIds.indexOf(identifier) : "";
|
|
4171
|
+
const primitiveSuffix = primitiveIndex > 0 || !lastPrimitive ? "-primitive-" + primitiveIndex : "";
|
|
4172
|
+
const uri = `meshes/${nodeIndex}${suffix}${primitiveSuffix}.glb`;
|
|
4173
|
+
state = "writing";
|
|
4174
|
+
await memory.write(
|
|
4175
|
+
{ resourceId: uri, parts: [resource.glb], contentType: "model/gltf-binary" },
|
|
4176
|
+
signal
|
|
4177
|
+
);
|
|
4178
|
+
const contentPlacements = placements.get(identifier) ?? [];
|
|
4179
|
+
contentPlacements.push({
|
|
4180
|
+
transform: [...tile.transform],
|
|
4181
|
+
origin: [...resource.origin],
|
|
4182
|
+
localBoundingBox: resource.localBoundingBox.map((bound) => [...bound]),
|
|
4183
|
+
uri
|
|
4184
|
+
});
|
|
4185
|
+
placements.set(identifier, contentPlacements);
|
|
4186
|
+
primitiveIdentifiers.add(resource.id);
|
|
4187
|
+
if (lastPrimitive) completedContents.add(identifier);
|
|
4188
|
+
state = "open";
|
|
4189
|
+
},
|
|
4190
|
+
/** Publishes original topology only after every source content has completed. */
|
|
4191
|
+
async finalize(report) {
|
|
4192
|
+
if (state !== "open" || completedContents.size !== hierarchy.nodes.reduce((count, node) => count + node.resourceIds.length, 0))
|
|
4193
|
+
throw new TileConversionError24(
|
|
4194
|
+
"MESH_HIERARCHY_INCOMPLETE",
|
|
4195
|
+
"Finalization requires every declared hierarchy content"
|
|
4196
|
+
);
|
|
4197
|
+
state = "writing";
|
|
4198
|
+
const tiles = new Array(hierarchy.nodes.length);
|
|
4199
|
+
const bounds = new Array(hierarchy.nodes.length);
|
|
4200
|
+
const transforms = hierarchy.nodes.map((node) => {
|
|
4201
|
+
const placement = placements.get(node.resourceIds[0])?.[0];
|
|
4202
|
+
return placement ? new Matrix43(placement.transform) : new Matrix43();
|
|
4203
|
+
});
|
|
4204
|
+
for (let index = hierarchy.nodes.length - 1; index >= 0; index--) {
|
|
4205
|
+
const node = hierarchy.nodes[index];
|
|
4206
|
+
const placement = placements.get(node.resourceIds[0])?.[0];
|
|
4207
|
+
const minimum = [Infinity, Infinity, Infinity];
|
|
4208
|
+
const maximum = [-Infinity, -Infinity, -Infinity];
|
|
4209
|
+
const contributors = node.children.map((child) => bounds[child]);
|
|
4210
|
+
const nodePlacements = node.resourceIds.flatMap((identifier) => placements.get(identifier));
|
|
4211
|
+
for (const placement2 of nodePlacements) {
|
|
4212
|
+
contributors.push(
|
|
4213
|
+
placement2.localBoundingBox.map(
|
|
4214
|
+
(bound, boundIndex) => bound.map((value, axis) => {
|
|
4215
|
+
const padding = 8 * Number.EPSILON * Math.max(
|
|
4216
|
+
1,
|
|
4217
|
+
Math.abs(placement2.origin[axis]),
|
|
4218
|
+
...placement2.localBoundingBox.map((limit) => Math.abs(limit[axis]))
|
|
4219
|
+
);
|
|
4220
|
+
return value + placement2.origin[axis] + (boundIndex ? padding : -padding);
|
|
4221
|
+
})
|
|
4222
|
+
)
|
|
4223
|
+
);
|
|
4224
|
+
}
|
|
4225
|
+
for (const [lower, upper] of contributors)
|
|
4226
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
4227
|
+
minimum[axis] = Math.min(minimum[axis], lower[axis]);
|
|
4228
|
+
maximum[axis] = Math.max(maximum[axis], upper[axis]);
|
|
4229
|
+
}
|
|
4230
|
+
if ([...minimum, ...maximum].some((value) => !Number.isFinite(value)))
|
|
4231
|
+
throw new TileConversionError24(
|
|
4232
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
4233
|
+
"Hierarchy absolute bounds must remain finite"
|
|
4234
|
+
);
|
|
4235
|
+
bounds[index] = [minimum, maximum];
|
|
4236
|
+
const inverse = new Matrix43(transforms[index]).invert();
|
|
4237
|
+
const center = Array.from(
|
|
4238
|
+
inverse.transformAsPoint(minimum.map((value, axis) => value / 2 + maximum[axis] / 2))
|
|
4239
|
+
);
|
|
4240
|
+
const halfSize = minimum.map((value, axis) => maximum[axis] / 2 - value / 2);
|
|
4241
|
+
const halfAxes = [0, 1, 2].flatMap(
|
|
4242
|
+
(axis) => Array.from(
|
|
4243
|
+
inverse.transformAsVector(
|
|
4244
|
+
halfSize.map((value, coordinate) => coordinate === axis ? value : 0)
|
|
4245
|
+
)
|
|
4246
|
+
)
|
|
4247
|
+
);
|
|
4248
|
+
if ([...center, ...halfAxes].some((value) => !Number.isFinite(value)))
|
|
4249
|
+
throw new TileConversionError24(
|
|
4250
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
4251
|
+
"Hierarchy local bounds must remain finite"
|
|
4252
|
+
);
|
|
4253
|
+
tiles[index] = {
|
|
4254
|
+
transform: Array.from(transforms[index]),
|
|
4255
|
+
boundingVolume: { box: [...center, ...halfAxes] },
|
|
4256
|
+
geometricError: node.geometricError + maxPositionError,
|
|
4257
|
+
refine: node.refine,
|
|
4258
|
+
...nodePlacements.length > 1 ? { contents: nodePlacements.map((placement2) => ({ uri: placement2.uri })) } : placement ? { content: { uri: placement.uri } } : {},
|
|
4259
|
+
...node.children.length ? {
|
|
4260
|
+
children: node.children.map((child) => {
|
|
4261
|
+
tiles[child].transform = Array.from(
|
|
4262
|
+
new Matrix43(inverse).multiplyRight(transforms[child])
|
|
4263
|
+
);
|
|
4264
|
+
if (tiles[child].transform.some((value) => !Number.isFinite(value)))
|
|
4265
|
+
throw new TileConversionError24(
|
|
4266
|
+
"MESH_TILESET_BOUNDS_INVALID",
|
|
4267
|
+
"Relative hierarchy transforms must remain finite"
|
|
4268
|
+
);
|
|
4269
|
+
return tiles[child];
|
|
4270
|
+
})
|
|
4271
|
+
} : {}
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
const json = JSON.stringify({
|
|
4275
|
+
asset: { version: "1.1" },
|
|
4276
|
+
...extensions && Object.keys(extensions).length ? { extensions, extensionsUsed: Object.keys(extensions) } : {},
|
|
4277
|
+
...extensionsRequired?.length ? { extensionsRequired } : {},
|
|
4278
|
+
geometricError: hierarchy.geometricError + maxPositionError,
|
|
4279
|
+
root: tiles[0]
|
|
4280
|
+
});
|
|
4281
|
+
await memory.write({
|
|
4282
|
+
resourceId: "tileset.json",
|
|
4283
|
+
parts: [json],
|
|
4284
|
+
contentType: "application/json"
|
|
4285
|
+
});
|
|
4286
|
+
await memory.finalize(report);
|
|
4287
|
+
state = "closed";
|
|
4288
|
+
completed = true;
|
|
4289
|
+
},
|
|
4290
|
+
/** Discards retained resources and hides all files after failure. */
|
|
4291
|
+
async abort(error) {
|
|
4292
|
+
state = "closed";
|
|
4293
|
+
completed = false;
|
|
4294
|
+
placements.clear();
|
|
4295
|
+
completedContents.clear();
|
|
4296
|
+
primitiveIdentifiers.clear();
|
|
4297
|
+
await memory.abort(error);
|
|
4298
|
+
}
|
|
4299
|
+
};
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
// src/v5/selected-mesh-source.ts
|
|
4303
|
+
import { coreApi, parse } from "@loaders.gl/core";
|
|
4304
|
+
import { Tiles3DLoader } from "@loaders.gl/3d-tiles";
|
|
4305
|
+
import { Tiles3DSource, Tileset3D } from "@loaders.gl/tiles";
|
|
4306
|
+
import {
|
|
4307
|
+
createBrowserTilesetConversionSource,
|
|
4308
|
+
selectBrowserTilesetContent,
|
|
4309
|
+
createTiles3DConversionSpatialContext as createTiles3DConversionSpatialContext2,
|
|
4310
|
+
TileConversionError as TileConversionError25
|
|
4311
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
4312
|
+
import {
|
|
4313
|
+
createMeshTilesetConversionSource as createMeshTilesetConversionSource2,
|
|
4314
|
+
createTileConversionResourceFetcher as createTileConversionResourceFetcher2
|
|
4315
|
+
} from "@loaders.gl/tile-converter/v5/adapters";
|
|
4316
|
+
var SELECTED_MESH_LIMITS = {
|
|
4317
|
+
/** Root JSON plus selected content transport bytes; also the decoded geometry/image/feature byte gate. */
|
|
4318
|
+
maxInputBytes: 16 * 1024 * 1024,
|
|
4319
|
+
/** Maximum declared content placements inspected. */
|
|
4320
|
+
maxInputResources: 1e3,
|
|
4321
|
+
/** Maximum inspected root/external document placements. */
|
|
4322
|
+
maxTilesetDocuments: 32,
|
|
4323
|
+
/** Maximum explicitly selected content placements in one partial archive. */
|
|
4324
|
+
maxSelectedResources: 64,
|
|
4325
|
+
/** Maximum primitive placements authored across all selected contents. */
|
|
4326
|
+
maxMeshResources: 64,
|
|
4327
|
+
/** Maximum retained file bytes and final archive bytes. */
|
|
4328
|
+
maxOutputBytes: 32 * 1024 * 1024,
|
|
4329
|
+
/** Maximum measured reconstruction error in meters. */
|
|
4330
|
+
maxPositionError: 0.01
|
|
4331
|
+
};
|
|
4332
|
+
async function createSelectedMeshConversionSource(inspection, options) {
|
|
4333
|
+
const {
|
|
4334
|
+
resourceIds,
|
|
4335
|
+
signal,
|
|
4336
|
+
dracoLibraryOptions,
|
|
4337
|
+
features,
|
|
4338
|
+
preserveHierarchy,
|
|
4339
|
+
fetcher = fetch
|
|
4340
|
+
} = options;
|
|
4341
|
+
const maxInputBytes = options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes;
|
|
4342
|
+
if (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1 || maxInputBytes > SELECTED_MESH_LIMITS.maxInputBytes)
|
|
4343
|
+
throw new TileConversionError25(
|
|
4344
|
+
"INVALID_SELECTED_MESH_OPTIONS",
|
|
4345
|
+
"Selected mesh input budget must be a positive safe integer at most 16 MiB"
|
|
4346
|
+
);
|
|
4347
|
+
if (!resourceIds.length || resourceIds.length > SELECTED_MESH_LIMITS.maxSelectedResources || new Set(resourceIds).size !== resourceIds.length || resourceIds.some(
|
|
4348
|
+
(identifier) => !inspection.resources.some((resource) => resource.resourceId === identifier)
|
|
4349
|
+
))
|
|
4350
|
+
throw new Error("Select between 1 and 64 distinct content placements.");
|
|
4351
|
+
if (preserveHierarchy !== void 0 && typeof preserveHierarchy !== "boolean")
|
|
4352
|
+
throw new TileConversionError25(
|
|
4353
|
+
"MESH_HIERARCHY_UNSUPPORTED",
|
|
4354
|
+
"preserveHierarchy must be a boolean"
|
|
4355
|
+
);
|
|
4356
|
+
if (preserveHierarchy) prepareMeshHierarchy(inspection, resourceIds);
|
|
4357
|
+
const descriptors = inspection.resources.filter(
|
|
4358
|
+
(resource) => resourceIds.includes(resource.resourceId)
|
|
4359
|
+
);
|
|
4360
|
+
const selections = descriptors.map(
|
|
4361
|
+
(descriptor) => selectBrowserTilesetContent(inspection, descriptor)
|
|
4362
|
+
);
|
|
4363
|
+
const selectedContents = new Map(
|
|
4364
|
+
descriptors.map((descriptor, index) => [descriptor.resourceId, selections[index]])
|
|
4365
|
+
);
|
|
4366
|
+
if (!preserveHierarchy && resourceIds.length > 1 && selections.some((selection) => !selection.isLeaf))
|
|
4367
|
+
throw new Error(
|
|
4368
|
+
"Multi-selection requires leaf contents; source LOD hierarchy is not exported."
|
|
4369
|
+
);
|
|
4370
|
+
signal.throwIfAborted();
|
|
4371
|
+
const boundedFetcher = createTileConversionResourceFetcher2({
|
|
4372
|
+
maxInputBytes,
|
|
4373
|
+
initialInputBytes: inspection.tilesetBytes,
|
|
4374
|
+
fetcher,
|
|
4375
|
+
signal
|
|
4376
|
+
});
|
|
4377
|
+
const qualification = createSelectedRuntime(
|
|
4378
|
+
inspection.tileset,
|
|
4379
|
+
new Uint8Array(),
|
|
4380
|
+
signal,
|
|
4381
|
+
boundedFetcher,
|
|
4382
|
+
inspection.rootUrl,
|
|
4383
|
+
dracoLibraryOptions
|
|
4384
|
+
);
|
|
4385
|
+
let spatialContext;
|
|
4386
|
+
try {
|
|
4387
|
+
await qualification.tilesetInitializationPromise;
|
|
4388
|
+
const metadata = await createMeshTilesetConversionSource2(qualification).inspect(signal);
|
|
4389
|
+
spatialContext = createTiles3DConversionSpatialContext2(metadata.spatialReference);
|
|
4390
|
+
} finally {
|
|
4391
|
+
qualification.destroy();
|
|
4392
|
+
}
|
|
4393
|
+
const rawSource = options.source ?? createBrowserTilesetConversionSource({
|
|
4394
|
+
input: inspection.rootUrl,
|
|
4395
|
+
...SELECTED_MESH_LIMITS,
|
|
4396
|
+
maxInputBytes,
|
|
4397
|
+
fetcher: boundedFetcher
|
|
4398
|
+
});
|
|
4399
|
+
const selectedInspection = { ...inspection, resources: descriptors };
|
|
4400
|
+
const source = {
|
|
4401
|
+
/** Reuses inspected declarations without fetching the root again. */
|
|
4402
|
+
inspect: async (inspectionSignal) => {
|
|
4403
|
+
signal.throwIfAborted();
|
|
4404
|
+
inspectionSignal?.throwIfAborted();
|
|
4405
|
+
return selectedInspection;
|
|
4406
|
+
},
|
|
4407
|
+
/** Decodes and releases each selected placement in declaration order. */
|
|
4408
|
+
async *read(_inspection, readSignal) {
|
|
4409
|
+
const signal2 = readSignal ? AbortSignal.any([options.signal, readSignal]) : options.signal;
|
|
4410
|
+
signal2.throwIfAborted();
|
|
4411
|
+
let decodedBytes = 0;
|
|
4412
|
+
let primitiveCount = 0;
|
|
4413
|
+
const tileOrigins = /* @__PURE__ */ new Map();
|
|
4414
|
+
for await (const raw of rawSource.read(selectedInspection, signal2)) {
|
|
4415
|
+
const selection = selectedContents.get(raw.resourceId);
|
|
4416
|
+
if (!selection)
|
|
4417
|
+
throw new TileConversionError25(
|
|
4418
|
+
"INVALID_BROWSER_SELECTION",
|
|
4419
|
+
"Source yielded an uninspected content placement"
|
|
4420
|
+
);
|
|
4421
|
+
const contentUrl = raw.contentUrl ?? selection.contentUrl;
|
|
4422
|
+
const runtime = createSelectedRuntime(
|
|
4423
|
+
selection.tileset,
|
|
4424
|
+
raw.data,
|
|
4425
|
+
signal2,
|
|
4426
|
+
boundedFetcher,
|
|
4427
|
+
contentUrl,
|
|
4428
|
+
dracoLibraryOptions,
|
|
4429
|
+
spatialContext.spatialReference
|
|
4430
|
+
);
|
|
4431
|
+
try {
|
|
4432
|
+
await runtime.tilesetInitializationPromise;
|
|
4433
|
+
const meshSource = createMeshTilesetConversionSource2(runtime, {
|
|
4434
|
+
unloadContent: true,
|
|
4435
|
+
features,
|
|
4436
|
+
readExternalResource: async (uri, _contentUri, resourceSignal) => {
|
|
4437
|
+
const response = await boundedFetcher(new URL(uri, contentUrl), {
|
|
4438
|
+
signal: resourceSignal
|
|
4439
|
+
});
|
|
4440
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
4441
|
+
}
|
|
4442
|
+
});
|
|
4443
|
+
const metadata = await meshSource.inspect(signal2);
|
|
4444
|
+
const selectedContext = createTiles3DConversionSpatialContext2(metadata.spatialReference);
|
|
4445
|
+
if (selectedContext.spatialReference.coordinateEpoch !== spatialContext.spatialReference.coordinateEpoch)
|
|
4446
|
+
throw new TileConversionError25(
|
|
4447
|
+
"MESH_SOURCE_EPOCH_MISMATCH",
|
|
4448
|
+
"Selected content coordinate epoch must match the root; epoch transformations are unsupported"
|
|
4449
|
+
);
|
|
4450
|
+
let meshCount = 0;
|
|
4451
|
+
for await (const resource of meshSource.read(metadata, signal2)) {
|
|
4452
|
+
meshCount++;
|
|
4453
|
+
if (++primitiveCount > SELECTED_MESH_LIMITS.maxMeshResources)
|
|
4454
|
+
throw new Error("Selected contents exceed the mesh primitive placement limit.");
|
|
4455
|
+
decodedBytes += measureMeshBytes(resource);
|
|
4456
|
+
if (decodedBytes > maxInputBytes)
|
|
4457
|
+
throw new TileConversionError25(
|
|
4458
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
4459
|
+
"Selected decoded geometry and encoded images exceed the aggregate input byte limit."
|
|
4460
|
+
);
|
|
4461
|
+
const tileIdentifier = raw.resourceId.replace(/content-\d+$/, "");
|
|
4462
|
+
if (preserveHierarchy && !tileOrigins.has(tileIdentifier))
|
|
4463
|
+
tileOrigins.set(tileIdentifier, [...resource.origin]);
|
|
4464
|
+
yield {
|
|
4465
|
+
...resource,
|
|
4466
|
+
origin: preserveHierarchy ? tileOrigins.get(tileIdentifier) : resource.origin,
|
|
4467
|
+
id: `${raw.resourceId}/${resource.id}`,
|
|
4468
|
+
contentResourceId: raw.resourceId
|
|
4469
|
+
};
|
|
4470
|
+
}
|
|
4471
|
+
if (!meshCount) throw new Error("Selected content has no mesh primitive.");
|
|
4472
|
+
} finally {
|
|
4473
|
+
runtime.destroy();
|
|
4474
|
+
}
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
};
|
|
4478
|
+
return {
|
|
4479
|
+
source,
|
|
4480
|
+
spatialContext,
|
|
4481
|
+
geometricError: Math.max(...selections.map((selection) => selection.geometricError)) + SELECTED_MESH_LIMITS.maxPositionError
|
|
4482
|
+
};
|
|
4483
|
+
}
|
|
4484
|
+
var SelectedTiles3DSource = class extends Tiles3DSource {
|
|
4485
|
+
/** Parent frame; explicit, unknown or unresolved child declarations are never replaced. */
|
|
4486
|
+
inheritedSpatialReference;
|
|
4487
|
+
/** Creates a selected source with optional external-document frame inheritance. */
|
|
4488
|
+
constructor(input, options, inheritedSpatialReference) {
|
|
4489
|
+
super(input, options);
|
|
4490
|
+
this.inheritedSpatialReference = inheritedSpatialReference;
|
|
4491
|
+
}
|
|
4492
|
+
/** Returns source metadata with the same absent-CRS inheritance used by external tilesets. */
|
|
4493
|
+
getMetadata() {
|
|
4494
|
+
const metadata = super.getMetadata();
|
|
4495
|
+
const reference = metadata.spatialReference;
|
|
4496
|
+
if (this.inheritedSpatialReference && reference?.crs.state === "absent" && reference.coordinateEpoch === void 0 && !reference.verticalCrs && !reference.warnings.length)
|
|
4497
|
+
return { ...metadata, spatialReference: this.inheritedSpatialReference };
|
|
4498
|
+
return metadata;
|
|
4499
|
+
}
|
|
4500
|
+
};
|
|
4501
|
+
function createSelectedRuntime(document, data, signal, boundedFetcher, contentUrl, dracoLibraryOptions, inheritedSpatialReference) {
|
|
4502
|
+
const {
|
|
4503
|
+
modules,
|
|
4504
|
+
decoderProfile,
|
|
4505
|
+
CDN: contentDeliveryNetwork,
|
|
4506
|
+
useLocalLibraries
|
|
4507
|
+
} = dracoLibraryOptions;
|
|
4508
|
+
const loadOptions = {
|
|
4509
|
+
modules,
|
|
4510
|
+
core: {
|
|
4511
|
+
worker: false,
|
|
4512
|
+
fetch: boundedFetcher,
|
|
4513
|
+
CDN: contentDeliveryNetwork,
|
|
4514
|
+
useLocalLibraries
|
|
4515
|
+
},
|
|
4516
|
+
// Reuse the full decoder already bundled for output verification.
|
|
4517
|
+
draco: { decoderProfile },
|
|
4518
|
+
// Preserve authored UVs and the transform for the converter's qualified GLB writer.
|
|
4519
|
+
gltf: {
|
|
4520
|
+
loadImages: false,
|
|
4521
|
+
decompressMeshes: true,
|
|
4522
|
+
excludeExtensions: {
|
|
4523
|
+
KHR_texture_transform: false,
|
|
4524
|
+
// Only Draco input compression is qualified by this conversion profile.
|
|
4525
|
+
EXT_meshopt_compression: false,
|
|
4526
|
+
KHR_meshopt_compression: false
|
|
4527
|
+
}
|
|
4528
|
+
},
|
|
4529
|
+
"3d-tiles": { loadGLTF: true }
|
|
4530
|
+
};
|
|
4531
|
+
return new Tileset3D(
|
|
4532
|
+
new SelectedTiles3DSource(
|
|
4533
|
+
{
|
|
4534
|
+
url: contentUrl,
|
|
4535
|
+
loader: Tiles3DLoader,
|
|
4536
|
+
resolver: {
|
|
4537
|
+
/** Normalizes the selected hierarchy through the format loader, retaining ancestor transforms. */
|
|
4538
|
+
loadRoot: async (_url, loader, options) => parse(new TextEncoder().encode(JSON.stringify(document)), loader, options),
|
|
4539
|
+
/** Decodes only the selected bounded payload; no sibling content can be requested. */
|
|
4540
|
+
loadResource: async (_url, loader, options) => {
|
|
4541
|
+
signal.throwIfAborted();
|
|
4542
|
+
return parse(data, loader, options, {
|
|
4543
|
+
url: contentUrl,
|
|
4544
|
+
baseUrl: contentUrl.startsWith("blob:") ? contentUrl : new URL(".", contentUrl).href,
|
|
4545
|
+
fetch: boundedFetcher,
|
|
4546
|
+
coreApi,
|
|
4547
|
+
_parse: parse
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
4550
|
+
}
|
|
4551
|
+
},
|
|
4552
|
+
loadOptions,
|
|
4553
|
+
inheritedSpatialReference
|
|
4554
|
+
)
|
|
4555
|
+
);
|
|
4556
|
+
}
|
|
4557
|
+
function measureMeshBytes(resource) {
|
|
4558
|
+
const featureBytes = resource.features ? resource.features.triangleFeatureIndices.byteLength + resource.features.batches.reduce(
|
|
4559
|
+
(bytes, batch) => bytes + batch.data.schema.fields.reduce(
|
|
4560
|
+
(columnBytes, field) => columnBytes + (batch.data.getChild(field.name)?.byteLength ?? 0),
|
|
4561
|
+
0
|
|
4562
|
+
),
|
|
4563
|
+
0
|
|
4564
|
+
) : 0;
|
|
4565
|
+
return Object.values(resource.mesh.attributes).reduce(
|
|
4566
|
+
(bytes, attribute) => bytes + attribute.value.byteLength,
|
|
4567
|
+
(resource.mesh.indices?.value.byteLength ?? 0) + (resource.material?.baseColorTexture?.data.byteLength ?? 0) + featureBytes
|
|
4568
|
+
);
|
|
4569
|
+
}
|
|
4570
|
+
|
|
4571
|
+
// src/v5/archive-source.ts
|
|
4572
|
+
import { BlobFile, HttpFile } from "@loaders.gl/loader-utils";
|
|
4573
|
+
import { parseSLPKArchive } from "@loaders.gl/i3s";
|
|
4574
|
+
import { parse3DTilesArchive } from "@loaders.gl/3d-tiles";
|
|
4575
|
+
import { TileConversionError as TileConversionError26 } from "@loaders.gl/tile-converter/v5/core";
|
|
4576
|
+
async function openTileConversionArchive(descriptor, maxInputBytes, signal, fetcher = fetch) {
|
|
4577
|
+
signal.throwIfAborted();
|
|
4578
|
+
if (!["slpk", "3tz"].includes(descriptor.format))
|
|
4579
|
+
throw new TileConversionError26("INVALID_ARCHIVE_SOURCE", "Select SLPK or 3TZ archive input");
|
|
4580
|
+
validateArchiveEntityTag(descriptor.identity?.etag);
|
|
4581
|
+
const transport = createTileConversionResourceFetcher({
|
|
4582
|
+
maxInputBytes,
|
|
4583
|
+
signal,
|
|
4584
|
+
fetcher: async (url, options) => {
|
|
4585
|
+
const response = await fetcher(url, options);
|
|
4586
|
+
if (response.status !== 206) {
|
|
4587
|
+
await response.body?.cancel();
|
|
4588
|
+
throw new Error("Remote conversion archives require HTTP 206 byte-range responses.");
|
|
4589
|
+
}
|
|
4590
|
+
try {
|
|
4591
|
+
validateArchiveResponseValidator(response.headers);
|
|
4592
|
+
} catch (error) {
|
|
4593
|
+
await response.body?.cancel(error);
|
|
4594
|
+
throw error;
|
|
4595
|
+
}
|
|
4596
|
+
return response;
|
|
4597
|
+
}
|
|
4598
|
+
});
|
|
4599
|
+
let file;
|
|
4600
|
+
let closePromise;
|
|
4601
|
+
async function closeFile() {
|
|
4602
|
+
if (file) {
|
|
4603
|
+
closePromise ??= file.close();
|
|
4604
|
+
await closePromise;
|
|
4605
|
+
}
|
|
4606
|
+
}
|
|
4607
|
+
try {
|
|
4608
|
+
if (typeof descriptor.input === "string") {
|
|
4609
|
+
if (!["http:", "https:"].includes(new URL(descriptor.input).protocol))
|
|
4610
|
+
throw new Error("Use an HTTP(S) archive URL.");
|
|
4611
|
+
const remote = new HttpFile(descriptor.input, {
|
|
4612
|
+
fetch: (url, options) => transport(url, options),
|
|
4613
|
+
consistency: "strict",
|
|
4614
|
+
...descriptor.identity
|
|
4615
|
+
});
|
|
4616
|
+
file = remote;
|
|
4617
|
+
await remote.open(signal);
|
|
4618
|
+
} else file = new BlobFile(descriptor.input);
|
|
4619
|
+
const ownedFile = file;
|
|
4620
|
+
file = void 0;
|
|
4621
|
+
return await createTileConversionArchiveTransport(
|
|
4622
|
+
ownedFile,
|
|
4623
|
+
descriptor.format,
|
|
4624
|
+
maxInputBytes,
|
|
4625
|
+
signal,
|
|
4626
|
+
descriptor
|
|
4627
|
+
);
|
|
4628
|
+
} catch (error) {
|
|
4629
|
+
await closeFile();
|
|
4630
|
+
throw error;
|
|
4631
|
+
}
|
|
4632
|
+
}
|
|
4633
|
+
async function createTileConversionArchiveTransport(file, format, maxInputBytes, signal, descriptor) {
|
|
4634
|
+
const rootUrl = `https://conversion.invalid/input.${format}`;
|
|
4635
|
+
let closed = false;
|
|
4636
|
+
let releasePromise;
|
|
4637
|
+
let closePromise;
|
|
4638
|
+
const closeFile = () => {
|
|
4639
|
+
closed = true;
|
|
4640
|
+
closePromise ??= file.close();
|
|
4641
|
+
return closePromise;
|
|
4642
|
+
};
|
|
4643
|
+
const checkOpen = () => {
|
|
4644
|
+
signal.throwIfAborted();
|
|
4645
|
+
if (closed)
|
|
4646
|
+
throw new TileConversionError26("ARCHIVE_SOURCE_CLOSED", "Conversion archive is closed");
|
|
4647
|
+
};
|
|
4648
|
+
try {
|
|
4649
|
+
checkOpen();
|
|
4650
|
+
if (!["slpk", "3tz"].includes(format) || !Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1)
|
|
4651
|
+
throw new TileConversionError26(
|
|
4652
|
+
"INVALID_ARCHIVE_SOURCE",
|
|
4653
|
+
"Archive source requires a supported format and positive safe byte budget"
|
|
4654
|
+
);
|
|
4655
|
+
if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > maxInputBytes)
|
|
4656
|
+
throw new Error("Conversion archive input must be at most 16 MiB.");
|
|
4657
|
+
let readBytes = 0;
|
|
4658
|
+
const boundedFile = {
|
|
4659
|
+
handle: file.handle,
|
|
4660
|
+
size: file.size,
|
|
4661
|
+
bigsize: file.bigsize,
|
|
4662
|
+
url: file.url,
|
|
4663
|
+
...file.stat ? { stat: () => file.stat() } : {},
|
|
4664
|
+
close: closeFile,
|
|
4665
|
+
/** Checks indexed reads before allocation and forwards operation cancellation. */
|
|
4666
|
+
read: async (start, length) => {
|
|
4667
|
+
checkOpen();
|
|
4668
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maxInputBytes - readBytes)
|
|
4669
|
+
throw new TileConversionError26(
|
|
4670
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
4671
|
+
"Archive indexed reads exceed the input budget"
|
|
4672
|
+
);
|
|
4673
|
+
readBytes += length;
|
|
4674
|
+
const bytes = await file.read(start, length, signal);
|
|
4675
|
+
checkOpen();
|
|
4676
|
+
return bytes;
|
|
4677
|
+
}
|
|
4678
|
+
};
|
|
4679
|
+
const archive = format === "slpk" ? await parseSLPKArchive(boundedFile) : await parse3DTilesArchive(boundedFile);
|
|
4680
|
+
const root = new URL(rootUrl);
|
|
4681
|
+
return {
|
|
4682
|
+
rootUrl: format === "3tz" ? `${rootUrl}/tileset.json` : rootUrl,
|
|
4683
|
+
descriptor: descriptor ? {
|
|
4684
|
+
...descriptor,
|
|
4685
|
+
...file instanceof HttpFile ? { identity: file.getIdentitySnapshot() } : {}
|
|
4686
|
+
} : void 0,
|
|
4687
|
+
/** Resolves only resources belonging to this archive, without external network fallback. */
|
|
4688
|
+
fetcher: (async (input, options) => {
|
|
4689
|
+
checkOpen();
|
|
4690
|
+
options?.signal?.throwIfAborted();
|
|
4691
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
4692
|
+
if (url.origin !== root.origin || url.pathname !== root.pathname && !url.pathname.startsWith(`${root.pathname}/`))
|
|
4693
|
+
throw new Error("Conversion dependencies must resolve inside the selected archive.");
|
|
4694
|
+
const path = decodeURIComponent(
|
|
4695
|
+
url.pathname.slice(root.pathname.length).replace(/^\//, "")
|
|
4696
|
+
);
|
|
4697
|
+
if (path.includes("\\") || path.split("/").some((segment) => segment === ".." || segment === "."))
|
|
4698
|
+
throw new Error("Archive dependency path cannot escape its root.");
|
|
4699
|
+
const bytes = format === "slpk" ? await archive.getFile(path, "http") : await archive.getFile(path);
|
|
4700
|
+
checkOpen();
|
|
4701
|
+
options?.signal?.throwIfAborted();
|
|
4702
|
+
return new Response(bytes);
|
|
4703
|
+
}),
|
|
4704
|
+
/** Releases the readable file on success, failure or cancellation. */
|
|
4705
|
+
close: async () => {
|
|
4706
|
+
closed = true;
|
|
4707
|
+
releasePromise ??= archive.close();
|
|
4708
|
+
await releasePromise;
|
|
4709
|
+
}
|
|
4710
|
+
};
|
|
4711
|
+
} catch (error) {
|
|
4712
|
+
await closeFile();
|
|
4713
|
+
throw error;
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
function validateArchiveEntityTag(entityTag) {
|
|
4717
|
+
if (entityTag !== void 0 && entityTag !== null && !/^"[\x21\x23-\x7e\x80-\xff]*"$/.test(entityTag))
|
|
4718
|
+
throw new TileConversionError26(
|
|
4719
|
+
"ARCHIVE_VALIDATOR_UNSUPPORTED",
|
|
4720
|
+
"Remote archives require a strong quoted ETag, or Last-Modified without an ETag; weak/malformed ETags are unsupported"
|
|
4721
|
+
);
|
|
4722
|
+
}
|
|
4723
|
+
function validateArchiveResponseValidator(headers) {
|
|
4724
|
+
const entityTag = headers.get("etag");
|
|
4725
|
+
validateArchiveEntityTag(entityTag);
|
|
4726
|
+
if (entityTag !== null) return;
|
|
4727
|
+
const lastModified = headers.get("last-modified");
|
|
4728
|
+
const responseDate = headers.get("date");
|
|
4729
|
+
const modifiedTime = parseArchiveHttpDate(lastModified);
|
|
4730
|
+
const responseTime = parseArchiveHttpDate(responseDate);
|
|
4731
|
+
if (!Number.isFinite(modifiedTime) || !Number.isFinite(responseTime) || responseTime - modifiedTime < 6e4)
|
|
4732
|
+
throw new TileConversionError26(
|
|
4733
|
+
"ARCHIVE_VALIDATOR_UNSUPPORTED",
|
|
4734
|
+
"Last-Modified archive identity requires a valid Date at least 60 seconds later; otherwise supply a strong ETag"
|
|
4735
|
+
);
|
|
4736
|
+
}
|
|
4737
|
+
function parseArchiveHttpDate(value) {
|
|
4738
|
+
const milliseconds = value === null ? NaN : Date.parse(value);
|
|
4739
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toUTCString() === value ? milliseconds : NaN;
|
|
4740
|
+
}
|
|
4741
|
+
|
|
4742
|
+
// src/v5/i3s-selection-source.ts
|
|
4743
|
+
import { coreApi as coreApi2, parse as parse2 } from "@loaders.gl/core";
|
|
4744
|
+
import { I3SLoader } from "@loaders.gl/i3s";
|
|
4745
|
+
import { I3SSource, Tileset3D as Tileset3D2 } from "@loaders.gl/tiles";
|
|
4746
|
+
import {
|
|
4747
|
+
createI3SConversionSpatialContext,
|
|
4748
|
+
TileConversionError as TileConversionError27
|
|
4749
|
+
} from "@loaders.gl/tile-converter/v5/core";
|
|
4750
|
+
function createRuntime(transport, layer, header) {
|
|
4751
|
+
const libraries = transport.dracoLibraryOptions;
|
|
4752
|
+
const sourceUrl = new URL(transport.rootUrl);
|
|
4753
|
+
sourceUrl.search = "";
|
|
4754
|
+
sourceUrl.hash = "";
|
|
4755
|
+
const options = {
|
|
4756
|
+
modules: libraries.modules,
|
|
4757
|
+
core: {
|
|
4758
|
+
worker: false,
|
|
4759
|
+
fetch: transport.fetcher,
|
|
4760
|
+
CDN: libraries.CDN,
|
|
4761
|
+
useLocalLibraries: libraries.useLocalLibraries
|
|
4762
|
+
},
|
|
4763
|
+
draco: { decoderProfile: libraries.decoderProfile },
|
|
4764
|
+
// The injected transport owns credential scope; runtime query parameters apply to every URL.
|
|
4765
|
+
i3s: { geometryMode: "source", decodeTextures: false, useCompressedTextures: false }
|
|
4766
|
+
};
|
|
4767
|
+
const loadResource = async (url, loader, loadOptions) => {
|
|
4768
|
+
const response = await transport.fetcher(url);
|
|
4769
|
+
const contextUrl = new URL(url);
|
|
4770
|
+
contextUrl.search = "";
|
|
4771
|
+
contextUrl.hash = "";
|
|
4772
|
+
return parse2(await response.arrayBuffer(), loader, loadOptions, {
|
|
4773
|
+
url: contextUrl.href,
|
|
4774
|
+
fetch: transport.fetcher,
|
|
4775
|
+
coreApi: coreApi2,
|
|
4776
|
+
_parse: parse2
|
|
4777
|
+
});
|
|
4778
|
+
};
|
|
4779
|
+
const source = new I3SSource(
|
|
4780
|
+
header && layer ? {
|
|
4781
|
+
...layer,
|
|
4782
|
+
root: { ...header, children: [] },
|
|
4783
|
+
type: "I3S",
|
|
4784
|
+
url: sourceUrl.href,
|
|
4785
|
+
basePath: sourceUrl.href,
|
|
4786
|
+
loader: I3SLoader,
|
|
4787
|
+
coreApi: coreApi2
|
|
4788
|
+
} : {
|
|
4789
|
+
url: sourceUrl.href,
|
|
4790
|
+
loader: I3SLoader,
|
|
4791
|
+
coreApi: coreApi2,
|
|
4792
|
+
resolver: { loadRoot: loadResource, loadResource }
|
|
4793
|
+
},
|
|
4794
|
+
options
|
|
4795
|
+
);
|
|
4796
|
+
source.coreApi = { ...coreApi2, load: loadResource };
|
|
4797
|
+
return new Tileset3D2(source, { i3s: options.i3s });
|
|
4798
|
+
}
|
|
4799
|
+
async function inspectI3SMeshConversionSource(options) {
|
|
4800
|
+
const { signal } = options;
|
|
4801
|
+
validateSelectionOptions(options);
|
|
4802
|
+
signal.throwIfAborted();
|
|
4803
|
+
const transport = options;
|
|
4804
|
+
let runtime;
|
|
4805
|
+
try {
|
|
4806
|
+
const response = await transport.fetcher(transport.rootUrl);
|
|
4807
|
+
const layer = await response.json();
|
|
4808
|
+
if (!["3DObject", "IntegratedMesh"].includes(layer.layerType))
|
|
4809
|
+
throw new Error("Conversion accepts I3S 3DObject or IntegratedMesh layers.");
|
|
4810
|
+
const rootFetcher = transport.fetcher;
|
|
4811
|
+
const cachedFetcher = (url, options2) => {
|
|
4812
|
+
const requestUrl = new URL(url instanceof Request ? url.url : String(url));
|
|
4813
|
+
const root = new URL(transport.rootUrl);
|
|
4814
|
+
return requestUrl.origin === root.origin && requestUrl.pathname === root.pathname ? Promise.resolve(new Response(JSON.stringify(layer))) : rootFetcher(url, options2);
|
|
4815
|
+
};
|
|
4816
|
+
runtime = createRuntime({ ...transport, fetcher: cachedFetcher });
|
|
4817
|
+
await runtime.tilesetInitializationPromise;
|
|
4818
|
+
await createI3SMeshTilesetConversionSource(runtime).inspect(signal);
|
|
4819
|
+
const stack = [runtime.root];
|
|
4820
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
4821
|
+
const resources = [];
|
|
4822
|
+
while (stack.length) {
|
|
4823
|
+
signal.throwIfAborted();
|
|
4824
|
+
const tile = stack.pop();
|
|
4825
|
+
if (!["string", "number"].includes(typeof tile.header.id) || String(tile.header.id).length === 0)
|
|
4826
|
+
throw new Error("I3S nodes require a declared identity.");
|
|
4827
|
+
const identifier = String(tile.header.id);
|
|
4828
|
+
if (identifiers.has(identifier))
|
|
4829
|
+
throw new Error("Repeated I3S node identity is unsupported.");
|
|
4830
|
+
identifiers.add(identifier);
|
|
4831
|
+
if (identifiers.size + stack.length + (tile.header.children?.length ?? 0) > (options.maxInputResources ?? SELECTED_MESH_LIMITS.maxInputResources))
|
|
4832
|
+
throw new Error("I3S inspection exceeds the 1,000 node limit.");
|
|
4833
|
+
await runtime.source.loadTileChildrenForTraversal(tile, signal);
|
|
4834
|
+
if (!tile.children.length && tile.contentUrl)
|
|
4835
|
+
resources.push({
|
|
4836
|
+
resourceId: identifier,
|
|
4837
|
+
uri: tile.contentUrl,
|
|
4838
|
+
header: structuredClone(tile.header)
|
|
4839
|
+
});
|
|
4840
|
+
for (const child of [...tile.children].reverse()) stack.push(child);
|
|
4841
|
+
}
|
|
4842
|
+
signal.throwIfAborted();
|
|
4843
|
+
if (!resources.length) throw new Error("No supported leaf mesh nodes are declared.");
|
|
4844
|
+
return {
|
|
4845
|
+
kind: "i3s",
|
|
4846
|
+
rootUrl: transport.rootUrl,
|
|
4847
|
+
layer,
|
|
4848
|
+
resources,
|
|
4849
|
+
metadataBytes: 0
|
|
4850
|
+
};
|
|
4851
|
+
} finally {
|
|
4852
|
+
runtime?.destroy();
|
|
4853
|
+
}
|
|
4854
|
+
}
|
|
4855
|
+
async function createSelectedI3SMeshConversionSource(inspection, options) {
|
|
4856
|
+
const { resourceIds, signal, features } = options;
|
|
4857
|
+
validateSelectionOptions(options);
|
|
4858
|
+
signal.throwIfAborted();
|
|
4859
|
+
if (inspection.rootUrl !== options.rootUrl)
|
|
4860
|
+
throw new TileConversionError27(
|
|
4861
|
+
"INVALID_I3S_SELECTION",
|
|
4862
|
+
"Selection transport must match the inspected layer"
|
|
4863
|
+
);
|
|
4864
|
+
if (!resourceIds.length || resourceIds.length > SELECTED_MESH_LIMITS.maxSelectedResources || new Set(resourceIds).size !== resourceIds.length || resourceIds.some(
|
|
4865
|
+
(identifier) => !inspection.resources.some((resource) => resource.resourceId === identifier)
|
|
4866
|
+
))
|
|
4867
|
+
throw new Error("Select between 1 and 64 distinct leaf mesh nodes.");
|
|
4868
|
+
const selected = inspection.resources.filter(
|
|
4869
|
+
(resource) => resourceIds.includes(resource.resourceId)
|
|
4870
|
+
);
|
|
4871
|
+
if (selected.some(
|
|
4872
|
+
(resource) => resource.header.children?.length || !resource.header.contentUrl || resource.uri !== resource.header.contentUrl
|
|
4873
|
+
))
|
|
4874
|
+
throw new Error("Only inspected leaf mesh headers can be converted.");
|
|
4875
|
+
const transport = options;
|
|
4876
|
+
let decodedBytes = 0;
|
|
4877
|
+
let meshCount = 0;
|
|
4878
|
+
{
|
|
4879
|
+
const qualification = createRuntime(transport, inspection.layer, selected[0].header);
|
|
4880
|
+
let spatialContext;
|
|
4881
|
+
try {
|
|
4882
|
+
await qualification.tilesetInitializationPromise;
|
|
4883
|
+
const metadata = await createI3SMeshTilesetConversionSource(qualification).inspect(signal);
|
|
4884
|
+
spatialContext = createI3SConversionSpatialContext(metadata.spatialReference, {
|
|
4885
|
+
targetCrs: "EPSG:4978"
|
|
4886
|
+
});
|
|
4887
|
+
} finally {
|
|
4888
|
+
qualification.destroy();
|
|
4889
|
+
}
|
|
4890
|
+
const source = {
|
|
4891
|
+
/** Reuses only immutable inspected declarations. */
|
|
4892
|
+
inspect: async () => inspection,
|
|
4893
|
+
/** Releases each dedicated node runtime before moving on or closing early. */
|
|
4894
|
+
async *read() {
|
|
4895
|
+
for (const descriptor of selected) {
|
|
4896
|
+
const runtime = createRuntime(transport, inspection.layer, descriptor.header);
|
|
4897
|
+
try {
|
|
4898
|
+
await runtime.tilesetInitializationPromise;
|
|
4899
|
+
const meshSource = createI3SMeshTilesetConversionSource(runtime, {
|
|
4900
|
+
unloadContent: true,
|
|
4901
|
+
features,
|
|
4902
|
+
readExternalResource: async (uri) => new Uint8Array(
|
|
4903
|
+
await (await transport.fetcher(runtime.source.getTileUrl(uri))).arrayBuffer()
|
|
4904
|
+
)
|
|
4905
|
+
});
|
|
4906
|
+
const metadata = await meshSource.inspect(signal);
|
|
4907
|
+
for await (const resource of meshSource.read(metadata, signal)) {
|
|
4908
|
+
decodedBytes += measureMeshBytes(resource);
|
|
4909
|
+
if (++meshCount > SELECTED_MESH_LIMITS.maxMeshResources || decodedBytes > (options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes))
|
|
4910
|
+
throw new TileConversionError27(
|
|
4911
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
4912
|
+
"Selected I3S decoded resources exceed the input/mesh budget"
|
|
4913
|
+
);
|
|
4914
|
+
yield { ...resource, id: descriptor.resourceId };
|
|
4915
|
+
}
|
|
4916
|
+
} finally {
|
|
4917
|
+
runtime.destroy();
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
}
|
|
4921
|
+
};
|
|
4922
|
+
return { source, spatialContext };
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
function validateSelectionOptions(options) {
|
|
4926
|
+
for (const [value, maximum] of [
|
|
4927
|
+
[
|
|
4928
|
+
options.maxInputBytes ?? SELECTED_MESH_LIMITS.maxInputBytes,
|
|
4929
|
+
SELECTED_MESH_LIMITS.maxInputBytes
|
|
4930
|
+
],
|
|
4931
|
+
[
|
|
4932
|
+
options.maxInputResources ?? SELECTED_MESH_LIMITS.maxInputResources,
|
|
4933
|
+
SELECTED_MESH_LIMITS.maxInputResources
|
|
4934
|
+
]
|
|
4935
|
+
])
|
|
4936
|
+
if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
|
|
4937
|
+
throw new TileConversionError27(
|
|
4938
|
+
"INVALID_I3S_SELECTION_LIMIT",
|
|
4939
|
+
"I3S selection limits must be positive safe integers within the bounded profile"
|
|
4940
|
+
);
|
|
4941
|
+
}
|
|
1956
4942
|
export {
|
|
4943
|
+
SELECTED_MESH_LIMITS,
|
|
1957
4944
|
convertFeatureAttributesToArrowBatches,
|
|
1958
4945
|
convertPointCloudSource,
|
|
4946
|
+
convertPointCloudToPotree,
|
|
4947
|
+
convertPointCloudToTileset,
|
|
1959
4948
|
createI3SMeshConversionCodec,
|
|
4949
|
+
createI3SMeshSink,
|
|
4950
|
+
createI3SMeshTilesetConversionSource,
|
|
1960
4951
|
createMeshConversionCodec,
|
|
4952
|
+
createMeshHierarchyTilesetSink,
|
|
4953
|
+
createMeshSourceFeatureOptions,
|
|
1961
4954
|
createMeshTilesetConversionSource,
|
|
1962
4955
|
createMeshTilesetSink,
|
|
4956
|
+
createPointCloudTilesetSink,
|
|
4957
|
+
createSelectedI3SMeshConversionSource,
|
|
4958
|
+
createSelectedMeshConversionSource,
|
|
1963
4959
|
createSingleMeshI3SSink,
|
|
1964
4960
|
createSingleMeshTilesetArchive,
|
|
1965
4961
|
createSingleMeshTilesetSink,
|
|
1966
4962
|
createTileConversionArchive,
|
|
4963
|
+
createTileConversionArchiveTransport,
|
|
4964
|
+
createTileConversionResourceFetcher,
|
|
1967
4965
|
encodeMeshTile,
|
|
1968
4966
|
encodePointCloudSource,
|
|
1969
4967
|
encodePointCloudSourceTile,
|
|
1970
4968
|
encodePointCloudTile,
|
|
1971
|
-
|
|
4969
|
+
encodePointCloudTileWithMetadata,
|
|
4970
|
+
encodePotreeArchive,
|
|
4971
|
+
encodeTileConversionArchiveInBatches,
|
|
4972
|
+
extractMeshVertices,
|
|
4973
|
+
inspectI3SMeshConversionSource,
|
|
4974
|
+
measureMeshBytes,
|
|
4975
|
+
openPointCloudFile,
|
|
4976
|
+
openTileConversionArchive,
|
|
4977
|
+
transformPointCloudSourceTile
|
|
1972
4978
|
};
|