@loaders.gl/tile-converter 5.0.0-alpha.7 → 5.0.0-alpha.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +389 -0
  2. package/dist/converter.min.cjs +194 -175
  3. package/dist/i3s-server/bin/i3s-server.min.cjs +84 -84
  4. package/dist/index.cjs +37747 -0
  5. package/dist/index.js +37718 -0
  6. package/dist/v5/browser-sink.d.ts +37 -0
  7. package/dist/v5/browser-source.d.ts +38 -0
  8. package/dist/v5/browser-tileset-source.d.ts +70 -0
  9. package/dist/v5/browser.cjs +10165 -0
  10. package/dist/v5/browser.d.ts +30 -0
  11. package/dist/v5/browser.js +10140 -0
  12. package/dist/v5/conversion-api.d.ts +122 -0
  13. package/dist/v5/conversion-archive.d.ts +18 -0
  14. package/dist/v5/feature-arrow.d.ts +36 -0
  15. package/dist/v5/index.cjs +11446 -0
  16. package/dist/v5/index.d.ts +28 -0
  17. package/dist/v5/index.js +11410 -0
  18. package/dist/v5/mesh-conversion.d.ts +55 -0
  19. package/dist/v5/mesh-tileset-archive.d.ts +16 -0
  20. package/dist/v5/mesh-tileset-sink.d.ts +31 -0
  21. package/dist/v5/mesh.d.ts +73 -0
  22. package/dist/v5/point-cloud-conversion.d.ts +31 -0
  23. package/dist/v5/point-cloud-source-encoder.d.ts +49 -0
  24. package/dist/v5/point-cloud-source.d.ts +25 -0
  25. package/dist/v5/point-cloud.d.ts +24 -0
  26. package/dist/v5/resource-manifest.d.ts +58 -0
  27. package/dist/v5/spatial-conversion.d.ts +59 -0
  28. package/dist/v5/tileset-source.d.ts +21 -0
  29. package/package.json +41 -26
  30. package/src/common/README.md +5 -0
  31. package/src/index.ts +1 -2
  32. package/src/{3d-tiles-converter → v4/3d-tiles-converter}/3d-tiles-converter.ts +13 -2
  33. package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +124 -0
  34. package/src/{deps-installer → v4/deps-installer}/deps-installer.ts +10 -7
  35. package/src/v4/i3s-converter/helpers/encode-string-attribute.ts +23 -0
  36. package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-converter.ts +15 -42
  37. package/src/{i3s-converter → v4/i3s-converter}/helpers/gltf-attributes.ts +5 -4
  38. package/src/{i3s-converter → v4/i3s-converter}/helpers/load-3d-tiles.ts +5 -9
  39. package/src/{i3s-converter → v4/i3s-converter}/i3s-converter.ts +6 -7
  40. package/src/{i3s-server → v4/i3s-server}/README.md +2 -2
  41. package/src/{i3s-server → v4/i3s-server}/routes/slpk-router.ts +1 -0
  42. package/src/v4/index.ts +2 -0
  43. package/src/v5/browser-sink.ts +130 -0
  44. package/src/v5/browser-source.ts +155 -0
  45. package/src/v5/browser-tileset-source.ts +328 -0
  46. package/src/v5/browser.ts +77 -0
  47. package/src/v5/conversion-api.ts +287 -0
  48. package/src/v5/conversion-archive.ts +51 -0
  49. package/src/v5/feature-arrow.ts +427 -0
  50. package/src/v5/index.ts +68 -0
  51. package/src/v5/mesh-conversion.ts +177 -0
  52. package/src/v5/mesh-tileset-archive.ts +48 -0
  53. package/src/v5/mesh-tileset-sink.ts +178 -0
  54. package/src/v5/mesh.ts +503 -0
  55. package/src/v5/point-cloud-conversion.ts +80 -0
  56. package/src/v5/point-cloud-source-encoder.ts +102 -0
  57. package/src/v5/point-cloud-source.ts +84 -0
  58. package/src/v5/point-cloud.ts +271 -0
  59. package/src/v5/resource-manifest.ts +213 -0
  60. package/src/v5/spatial-conversion.ts +305 -0
  61. package/src/v5/tileset-source.ts +54 -0
  62. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/3d-tiles-content-converter.ts +0 -0
  63. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/i3s-obb-to-3d-tiles-obb.ts +0 -0
  64. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/load-i3s.ts +0 -0
  65. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/helpers/texture-atlas.ts +0 -0
  66. /package/src/{3d-tiles-converter → v4/3d-tiles-converter}/json-templates/tileset.ts +0 -0
  67. /package/src/{constants.ts → v4/constants.ts} +0 -0
  68. /package/src/{converter-cli.ts → v4/converter-cli.ts} +0 -0
  69. /package/src/{i3s-converter → v4/i3s-converter}/helpers/attribute-metadata-info.ts +0 -0
  70. /package/src/{i3s-converter → v4/i3s-converter}/helpers/batch-ids-extensions.ts +0 -0
  71. /package/src/{i3s-converter → v4/i3s-converter}/helpers/coordinate-converter.ts +0 -0
  72. /package/src/{i3s-converter → v4/i3s-converter}/helpers/create-scene-server-path.ts +0 -0
  73. /package/src/{i3s-converter → v4/i3s-converter}/helpers/feature-attributes.ts +0 -0
  74. /package/src/{i3s-converter → v4/i3s-converter}/helpers/geometry-attributes.ts +0 -0
  75. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-debug.ts +0 -0
  76. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-index-document.ts +0 -0
  77. /package/src/{i3s-converter → v4/i3s-converter}/helpers/node-pages.ts +0 -0
  78. /package/src/{i3s-converter → v4/i3s-converter}/helpers/preprocess-3d-tiles.ts +0 -0
  79. /package/src/{i3s-converter → v4/i3s-converter}/helpers/progress.ts +0 -0
  80. /package/src/{i3s-converter → v4/i3s-converter}/helpers/tileset-traversal.ts +0 -0
  81. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/geometry-definitions.ts +0 -0
  82. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/layers.ts +0 -0
  83. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/metadata.ts +0 -0
  84. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/node.ts +0 -0
  85. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/scene-server.ts +0 -0
  86. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/shared-resources.ts +0 -0
  87. /package/src/{i3s-converter → v4/i3s-converter}/json-templates/store.ts +0 -0
  88. /package/src/{i3s-converter → v4/i3s-converter}/types.ts +0 -0
  89. /package/src/{i3s-server → v4/i3s-server}/app.ts +0 -0
  90. /package/src/{i3s-server → v4/i3s-server}/bin/www.ts +0 -0
  91. /package/src/{i3s-server → v4/i3s-server}/certs/cert.pem +0 -0
  92. /package/src/{i3s-server → v4/i3s-server}/certs/key.pem +0 -0
  93. /package/src/{i3s-server → v4/i3s-server}/controllers/slpk-controller.ts +0 -0
  94. /package/src/{i3s-server → v4/i3s-server}/utils/create-scene-server.ts +0 -0
  95. /package/src/{i3s-server → v4/i3s-server}/utils/server-utils.ts +0 -0
  96. /package/src/{lib → v4/lib}/json-schemas/conversion-dump-json-schema.ts +0 -0
  97. /package/src/{lib → v4/lib}/utils/cli-utils.ts +0 -0
  98. /package/src/{lib → v4/lib}/utils/compress-util.ts +0 -0
  99. /package/src/{lib → v4/lib}/utils/conversion-dump.ts +0 -0
  100. /package/src/{lib → v4/lib}/utils/file-utils.ts +0 -0
  101. /package/src/{lib → v4/lib}/utils/geometry-utils.ts +0 -0
  102. /package/src/{lib → v4/lib}/utils/lod-conversion-utils.ts +0 -0
  103. /package/src/{lib → v4/lib}/utils/queue.ts +0 -0
  104. /package/src/{lib → v4/lib}/utils/statistic-utils.ts +0 -0
  105. /package/src/{lib → v4/lib}/utils/write-queue.ts +0 -0
  106. /package/src/{pgm-loader.ts → v4/pgm-loader.ts} +0 -0
@@ -0,0 +1,124 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright vis.gl contributors
4
+
5
+ import {parseWKTCRS, type WKTCRSNode} from '@math.gl/crs';
6
+ import {getSpatialCoordinateFrame} from '@loaders.gl/tiles';
7
+
8
+ /** Horizontal CRS declarations consumed by the legacy I3S converter. */
9
+ export type I3SConversionSpatialReference = {
10
+ /** Layer-level horizontal coordinate reference system. */
11
+ spatialReference?: {
12
+ /** Original spatial reference identifier. */
13
+ wkid?: number;
14
+ /** Current spatial reference identifier. */
15
+ latestWkid?: number;
16
+ /** Custom spatial reference definition. */
17
+ wkt?: string;
18
+ };
19
+ /** Fallback CRS declaration on the layer extent. */
20
+ fullExtent?: {
21
+ /** Coordinate system shared by the extent and layer. */
22
+ spatialReference?: I3SConversionSpatialReference['spatialReference'];
23
+ };
24
+ /** Coordinate systems declared for node bounds and geometry. */
25
+ store?: {
26
+ /** Coordinate system used by node bounds. */
27
+ indexCRS?: string;
28
+ /** Coordinate system used by geometry positions. */
29
+ vertexCRS?: string;
30
+ };
31
+ };
32
+
33
+ /**
34
+ * Reject explicit horizontal CRS declarations that the legacy converter cannot transform.
35
+ * Undeclared CRS metadata retains the historical WGS-84 assumption.
36
+ * @param layer - Source layer metadata, before interpreting any coordinates as longitude/latitude.
37
+ */
38
+ export function validateI3SConversionSpatialReference(layer: I3SConversionSpatialReference): void {
39
+ const spatialReferences = [layer.spatialReference, layer.fullExtent?.spatialReference];
40
+ const unsupportedSpatialReference = spatialReferences.find(spatialReference => {
41
+ if (!spatialReference) {
42
+ return false;
43
+ }
44
+ const identifier = spatialReference.latestWkid ?? spatialReference.wkid;
45
+ return (
46
+ (identifier !== undefined && identifier !== 4326 && identifier !== 4979) ||
47
+ (spatialReference.wkt !== undefined && !isWgs84GeographicWkt(spatialReference.wkt))
48
+ );
49
+ });
50
+ const declarations = [layer.store?.indexCRS, layer.store?.vertexCRS].filter(
51
+ (declaration): declaration is string => Boolean(declaration)
52
+ );
53
+ const unsupportedDeclaration = declarations.find(
54
+ declaration =>
55
+ !/^(?:EPSG:(?:4326|4979)|urn:ogc:def:crs:EPSG::(?:4326|4979)|https?:\/\/www\.opengis\.net\/def\/crs\/EPSG\/0\/(?:4326|4979)\/?)$/i.test(
56
+ declaration
57
+ )
58
+ );
59
+ if (unsupportedSpatialReference || unsupportedDeclaration) {
60
+ const identifier = unsupportedSpatialReference?.latestWkid ?? unsupportedSpatialReference?.wkid;
61
+ const description =
62
+ unsupportedDeclaration || (identifier !== undefined ? `EPSG:${identifier}` : 'custom WKT');
63
+ throw new Error(
64
+ `The legacy I3S-to-3D Tiles converter requires WGS-84 (EPSG:4326) input; received ${description}. Reproject the source I3S dataset to EPSG:4326 before conversion.`
65
+ );
66
+ }
67
+ }
68
+
69
+ /** Recognize geographic WGS-84 WKT with degree units and the Greenwich prime meridian. */
70
+ function isWgs84GeographicWkt(wellKnownText: string): boolean {
71
+ try {
72
+ if (getSpatialCoordinateFrame(wellKnownText) !== 'geographic') {
73
+ return false;
74
+ }
75
+ const {root} = parseWKTCRS(wellKnownText);
76
+ const nodes = collectWktNodes(root);
77
+ const datum = root.values.find(
78
+ value =>
79
+ value.type === 'node' &&
80
+ ['DATUM', 'ENSEMBLE', 'GEODETICDATUM'].includes(value.keyword.toUpperCase())
81
+ );
82
+ if (datum?.type !== 'node' || datum.values[0]?.type !== 'string') {
83
+ return false;
84
+ }
85
+ const datumName = datum.values[0].value.toLowerCase().replace(/[^a-z0-9]/g, '');
86
+ if (
87
+ ![
88
+ 'wgs84',
89
+ 'wgs1984',
90
+ 'dwgs1984',
91
+ 'worldgeodeticsystem1984',
92
+ 'worldgeodeticsystem1984ensemble'
93
+ ].includes(datumName)
94
+ ) {
95
+ return false;
96
+ }
97
+ const angularUnits = nodes.filter(
98
+ node =>
99
+ ['UNIT', 'ANGLEUNIT'].includes(node.keyword.toUpperCase()) &&
100
+ (node === root || root.values.includes(node) || node.keyword.toUpperCase() === 'ANGLEUNIT')
101
+ );
102
+ const primeMeridian = nodes.find(node => node.keyword.toUpperCase() === 'PRIMEM');
103
+ return (
104
+ angularUnits.length > 0 &&
105
+ angularUnits.every(
106
+ node =>
107
+ node.values[1]?.type === 'number' &&
108
+ Math.abs(node.values[1].value - Math.PI / 180) < 1e-12
109
+ ) &&
110
+ (!primeMeridian ||
111
+ (primeMeridian.values[1]?.type === 'number' && primeMeridian.values[1].value === 0))
112
+ );
113
+ } catch {
114
+ return false;
115
+ }
116
+ }
117
+
118
+ /** Collect WKT nodes for semantic checks without inspecting quoted text as syntax. */
119
+ function collectWktNodes(node: WKTCRSNode): WKTCRSNode[] {
120
+ return [
121
+ node,
122
+ ...node.values.flatMap(value => (value.type === 'node' ? collectWktNodes(value) : []))
123
+ ];
124
+ }
@@ -48,13 +48,13 @@ export class DepsInstaller {
48
48
  await this.installFromNpm('draco', 'draco-worker-node.js');
49
49
 
50
50
  console.log('Installing "Draco Writer" worker'); // eslint-disable-line no-console
51
- await this.installFromNpm('draco', 'draco-writer-worker-node.js');
51
+ await this.installFromNpm('draco', 'draco-writer-worker-node.cjs');
52
52
 
53
53
  console.log('Installing "Basis Loader" worker'); // eslint-disable-line no-console
54
- await this.installFromNpm('textures', 'basis-worker-node.js');
54
+ await this.installFromNpm('textures', 'basis-worker-node.cjs');
55
55
 
56
56
  console.log('Installing "KTX2 Basis Writer" worker'); // eslint-disable-line no-console
57
- await this.installFromNpm('textures', 'ktx2-basis-writer-worker-node.js');
57
+ await this.installFromNpm('textures', 'ktx2-basis-writer-worker-node.cjs');
58
58
 
59
59
  console.log('Installing "Draco decoder" library'); // eslint-disable-line no-console
60
60
  await this.installFromUrl(
@@ -101,13 +101,16 @@ export class DepsInstaller {
101
101
  console.log('All dependencies were installed succesfully.'); // eslint-disable-line no-console
102
102
  }
103
103
 
104
+ /** Download a published worker or library into the converter's local resource directory. */
104
105
  private async installFromNpm(module: string, name: string, extraPath: string = '') {
105
- const fileResponse = await fetchFile(
106
- `https://unpkg.com/@loaders.gl/${module}@${VERSION}/dist/${extraPath}/${name}`
107
- );
106
+ const resourcePath = ['dist', extraPath, name].filter(Boolean).join('/');
107
+ const resourceUrl = `https://unpkg.com/@loaders.gl/${module}@${VERSION}/${resourcePath}`;
108
+ const fileResponse = await fetchFile(resourceUrl);
108
109
 
109
110
  if (fileResponse.status < 200 || fileResponse.status >= 300) {
110
- throw new Error(`Failed to load resource ${name}`);
111
+ throw new Error(
112
+ `Failed to load resource ${name} from ${resourceUrl}: HTTP ${fileResponse.status}`
113
+ );
111
114
  }
112
115
 
113
116
  const fileData = await fileResponse.arrayBuffer();
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Encodes I3S string attributes as little-endian counts followed by UTF-8 strings.
3
+ * Each string's byte count includes its trailing null byte. Values retain the
4
+ * converter's existing JavaScript string coercion, including null and undefined.
5
+ */
6
+ export function encodeStringAttribute(values: readonly unknown[]): ArrayBuffer {
7
+ const textEncoder = new TextEncoder();
8
+ const encodedValues = Array.from(values, value => textEncoder.encode(`${String(value)}\0`));
9
+ const stringByteLength = encodedValues.reduce((total, value) => total + value.byteLength, 0);
10
+ const headerByteLength = (2 + values.length) * Uint32Array.BYTES_PER_ELEMENT;
11
+ const output = new Uint8Array(headerByteLength + stringByteLength);
12
+ const header = new DataView(output.buffer);
13
+ header.setUint32(0, values.length, true);
14
+ header.setUint32(4, stringByteLength, true);
15
+
16
+ let byteOffset = headerByteLength;
17
+ for (const [index, encodedValue] of encodedValues.entries()) {
18
+ header.setUint32(8 + index * Uint32Array.BYTES_PER_ELEMENT, encodedValue.byteLength, true);
19
+ output.set(encodedValue, byteOffset);
20
+ byteOffset += encodedValue.byteLength;
21
+ }
22
+ return output.buffer;
23
+ }
@@ -21,7 +21,6 @@ import {Ellipsoid} from '@math.gl/geospatial';
21
21
  /* @ts-expect-error TS2732: Cannot find module 'node:module' or its corresponding type declarations. */
22
22
  import {createRequire} from 'node:module';
23
23
  /* @ts-expect-error TS2732: Cannot find module 'node:module' or its corresponding type declarations. */
24
- import {dirname, join} from 'node:path';
25
24
 
26
25
  import {DracoWriterWorker} from '@loaders.gl/draco';
27
26
  import {assert, encode} from '@loaders.gl/core';
@@ -34,6 +33,7 @@ import {
34
33
  import md5 from 'md5';
35
34
  import {v4 as uuidv4} from 'uuid';
36
35
  import {generateAttributes} from './geometry-attributes';
36
+ import {encodeStringAttribute} from './encode-string-attribute';
37
37
  import {createBoundingVolumesFromGeometry} from './coordinate-converter';
38
38
  import {
39
39
  ConvertedAttributes,
@@ -1028,15 +1028,18 @@ async function mergeMaterials(
1028
1028
  material1.mergedMaterials &&
1029
1029
  material2.mergedMaterials
1030
1030
  ) {
1031
- const buffer1 = Buffer.from(material1.texture.bufferView.data);
1032
- const buffer2 = Buffer.from(material2.texture.bufferView.data);
1031
+ const firstImageBytes = new Uint8Array(material1.texture.bufferView.data);
1032
+ const secondImageBytes = new Uint8Array(material2.texture.bufferView.data);
1033
1033
  try {
1034
1034
  // @ts-ignore
1035
1035
  const {joinImages} = await import('join-images');
1036
- const sharpData = await joinImages([buffer1, buffer2], {direction: 'horizontal'});
1037
- material1.texture.bufferView.data = await sharpData
1036
+ const sharpData = await joinImages([firstImageBytes, secondImageBytes], {
1037
+ direction: 'horizontal'
1038
+ });
1039
+ const encodedImage = await sharpData
1038
1040
  .toFormat(material1.texture.mimeType === 'image/png' ? 'png' : 'jpeg')
1039
1041
  .toBuffer();
1042
+ material1.texture.bufferView.data = new Uint8Array(encodedImage);
1040
1043
  } catch (error) {
1041
1044
  // eslint-disable-next-line no-console
1042
1045
  console.log(
@@ -1458,10 +1461,10 @@ function generateAttributeBuffer(type: string, value: any): ArrayBuffer {
1458
1461
  attributeBuffer = generateDoubleAttributeBuffer(value);
1459
1462
  break;
1460
1463
  case STRING_TYPE:
1461
- attributeBuffer = generateStringAttributeBuffer(value);
1464
+ attributeBuffer = encodeStringAttribute(value);
1462
1465
  break;
1463
1466
  default:
1464
- attributeBuffer = generateStringAttributeBuffer(value);
1467
+ attributeBuffer = encodeStringAttribute(value);
1465
1468
  }
1466
1469
 
1467
1470
  return attributeBuffer;
@@ -1497,7 +1500,7 @@ function getAttributeType(key: string, attributeStorageInfo: any[]): string {
1497
1500
  /**
1498
1501
  * Convert short integer to attribute arrayBuffer.
1499
1502
  * @param featureIds
1500
- * @returns - Buffer with objectId data.
1503
+ * @returns ArrayBuffer with objectId data.
1501
1504
  */
1502
1505
  function generateShortIntegerAttributeBuffer(featureIds: any[]): ArrayBuffer {
1503
1506
  const count = new Uint32Array([featureIds.length]);
@@ -1508,7 +1511,7 @@ function generateShortIntegerAttributeBuffer(featureIds: any[]): ArrayBuffer {
1508
1511
  /**
1509
1512
  * Convert double to attribute arrayBuffer.
1510
1513
  * @param featureIds
1511
- * @returns - Buffer with objectId data.
1514
+ * @returns ArrayBuffer with objectId data.
1512
1515
  */
1513
1516
  function generateDoubleAttributeBuffer(featureIds: any[]): ArrayBuffer {
1514
1517
  const count = new Uint32Array([featureIds.length]);
@@ -1518,36 +1521,6 @@ function generateDoubleAttributeBuffer(featureIds: any[]): ArrayBuffer {
1518
1521
  return concatenateArrayBuffers(count.buffer, padding.buffer, valuesArray.buffer);
1519
1522
  }
1520
1523
 
1521
- /**
1522
- * Convert batch table attributes to array buffer with batch table data.
1523
- * @param batchAttributes
1524
- * @returns - Buffer with batch table data.
1525
- */
1526
- function generateStringAttributeBuffer(batchAttributes: any[]): ArrayBuffer {
1527
- const stringCountArray = new Uint32Array([batchAttributes.length]);
1528
- let totalNumberOfBytes = 0;
1529
- const stringSizesArray = new Uint32Array(batchAttributes.length);
1530
- const stringBufferArray: ArrayBuffer[] = [];
1531
-
1532
- for (let index = 0; index < batchAttributes.length; index++) {
1533
- const currentString = `${String(batchAttributes[index])}\0`;
1534
- const currentStringBuffer = Buffer.from(currentString);
1535
- const currentStringSize = currentStringBuffer.length;
1536
- totalNumberOfBytes += currentStringSize;
1537
- stringSizesArray[index] = currentStringSize;
1538
- stringBufferArray.push(currentStringBuffer);
1539
- }
1540
-
1541
- const totalBytes = new Uint32Array([totalNumberOfBytes]);
1542
-
1543
- return concatenateArrayBuffers(
1544
- stringCountArray.buffer,
1545
- totalBytes.buffer,
1546
- stringSizesArray.buffer,
1547
- ...stringBufferArray
1548
- );
1549
- }
1550
-
1551
1524
  /**
1552
1525
  * Convert featureIds to BigUint64Array.
1553
1526
  * @param featureIds
@@ -1642,14 +1615,14 @@ async function generateCompressedGeometry(
1642
1615
  );
1643
1616
  }
1644
1617
 
1618
+ /** Resolve the published Node Draco writer, falling back to the dependency installer destination. */
1645
1619
  function getLocalDracoWriterWorkerUrl(): string {
1646
1620
  try {
1647
1621
  const require = createRequire(import.meta.url);
1648
- const packageJsonPath = require.resolve('@loaders.gl/draco/package.json');
1649
- return join(dirname(packageJsonPath), 'src/workers/draco-writer-worker-node.cjs');
1622
+ return require.resolve('@loaders.gl/draco/draco-writer-worker-node.cjs');
1650
1623
  } catch {
1651
1624
  // Fall back to monorepo-local path
1652
- return './modules/draco/src/workers/draco-writer-worker-node.cjs';
1625
+ return './modules/draco/dist/draco-writer-worker-node.cjs';
1653
1626
  }
1654
1627
  }
1655
1628
 
@@ -2,6 +2,7 @@ import type {Tiles3DTileContent} from '@loaders.gl/3d-tiles';
2
2
  import type {GLTFAccessorPostprocessed, GLTFNodePostprocessed} from '@loaders.gl/gltf';
3
3
  import {Matrix4, TypedArray, Vector3} from '@math.gl/core';
4
4
  import {BoundingSphere, OrientedBoundingBox} from '@math.gl/culling';
5
+ import {getImageData} from '@loaders.gl/images';
5
6
  import {Ellipsoid} from '@math.gl/geospatial';
6
7
  import {GLTFAttributesData} from '../types';
7
8
 
@@ -30,15 +31,15 @@ export function prepareDataForAttributesConversion(
30
31
  if (imageObject?.image?.compressed) {
31
32
  return null;
32
33
  }
33
- const data = imageObject?.image?.data;
34
+ const {data, width, height} = getImageData(imageObject.image);
34
35
  const dataCopy = new Uint8Array(data.length);
35
36
  dataCopy.set(data);
36
37
  return {
37
38
  data: dataCopy,
38
39
  compressed: false,
39
- height: imageObject.image.height,
40
- width: imageObject.image.width,
41
- components: imageObject.image.components,
40
+ height,
41
+ width,
42
+ components: data.length / (width * height),
42
43
  mimeType: imageObject.mimeType
43
44
  };
44
45
  }) || [];
@@ -119,15 +119,11 @@ export async function loadFromArchive(
119
119
  const hashTable = await loadHashTable(fileProvider);
120
120
  const archive = new Tiles3DArchive(fileProvider, hashTable, tz3Path);
121
121
  const fileSystem = new ZipFileSystem(archive);
122
- // const loadOptionsWithoutDeprecatedFetch = {...loadOptions} as Tiles3DLoaderOptions & {
123
- // fetch?: unknown;
124
- // };
125
- // delete loadOptionsWithoutDeprecatedFetch.fetch;
126
- // const mergedOptions = {
127
- // ...loadOptionsWithoutDeprecatedFetch,
128
- // core: {...loadOptions?.core, fetch: fileSystem.fetch.bind(fileSystem)}
129
- // }
130
- const content = await load(filename, loader, loadOptions);
122
+ const archiveLoadOptions = {
123
+ ...loadOptions,
124
+ core: {...loadOptions?.core, fetch: fileSystem.fetch.bind(fileSystem)}
125
+ };
126
+ const content = await load(filename, loader, archiveLoadOptions);
131
127
  await fileSystem.destroy();
132
128
  return content;
133
129
  }
@@ -49,7 +49,7 @@ import {validateNodeBoundingVolumes} from './helpers/node-debug';
49
49
  import {KTX2BasisWriterWorker} from '@loaders.gl/textures';
50
50
  import {LoaderWithParser} from '@loaders.gl/loader-utils';
51
51
  import {I3SMaterialDefinition, TextureSetDefinitionFormats} from '@loaders.gl/i3s';
52
- import {ImageWriter} from '@loaders.gl/images';
52
+ import {getImageData, ImageWriter} from '@loaders.gl/images';
53
53
  import {GLTFImagePostprocessed} from '@loaders.gl/gltf';
54
54
  import {
55
55
  GLTFPrimitiveModeString,
@@ -143,7 +143,7 @@ export default class I3SConverter {
143
143
  basis: {
144
144
  format: 'rgba32',
145
145
  // We need to load local fs workers because nodejs can't load workers from the Internet
146
- workerUrl: './modules/textures/dist/basis-worker-node.js'
146
+ workerUrl: './modules/textures/dist/basis-worker-node.cjs'
147
147
  },
148
148
  // We need to load local fs workers because nodejs can't load workers from the Internet
149
149
  draco: {workerUrl: './modules/draco/dist/draco-worker-node.js'},
@@ -1266,18 +1266,17 @@ export default class I3SConverter {
1266
1266
 
1267
1267
  if (this.generateTextures) {
1268
1268
  formats.push({name: '1', format: 'ktx2'});
1269
- // For Node.js texture.image.data is type of Buffer
1270
- const copyArrayBuffer = texture.image.data.subarray();
1271
- const arrayToEncode = new Uint8Array(copyArrayBuffer);
1269
+ const imageData = getImageData(texture.image);
1270
+ const arrayToEncode = new Uint8Array(imageData.data);
1272
1271
  const ktx2TextureData = encode(
1273
- {...texture.image, data: arrayToEncode},
1272
+ {...imageData, data: arrayToEncode},
1274
1273
  // @ts-expect-error - Worker encoder typing is still WIP
1275
1274
  KTX2BasisWriterWorker,
1276
1275
  {
1277
1276
  ...KTX2BasisWriterWorker.options,
1278
1277
  ['ktx2-basis-writer']: {
1279
1278
  // We need to load local fs workers because nodejs can't load workers from the Internet
1280
- workerUrl: './modules/textures/dist/ktx2-basis-writer-worker-node.js'
1279
+ workerUrl: './modules/textures/dist/ktx2-basis-writer-worker-node.cjs'
1281
1280
  },
1282
1281
  reuseWorkers: true,
1283
1282
  _nodeWorkers: true,
@@ -6,8 +6,8 @@ The server provides I3S Rest endpoints per specification https://github.com/Esri
6
6
  ## Development
7
7
 
8
8
  - install tsx globally `yarn global add tsx`
9
- - uncomment "local debug" lines in `modules/tile-converter/src/i3s-server/app.ts` and `modules/tile-converter/src/i3s-server/bin/www.ts` (see comments);
10
- - run `PORT=8080 HTTPS_PORT=4443 I3sLayerPath="./path/to/dataset" DEBUG=i3s-server:* npx tsx ./src/i3s-server/bin/www.ts`
9
+ - uncomment "local debug" lines in `src/v4/i3s-server/app.ts` and `src/v4/i3s-server/bin/www.ts` (see comments);
10
+ - run `PORT=8080 HTTPS_PORT=4443 I3sLayerPath="./path/to/dataset" DEBUG=i3s-server:* npx tsx ./src/v4/i3s-server/bin/www.ts`
11
11
 
12
12
  ## Usage
13
13
 
@@ -29,6 +29,7 @@ router.get('*', (req, res, next) => {
29
29
  const json = JSON.parse(textDecoder.decode(file));
30
30
  res.send(json);
31
31
  } catch (_e) {
32
+ // Express 4 sends typed arrays as JSON; adapt only at this Node.js HTTP boundary.
32
33
  res.send(Buffer.from(file));
33
34
  }
34
35
  } else {
@@ -0,0 +1,2 @@
1
+ export {default as I3SConverter} from './i3s-converter/i3s-converter';
2
+ export {default as Tiles3DConverter} from './3d-tiles-converter/3d-tiles-converter';
@@ -0,0 +1,130 @@
1
+ // loaders.gl
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ import {TileConversionError} from './conversion-api.js';
6
+ import type {TileConversionReport, TileConversionSink} from './conversion-api.js';
7
+
8
+ const BLOB_SIZE_GETTER = Object.getOwnPropertyDescriptor(Blob.prototype, 'size')?.get;
9
+
10
+ /** One named browser output resource. */
11
+ export interface BrowserTileConversionResource {
12
+ /** Stable relative output path supplied by the target profile. */
13
+ readonly resourceId: string;
14
+ /** Parts combined into a Blob after the total output budget is checked. */
15
+ readonly parts: readonly BlobPart[];
16
+ /** MIME type recorded on the resulting Blob. */
17
+ readonly contentType?: string;
18
+ }
19
+
20
+ /** A named output resource retained by the bounded browser sink. */
21
+ export interface BrowserTileConversionFile {
22
+ /** Stable relative output path supplied by the target profile. */
23
+ readonly resourceId: string;
24
+ /** Browser-native data for download or application-provided packaging. */
25
+ readonly blob: Blob;
26
+ }
27
+
28
+ /** Memory limit for a browser conversion sink. */
29
+ export interface BoundedMemoryTileConversionSinkOptions {
30
+ /** Maximum combined Blob size retained by this sink, captured at creation, in bytes. */
31
+ readonly maxTotalBytes: number;
32
+ }
33
+
34
+ /** Browser sink with read access to its bounded output resources. */
35
+ export interface BoundedMemoryTileConversionSink
36
+ extends TileConversionSink<BrowserTileConversionResource> {
37
+ /** Returns the retained files in stable resource-ID order. */
38
+ getFiles(): readonly BrowserTileConversionFile[];
39
+ }
40
+
41
+ /**
42
+ * Creates a browser-only sink that retains output as Blobs under a total byte limit.
43
+ *
44
+ * The sink does not download or archive files. Applications can use `getFiles()` after
45
+ * `convertTileset` completes and package or download the returned Blobs as needed.
46
+ *
47
+ * @param options - Maximum in-memory output size.
48
+ * @returns A sink that stores browser-native Blobs and clears them on abort.
49
+ */
50
+ export function createBoundedMemoryTileConversionSink(
51
+ options: BoundedMemoryTileConversionSinkOptions
52
+ ): BoundedMemoryTileConversionSink {
53
+ const {maxTotalBytes} = options;
54
+ if (!Number.isSafeInteger(maxTotalBytes) || maxTotalBytes < 0) {
55
+ throw new TileConversionError(
56
+ 'INVALID_OUTPUT_MEMORY_LIMIT',
57
+ 'The browser output memory limit must be a non-negative safe integer'
58
+ );
59
+ }
60
+
61
+ const files = new Map<string, BrowserTileConversionFile>();
62
+ let totalBytes = 0;
63
+ let closed = false;
64
+
65
+ return {
66
+ getFiles() {
67
+ return [...files.values()].sort((left, right) =>
68
+ left.resourceId < right.resourceId ? -1 : left.resourceId > right.resourceId ? 1 : 0
69
+ );
70
+ },
71
+ async write(resource, signal) {
72
+ if (signal?.aborted) {
73
+ throw signal.reason ?? new DOMException('The operation was aborted', 'AbortError');
74
+ }
75
+ if (closed) {
76
+ throw new TileConversionError(
77
+ 'RESOURCE_SINK_CLOSED',
78
+ 'Cannot write after sink finalization'
79
+ );
80
+ }
81
+ if (!resource.resourceId || files.has(resource.resourceId)) {
82
+ throw new TileConversionError(
83
+ 'INVALID_BROWSER_RESOURCE_ID',
84
+ 'Browser output resource IDs must be non-empty and unique'
85
+ );
86
+ }
87
+
88
+ const byteLength = resource.parts.reduce((total, part) => total + measureBlobPart(part), 0);
89
+ if (!Number.isSafeInteger(byteLength) || totalBytes + byteLength > maxTotalBytes) {
90
+ throw new TileConversionError(
91
+ 'OUTPUT_MEMORY_LIMIT_EXCEEDED',
92
+ `Browser output exceeds the configured ${maxTotalBytes} byte memory limit`
93
+ );
94
+ }
95
+
96
+ const blob = new Blob([...resource.parts], {type: resource.contentType});
97
+ files.set(resource.resourceId, {resourceId: resource.resourceId, blob});
98
+ totalBytes += blob.size;
99
+ },
100
+ async finalize(_report: TileConversionReport) {
101
+ if (closed) {
102
+ throw new TileConversionError('RESOURCE_SINK_CLOSED', 'Sink has already been finalized');
103
+ }
104
+ closed = true;
105
+ },
106
+ async abort() {
107
+ files.clear();
108
+ totalBytes = 0;
109
+ closed = true;
110
+ }
111
+ };
112
+ }
113
+
114
+ /** Measures supported Blob parts without copying them into a Blob first. */
115
+ function measureBlobPart(part: BlobPart): number {
116
+ if (typeof part === 'string') {
117
+ return new TextEncoder().encode(part).byteLength;
118
+ }
119
+ if (BLOB_SIZE_GETTER) {
120
+ try {
121
+ return BLOB_SIZE_GETTER.call(part) as number;
122
+ } catch {
123
+ // Keep checking the remaining supported Blob part types.
124
+ }
125
+ }
126
+ if (part instanceof ArrayBuffer || ArrayBuffer.isView(part)) {
127
+ return part.byteLength;
128
+ }
129
+ throw new TileConversionError('UNSUPPORTED_BROWSER_RESOURCE', 'Unsupported browser Blob part');
130
+ }