@loaders.gl/tile-converter 5.0.0-alpha.11 → 5.0.0-alpha.14
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/LICENSE +1 -1
- package/README.md +180 -4
- package/bin/converter-v5.js +2 -0
- package/dist/converter.min.cjs +172 -182
- package/dist/i3s-server/bin/i3s-server.min.cjs +113 -158
- package/dist/index.cjs +18832 -19228
- package/dist/index.js +19566 -19968
- package/dist/v5/adapters.cjs +2002 -230
- package/dist/v5/adapters.d.ts +20 -0
- package/dist/v5/adapters.js +2005 -214
- package/dist/v5/archive-source.d.ts +36 -0
- package/dist/v5/browser-tileset-source.d.ts +55 -11
- package/dist/v5/browser.cjs +18 -0
- package/dist/v5/browser.d.ts +15 -1
- package/dist/v5/browser.js +26 -0
- package/dist/v5/conversion-api.d.ts +34 -0
- package/dist/v5/converter-cli.d.ts +1 -0
- package/dist/v5/converter-cli.js +1230 -0
- package/dist/v5/core.cjs +547 -77
- package/dist/v5/core.d.ts +3 -1
- package/dist/v5/core.js +540 -70
- package/dist/v5/feature-arrow.d.ts +1 -1
- package/dist/v5/i3s-mesh-conversion.d.ts +5 -1
- package/dist/v5/i3s-mesh-source-features.d.ts +2 -2
- package/dist/v5/i3s-mesh-source-material.d.ts +1 -1
- package/dist/v5/i3s-mesh-source.d.ts +2 -2
- package/dist/v5/i3s-selection-source.d.ts +70 -0
- package/dist/v5/index.cjs +15 -0
- package/dist/v5/index.d.ts +13 -1
- package/dist/v5/index.js +22 -0
- package/dist/v5/mesh-conversion.d.ts +21 -3
- package/dist/v5/mesh-draco.d.ts +2 -2
- package/dist/v5/mesh-features.d.ts +2 -2
- package/dist/v5/mesh-hierarchy.d.ts +47 -0
- package/dist/v5/mesh-source-features.d.ts +6 -5
- package/dist/v5/mesh-source-schema.d.ts +12 -0
- package/dist/v5/mesh-source-texture.d.ts +3 -3
- package/dist/v5/mesh-source.d.ts +10 -7
- package/dist/v5/mesh-tileset-sink.d.ts +5 -0
- package/dist/v5/mesh.d.ts +1 -1
- package/dist/v5/node-cli.d.ts +9 -0
- package/dist/v5/node-conversion.d.ts +34 -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 +913 -0
- package/dist/v5/node.d.ts +6 -0
- package/dist/v5/node.js +890 -0
- package/dist/v5/point-cloud-attributes.d.ts +19 -0
- package/dist/v5/point-cloud-conversion.d.ts +1 -1
- package/dist/v5/point-cloud-copc.d.ts +44 -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 +1 -1
- package/dist/v5/point-cloud-source.d.ts +1 -1
- package/dist/v5/point-cloud-tileset.d.ts +44 -0
- package/dist/v5/point-cloud.d.ts +1 -1
- 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 +1 -1
- package/dist/v5/tileset-source.d.ts +1 -1
- package/package.json +25 -32
- package/src/index.ts +4 -1
- package/src/v4/3d-tiles-converter/3d-tiles-converter.ts +11 -12
- package/src/v4/3d-tiles-converter/helpers/3d-tiles-content-converter.ts +7 -3
- package/src/v4/3d-tiles-converter/helpers/i3s-obb-to-3d-tiles-obb.ts +2 -2
- package/src/v4/3d-tiles-converter/helpers/load-i3s.ts +4 -6
- package/src/v4/3d-tiles-converter/helpers/texture-atlas.ts +1 -1
- package/src/v4/3d-tiles-converter/helpers/validate-i3s-spatial-reference.ts +2 -2
- package/src/v4/3d-tiles-converter/json-templates/tileset.ts +58 -39
- package/src/v4/converter-cli.ts +10 -4
- package/src/v4/deps-installer/deps-installer.ts +3 -3
- package/src/v4/i3s-converter/helpers/attribute-metadata-info.ts +1 -1
- package/src/v4/i3s-converter/helpers/batch-ids-extensions.ts +9 -10
- package/src/v4/i3s-converter/helpers/conversion-gltf.ts +108 -0
- package/src/v4/i3s-converter/helpers/coordinate-converter.ts +6 -5
- package/src/v4/i3s-converter/helpers/create-scene-server-path.ts +5 -6
- package/src/v4/i3s-converter/helpers/feature-attributes.ts +4 -4
- package/src/v4/i3s-converter/helpers/geometry-attributes.ts +1 -1
- package/src/v4/i3s-converter/helpers/geometry-converter.ts +69 -77
- package/src/v4/i3s-converter/helpers/gltf-attributes.ts +58 -74
- package/src/v4/i3s-converter/helpers/load-3d-tiles.ts +6 -54
- package/src/v4/i3s-converter/helpers/node-debug.ts +7 -3
- package/src/v4/i3s-converter/helpers/node-index-document.ts +11 -9
- package/src/v4/i3s-converter/helpers/node-pages.ts +3 -4
- package/src/v4/i3s-converter/helpers/preprocess-3d-tiles.ts +2 -2
- package/src/v4/i3s-converter/helpers/tileset-traversal.ts +1 -1
- package/src/v4/i3s-converter/i3s-converter.ts +29 -31
- package/src/v4/i3s-converter/json-templates/geometry-definitions.ts +71 -66
- package/src/v4/i3s-converter/json-templates/layers.ts +97 -132
- package/src/v4/i3s-converter/json-templates/metadata.ts +31 -21
- package/src/v4/i3s-converter/json-templates/node.ts +60 -74
- package/src/v4/i3s-converter/json-templates/scene-server.ts +36 -27
- package/src/v4/i3s-converter/json-templates/shared-resources.ts +62 -123
- package/src/v4/i3s-converter/json-templates/store.ts +39 -57
- package/src/v4/i3s-converter/types.ts +4 -4
- package/src/v4/i3s-server/controllers/slpk-controller.ts +3 -3
- package/src/v4/i3s-server/utils/create-scene-server.ts +3 -3
- package/src/v4/lib/utils/builder-input.ts +8 -0
- package/src/v4/lib/utils/conversion-dump.ts +5 -1
- package/src/v4/lib/utils/file-utils.ts +1 -1
- package/src/v4/lib/utils/hash-string.ts +21 -0
- package/src/v4/lib/utils/lod-conversion-utils.ts +3 -3
- package/src/v4/pgm-loader.ts +2 -2
- package/src/v5/adapters.ts +53 -0
- package/src/v5/archive-source.ts +235 -0
- package/src/v5/browser-tileset-source.ts +418 -48
- package/src/v5/browser.ts +46 -0
- package/src/v5/conversion-api.ts +184 -17
- package/src/v5/conversion-archive.ts +2 -2
- package/src/v5/converter-cli.ts +28 -0
- package/src/v5/core.ts +7 -0
- package/src/v5/feature-arrow.ts +2 -2
- package/src/v5/i3s-mesh-conversion.ts +41 -3
- package/src/v5/i3s-mesh-sink.ts +1 -1
- package/src/v5/i3s-mesh-source-features.ts +3 -3
- package/src/v5/i3s-mesh-source-material.ts +18 -11
- package/src/v5/i3s-mesh-source.ts +3 -2
- package/src/v5/i3s-selection-source.ts +298 -0
- package/src/v5/index.ts +41 -0
- package/src/v5/mesh-conversion.ts +24 -4
- package/src/v5/mesh-draco.ts +2 -2
- package/src/v5/mesh-features.ts +3 -3
- package/src/v5/mesh-hierarchy.ts +648 -0
- package/src/v5/mesh-source-features.ts +165 -51
- package/src/v5/mesh-source-schema.ts +140 -0
- package/src/v5/mesh-source-texture.ts +49 -19
- package/src/v5/mesh-source.ts +80 -49
- package/src/v5/mesh-tileset-archive.ts +1 -1
- package/src/v5/mesh-tileset-sink.ts +2 -2
- package/src/v5/mesh.ts +1 -1
- package/src/v5/node-cli.ts +201 -0
- package/src/v5/node-conversion.ts +362 -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-attributes.ts +113 -0
- package/src/v5/point-cloud-conversion.ts +1 -1
- package/src/v5/point-cloud-copc.ts +289 -0
- package/src/v5/point-cloud-file.ts +306 -0
- package/src/v5/point-cloud-potree.ts +134 -0
- package/src/v5/point-cloud-source-encoder.ts +1 -1
- package/src/v5/point-cloud-source.ts +1 -1
- package/src/v5/point-cloud-spatial.ts +2 -2
- package/src/v5/point-cloud-tileset.ts +137 -0
- package/src/v5/point-cloud.ts +3 -3
- package/src/v5/resource-fetcher.ts +4 -1
- package/src/v5/resource-manifest.ts +40 -5
- package/src/v5/selected-mesh-source.ts +384 -0
- package/src/v5/spatial-conversion.ts +90 -19
- package/src/v5/tileset-source.ts +2 -2
package/dist/v5/node.cjs
ADDED
|
@@ -0,0 +1,913 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/v5/node.ts
|
|
31
|
+
var node_exports = {};
|
|
32
|
+
__export(node_exports, {
|
|
33
|
+
NODE_TILESET_LIMITS: () => NODE_TILESET_LIMITS,
|
|
34
|
+
convertNodeTileset: () => convertNodeTileset,
|
|
35
|
+
createNodeI3SConversionSource: () => createNodeI3SConversionSource,
|
|
36
|
+
createNodeTilesetConversionSource: () => createNodeTilesetConversionSource,
|
|
37
|
+
writeNodeTileConversionOutput: () => writeNodeTileConversionOutput
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(node_exports);
|
|
40
|
+
|
|
41
|
+
// src/v5/node-source.ts
|
|
42
|
+
var import_promises = require("node:fs/promises");
|
|
43
|
+
var import_node_path = require("node:path");
|
|
44
|
+
var import_node_url = require("node:url");
|
|
45
|
+
var import_d_tiles2 = require("@loaders.gl/tiles-3d/3d-tiles");
|
|
46
|
+
var import_core2 = require("@loaders.gl/tile-converter/v5/core");
|
|
47
|
+
|
|
48
|
+
// src/v5/conversion-api.ts
|
|
49
|
+
var TileConversionError = class extends Error {
|
|
50
|
+
/** Stable machine-readable failure code. */
|
|
51
|
+
code;
|
|
52
|
+
/** Diagnostics associated with this failure. */
|
|
53
|
+
diagnostics;
|
|
54
|
+
/** Creates a typed conversion failure. */
|
|
55
|
+
constructor(code, message, diagnostics = []) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "TileConversionError";
|
|
58
|
+
this.code = code;
|
|
59
|
+
this.diagnostics = diagnostics;
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// src/v5/resource-fetcher.ts
|
|
64
|
+
function createTileConversionResourceFetcher(options) {
|
|
65
|
+
let consumed = options.initialInputBytes ?? 0;
|
|
66
|
+
if (!Number.isSafeInteger(options.maxInputBytes) || options.maxInputBytes < 1 || !Number.isSafeInteger(consumed) || consumed < 0 || consumed > options.maxInputBytes)
|
|
67
|
+
throw new TileConversionError(
|
|
68
|
+
"INVALID_INPUT_MEMORY_LIMIT",
|
|
69
|
+
"Resource fetch requires a positive aggregate byte budget and valid initial bytes"
|
|
70
|
+
);
|
|
71
|
+
const fetcher = options.fetcher ?? fetch;
|
|
72
|
+
return async (input, requestOptions) => {
|
|
73
|
+
options.signal?.throwIfAborted();
|
|
74
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
75
|
+
if (!["http:", "https:", "blob:", "data:"].includes(url.protocol))
|
|
76
|
+
throw new TileConversionError(
|
|
77
|
+
"EXTERNAL_RESOURCE_UNSUPPORTED",
|
|
78
|
+
"Conversion transport requires HTTP(S), Blob or embedded data resources"
|
|
79
|
+
);
|
|
80
|
+
const requestSignal = requestOptions?.signal ?? (input instanceof Request ? input.signal : void 0);
|
|
81
|
+
const signal = options.signal && requestSignal ? AbortSignal.any([options.signal, requestSignal]) : options.signal ?? requestSignal;
|
|
82
|
+
const response = await fetcher(input, { ...requestOptions, signal });
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
await response.body?.cancel();
|
|
85
|
+
throw new TileConversionError(
|
|
86
|
+
"BROWSER_SOURCE_FETCH_FAILED",
|
|
87
|
+
`Could not fetch conversion resource (${response.status})`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const reader = response.body?.getReader();
|
|
91
|
+
if (!reader)
|
|
92
|
+
throw new TileConversionError(
|
|
93
|
+
"BROWSER_SOURCE_STREAM_UNAVAILABLE",
|
|
94
|
+
"Conversion resources require readable response streams"
|
|
95
|
+
);
|
|
96
|
+
const cancelRead = () => {
|
|
97
|
+
void reader.cancel(signal?.reason).catch(() => {
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
signal?.addEventListener("abort", cancelRead, { once: true });
|
|
101
|
+
const parts = [];
|
|
102
|
+
let byteLength = 0;
|
|
103
|
+
try {
|
|
104
|
+
while (true) {
|
|
105
|
+
signal?.throwIfAborted();
|
|
106
|
+
const { done, value } = await reader.read();
|
|
107
|
+
signal?.throwIfAborted();
|
|
108
|
+
if (done) break;
|
|
109
|
+
if (value.byteLength > options.maxInputBytes - consumed)
|
|
110
|
+
throw new TileConversionError(
|
|
111
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
112
|
+
"Selected contents and external dependencies exceed the aggregate input byte budget"
|
|
113
|
+
);
|
|
114
|
+
consumed += value.byteLength;
|
|
115
|
+
byteLength += value.byteLength;
|
|
116
|
+
parts.push(value);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
await reader.cancel(error);
|
|
120
|
+
throw error;
|
|
121
|
+
} finally {
|
|
122
|
+
signal?.removeEventListener("abort", cancelRead);
|
|
123
|
+
reader.releaseLock();
|
|
124
|
+
}
|
|
125
|
+
const bytes = new Uint8Array(byteLength);
|
|
126
|
+
let offset = 0;
|
|
127
|
+
for (const part of parts) {
|
|
128
|
+
bytes.set(part, offset);
|
|
129
|
+
offset += part.byteLength;
|
|
130
|
+
}
|
|
131
|
+
const boundedResponse = new Response(bytes, {
|
|
132
|
+
status: response.status,
|
|
133
|
+
statusText: response.statusText,
|
|
134
|
+
headers: response.headers
|
|
135
|
+
});
|
|
136
|
+
Object.defineProperty(boundedResponse, "url", { value: response.url });
|
|
137
|
+
return boundedResponse;
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/v5/archive-source.ts
|
|
142
|
+
var import_loader_utils = require("@loaders.gl/core/loader-utils");
|
|
143
|
+
var import_i3s = require("@loaders.gl/tiles-3d/i3s");
|
|
144
|
+
var import_d_tiles = require("@loaders.gl/tiles-3d/3d-tiles");
|
|
145
|
+
var import_core = require("@loaders.gl/tile-converter/v5/core");
|
|
146
|
+
async function openTileConversionArchive(descriptor, maxInputBytes, signal, fetcher = fetch) {
|
|
147
|
+
signal.throwIfAborted();
|
|
148
|
+
if (!["slpk", "3tz"].includes(descriptor.format))
|
|
149
|
+
throw new import_core.TileConversionError("INVALID_ARCHIVE_SOURCE", "Select SLPK or 3TZ archive input");
|
|
150
|
+
validateArchiveEntityTag(descriptor.identity?.etag);
|
|
151
|
+
const transport = createTileConversionResourceFetcher({
|
|
152
|
+
maxInputBytes,
|
|
153
|
+
signal,
|
|
154
|
+
fetcher: async (url, options) => {
|
|
155
|
+
const response = await fetcher(url, options);
|
|
156
|
+
if (response.status !== 206) {
|
|
157
|
+
await response.body?.cancel();
|
|
158
|
+
throw new Error("Remote conversion archives require HTTP 206 byte-range responses.");
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
validateArchiveResponseValidator(response.headers);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
await response.body?.cancel(error);
|
|
164
|
+
throw error;
|
|
165
|
+
}
|
|
166
|
+
return response;
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
let file;
|
|
170
|
+
let closePromise;
|
|
171
|
+
async function closeFile() {
|
|
172
|
+
if (file) {
|
|
173
|
+
closePromise ??= file.close();
|
|
174
|
+
await closePromise;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
if (typeof descriptor.input === "string") {
|
|
179
|
+
if (!["http:", "https:"].includes(new URL(descriptor.input).protocol))
|
|
180
|
+
throw new Error("Use an HTTP(S) archive URL.");
|
|
181
|
+
const remote = new import_loader_utils.HttpFile(descriptor.input, {
|
|
182
|
+
fetch: (url, options) => transport(url, options),
|
|
183
|
+
consistency: "strict",
|
|
184
|
+
...descriptor.identity
|
|
185
|
+
});
|
|
186
|
+
file = remote;
|
|
187
|
+
await remote.open(signal);
|
|
188
|
+
} else file = new import_loader_utils.BlobFile(descriptor.input);
|
|
189
|
+
const ownedFile = file;
|
|
190
|
+
file = void 0;
|
|
191
|
+
return await createTileConversionArchiveTransport(
|
|
192
|
+
ownedFile,
|
|
193
|
+
descriptor.format,
|
|
194
|
+
maxInputBytes,
|
|
195
|
+
signal,
|
|
196
|
+
descriptor
|
|
197
|
+
);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
await closeFile();
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async function createTileConversionArchiveTransport(file, format, maxInputBytes, signal, descriptor) {
|
|
204
|
+
const rootUrl = `https://conversion.invalid/input.${format}`;
|
|
205
|
+
let closed = false;
|
|
206
|
+
let releasePromise;
|
|
207
|
+
let closePromise;
|
|
208
|
+
const closeFile = () => {
|
|
209
|
+
closed = true;
|
|
210
|
+
closePromise ??= file.close();
|
|
211
|
+
return closePromise;
|
|
212
|
+
};
|
|
213
|
+
const checkOpen = () => {
|
|
214
|
+
signal.throwIfAborted();
|
|
215
|
+
if (closed)
|
|
216
|
+
throw new import_core.TileConversionError("ARCHIVE_SOURCE_CLOSED", "Conversion archive is closed");
|
|
217
|
+
};
|
|
218
|
+
try {
|
|
219
|
+
checkOpen();
|
|
220
|
+
if (!["slpk", "3tz"].includes(format) || !Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1)
|
|
221
|
+
throw new import_core.TileConversionError(
|
|
222
|
+
"INVALID_ARCHIVE_SOURCE",
|
|
223
|
+
"Archive source requires a supported format and positive safe byte budget"
|
|
224
|
+
);
|
|
225
|
+
if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > maxInputBytes)
|
|
226
|
+
throw new Error("Conversion archive input must be at most 16 MiB.");
|
|
227
|
+
let readBytes = 0;
|
|
228
|
+
const boundedFile = {
|
|
229
|
+
handle: file.handle,
|
|
230
|
+
size: file.size,
|
|
231
|
+
bigsize: file.bigsize,
|
|
232
|
+
url: file.url,
|
|
233
|
+
...file.stat ? { stat: () => file.stat() } : {},
|
|
234
|
+
close: closeFile,
|
|
235
|
+
/** Checks indexed reads before allocation and forwards operation cancellation. */
|
|
236
|
+
read: async (start, length) => {
|
|
237
|
+
checkOpen();
|
|
238
|
+
if (!Number.isSafeInteger(length) || length < 0 || length > maxInputBytes - readBytes)
|
|
239
|
+
throw new import_core.TileConversionError(
|
|
240
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
241
|
+
"Archive indexed reads exceed the input budget"
|
|
242
|
+
);
|
|
243
|
+
readBytes += length;
|
|
244
|
+
const bytes = await file.read(start, length, signal);
|
|
245
|
+
checkOpen();
|
|
246
|
+
return bytes;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
const archive = format === "slpk" ? await (0, import_i3s.parseSLPKArchive)(boundedFile) : await (0, import_d_tiles.parse3DTilesArchive)(boundedFile);
|
|
250
|
+
const root = new URL(rootUrl);
|
|
251
|
+
return {
|
|
252
|
+
rootUrl: format === "3tz" ? `${rootUrl}/tileset.json` : rootUrl,
|
|
253
|
+
descriptor: descriptor ? {
|
|
254
|
+
...descriptor,
|
|
255
|
+
...file instanceof import_loader_utils.HttpFile ? { identity: file.getIdentitySnapshot() } : {}
|
|
256
|
+
} : void 0,
|
|
257
|
+
/** Resolves only resources belonging to this archive, without external network fallback. */
|
|
258
|
+
fetcher: (async (input, options) => {
|
|
259
|
+
checkOpen();
|
|
260
|
+
options?.signal?.throwIfAborted();
|
|
261
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
262
|
+
if (url.origin !== root.origin || url.pathname !== root.pathname && !url.pathname.startsWith(`${root.pathname}/`))
|
|
263
|
+
throw new Error("Conversion dependencies must resolve inside the selected archive.");
|
|
264
|
+
const path = decodeURIComponent(
|
|
265
|
+
url.pathname.slice(root.pathname.length).replace(/^\//, "")
|
|
266
|
+
);
|
|
267
|
+
if (path.includes("\\") || path.split("/").some((segment) => segment === ".." || segment === "."))
|
|
268
|
+
throw new Error("Archive dependency path cannot escape its root.");
|
|
269
|
+
const bytes = format === "slpk" ? await archive.getFile(path, "http") : await archive.getFile(path);
|
|
270
|
+
checkOpen();
|
|
271
|
+
options?.signal?.throwIfAborted();
|
|
272
|
+
return new Response(bytes);
|
|
273
|
+
}),
|
|
274
|
+
/** Releases the readable file on success, failure or cancellation. */
|
|
275
|
+
close: async () => {
|
|
276
|
+
closed = true;
|
|
277
|
+
releasePromise ??= archive.close();
|
|
278
|
+
await releasePromise;
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
} catch (error) {
|
|
282
|
+
await closeFile();
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function validateArchiveEntityTag(entityTag) {
|
|
287
|
+
if (entityTag !== void 0 && entityTag !== null && !/^"[\x21\x23-\x7e\x80-\xff]*"$/.test(entityTag))
|
|
288
|
+
throw new import_core.TileConversionError(
|
|
289
|
+
"ARCHIVE_VALIDATOR_UNSUPPORTED",
|
|
290
|
+
"Remote archives require a strong quoted ETag, or Last-Modified without an ETag; weak/malformed ETags are unsupported"
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
function validateArchiveResponseValidator(headers) {
|
|
294
|
+
const entityTag = headers.get("etag");
|
|
295
|
+
validateArchiveEntityTag(entityTag);
|
|
296
|
+
if (entityTag !== null) return;
|
|
297
|
+
const lastModified = headers.get("last-modified");
|
|
298
|
+
const responseDate = headers.get("date");
|
|
299
|
+
const modifiedTime = parseArchiveHttpDate(lastModified);
|
|
300
|
+
const responseTime = parseArchiveHttpDate(responseDate);
|
|
301
|
+
if (!Number.isFinite(modifiedTime) || !Number.isFinite(responseTime) || responseTime - modifiedTime < 6e4)
|
|
302
|
+
throw new import_core.TileConversionError(
|
|
303
|
+
"ARCHIVE_VALIDATOR_UNSUPPORTED",
|
|
304
|
+
"Last-Modified archive identity requires a valid Date at least 60 seconds later; otherwise supply a strong ETag"
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
function parseArchiveHttpDate(value) {
|
|
308
|
+
const milliseconds = value === null ? NaN : Date.parse(value);
|
|
309
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toUTCString() === value ? milliseconds : NaN;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/v5/node-source.ts
|
|
313
|
+
var NODE_TILESET_LIMITS = {
|
|
314
|
+
/** Aggregate response bytes and, separately, indexed archive read bytes. */
|
|
315
|
+
maxInputBytes: 16 * 1024 * 1024,
|
|
316
|
+
/** Declared content placements in the explicit root document. */
|
|
317
|
+
maxInputResources: 1e3,
|
|
318
|
+
/** Maximum root/external document placements inspected or traversed. */
|
|
319
|
+
maxTilesetDocuments: 32
|
|
320
|
+
};
|
|
321
|
+
async function createNodeTilesetConversionSource(options) {
|
|
322
|
+
const maxInputBytes = options.maxInputBytes ?? NODE_TILESET_LIMITS.maxInputBytes;
|
|
323
|
+
const maxInputResources = options.maxInputResources ?? NODE_TILESET_LIMITS.maxInputResources;
|
|
324
|
+
const maxTilesetDocuments = options.maxTilesetDocuments ?? 1;
|
|
325
|
+
if (!Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1 || maxInputBytes > NODE_TILESET_LIMITS.maxInputBytes || !Number.isSafeInteger(maxInputResources) || maxInputResources < 1 || maxInputResources > NODE_TILESET_LIMITS.maxInputResources || !Number.isSafeInteger(maxTilesetDocuments) || maxTilesetDocuments < 1 || maxTilesetDocuments > NODE_TILESET_LIMITS.maxTilesetDocuments || !["3d-tiles", "3tz"].includes(options.format ?? "3d-tiles"))
|
|
326
|
+
throw new import_core2.TileConversionError(
|
|
327
|
+
"INVALID_NODE_SOURCE_OPTIONS",
|
|
328
|
+
"Node sources require positive byte/resource budgets within the initial profile limits"
|
|
329
|
+
);
|
|
330
|
+
options.signal?.throwIfAborted();
|
|
331
|
+
const controller = new AbortController();
|
|
332
|
+
const lifetimeSignal = options.signal ? AbortSignal.any([controller.signal, options.signal]) : controller.signal;
|
|
333
|
+
let archiveFile;
|
|
334
|
+
let closeArchive;
|
|
335
|
+
let consumedReadBytes = 0;
|
|
336
|
+
const reserveRead = (length) => {
|
|
337
|
+
if (length > maxInputBytes - consumedReadBytes)
|
|
338
|
+
throw new import_core2.TileConversionError(
|
|
339
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
340
|
+
"Local reads exceed the aggregate input budget"
|
|
341
|
+
);
|
|
342
|
+
consumedReadBytes += length;
|
|
343
|
+
};
|
|
344
|
+
try {
|
|
345
|
+
const inputUrl = /^(?:https?|file):/i.test(options.input) ? new URL(options.input) : void 0;
|
|
346
|
+
if (!inputUrl && /^[a-z][a-z\d+.-]*:/i.test(options.input) && !/^[a-z]:[\\/]/i.test(options.input))
|
|
347
|
+
throw new import_core2.TileConversionError(
|
|
348
|
+
"UNSUPPORTED_NODE_SOURCE",
|
|
349
|
+
"Use a local path, file URL or HTTP(S) tileset URL"
|
|
350
|
+
);
|
|
351
|
+
const remote = inputUrl && inputUrl.protocol !== "file:";
|
|
352
|
+
let rootUrl;
|
|
353
|
+
let transport;
|
|
354
|
+
let virtualRoot;
|
|
355
|
+
if (remote && options.format === "3tz") {
|
|
356
|
+
const archive = await openTileConversionArchive(
|
|
357
|
+
{ input: inputUrl.href, format: "3tz" },
|
|
358
|
+
maxInputBytes,
|
|
359
|
+
lifetimeSignal,
|
|
360
|
+
options.fetcher
|
|
361
|
+
);
|
|
362
|
+
rootUrl = archive.rootUrl;
|
|
363
|
+
transport = archive.fetcher;
|
|
364
|
+
closeArchive = archive.close;
|
|
365
|
+
} else if (remote) {
|
|
366
|
+
rootUrl = inputUrl.href;
|
|
367
|
+
transport = options.fetcher ?? fetch;
|
|
368
|
+
} else {
|
|
369
|
+
const inputPath = await (0, import_promises.realpath)(inputUrl ? (0, import_node_url.fileURLToPath)(inputUrl) : (0, import_node_path.resolve)(options.input));
|
|
370
|
+
lifetimeSignal.throwIfAborted();
|
|
371
|
+
const directory = (0, import_node_path.dirname)(inputPath);
|
|
372
|
+
virtualRoot = new URL("https://conversion.invalid/input/");
|
|
373
|
+
rootUrl = new URL(
|
|
374
|
+
options.format === "3tz" ? "tileset.json" : encodeURIComponent((0, import_node_path.basename)(inputPath)),
|
|
375
|
+
virtualRoot
|
|
376
|
+
).href;
|
|
377
|
+
if (options.format === "3tz") {
|
|
378
|
+
archiveFile = await openLocalFile(inputPath, maxInputBytes, lifetimeSignal, reserveRead);
|
|
379
|
+
const archive = await (0, import_d_tiles2.parse3DTilesArchive)(archiveFile);
|
|
380
|
+
transport = async (input, requestOptions) => {
|
|
381
|
+
lifetimeSignal.throwIfAborted();
|
|
382
|
+
requestOptions?.signal?.throwIfAborted();
|
|
383
|
+
const path = getLocalResourcePath(input, virtualRoot);
|
|
384
|
+
const bytes = await archive.getFile(path);
|
|
385
|
+
lifetimeSignal.throwIfAborted();
|
|
386
|
+
requestOptions?.signal?.throwIfAborted();
|
|
387
|
+
return new Response(bytes);
|
|
388
|
+
};
|
|
389
|
+
} else {
|
|
390
|
+
transport = async (input, requestOptions) => {
|
|
391
|
+
const path = getLocalResourcePath(input, virtualRoot);
|
|
392
|
+
const resolvedPath = await (0, import_promises.realpath)((0, import_node_path.resolve)(directory, path));
|
|
393
|
+
const relativePath = (0, import_node_path.relative)(directory, resolvedPath);
|
|
394
|
+
if (relativePath === ".." || relativePath.startsWith(`..${import_node_path.sep}`) || (0, import_node_path.isAbsolute)(relativePath))
|
|
395
|
+
throw new import_core2.TileConversionError(
|
|
396
|
+
"EXTERNAL_RESOURCE_UNSUPPORTED",
|
|
397
|
+
"Local dependencies must remain inside the tileset directory"
|
|
398
|
+
);
|
|
399
|
+
const signal = requestOptions?.signal ? AbortSignal.any([lifetimeSignal, requestOptions.signal]) : lifetimeSignal;
|
|
400
|
+
const file = await openLocalFile(resolvedPath, maxInputBytes, signal, reserveRead);
|
|
401
|
+
try {
|
|
402
|
+
return new Response(await file.read());
|
|
403
|
+
} finally {
|
|
404
|
+
await file.close();
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const resourceFetcher = createTileConversionResourceFetcher({
|
|
410
|
+
maxInputBytes,
|
|
411
|
+
signal: lifetimeSignal,
|
|
412
|
+
fetcher: transport
|
|
413
|
+
});
|
|
414
|
+
const source = (0, import_core2.createBrowserTilesetConversionSource)({
|
|
415
|
+
input: rootUrl,
|
|
416
|
+
maxInputBytes,
|
|
417
|
+
maxInputResources,
|
|
418
|
+
maxTilesetDocuments,
|
|
419
|
+
inspectNestedTilesets: options.inspectNestedTilesets,
|
|
420
|
+
fetcher: resourceFetcher
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
fetcher: resourceFetcher,
|
|
424
|
+
/** Inspects declarations without opening tile payloads or decoding coordinates. */
|
|
425
|
+
async inspect(signal) {
|
|
426
|
+
const inspection = await source.inspect(
|
|
427
|
+
signal ? AbortSignal.any([lifetimeSignal, signal]) : lifetimeSignal
|
|
428
|
+
);
|
|
429
|
+
if (virtualRoot)
|
|
430
|
+
for (const resource of inspection.resources)
|
|
431
|
+
getLocalResourcePath(
|
|
432
|
+
new URL(resource.uri, resource.tilesetUrl ?? inspection.rootUrl),
|
|
433
|
+
virtualRoot
|
|
434
|
+
);
|
|
435
|
+
return inspection;
|
|
436
|
+
},
|
|
437
|
+
/** Reads each declared placement with the same lifetime-wide input budget. */
|
|
438
|
+
read: (inspection, signal) => source.read(
|
|
439
|
+
inspection,
|
|
440
|
+
signal ? AbortSignal.any([lifetimeSignal, signal]) : lifetimeSignal
|
|
441
|
+
),
|
|
442
|
+
/** Cancels future reads and releases the archive handle. */
|
|
443
|
+
async close() {
|
|
444
|
+
controller.abort(new import_core2.TileConversionError("NODE_SOURCE_CLOSED", "Node source is closed"));
|
|
445
|
+
const file = archiveFile;
|
|
446
|
+
archiveFile = void 0;
|
|
447
|
+
await closeArchive?.();
|
|
448
|
+
await file?.close();
|
|
449
|
+
}
|
|
450
|
+
};
|
|
451
|
+
} catch (error) {
|
|
452
|
+
await closeArchive?.();
|
|
453
|
+
await archiveFile?.close();
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
function getLocalResourcePath(input, root) {
|
|
458
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
459
|
+
if (url.origin !== root.origin || !url.pathname.startsWith(root.pathname))
|
|
460
|
+
throw new import_core2.TileConversionError(
|
|
461
|
+
"EXTERNAL_RESOURCE_UNSUPPORTED",
|
|
462
|
+
"Dependencies must resolve inside the selected local input"
|
|
463
|
+
);
|
|
464
|
+
const path = decodeURIComponent(url.pathname.slice(root.pathname.length));
|
|
465
|
+
if (!path || path.startsWith("/") || path.includes("\\") || path.split("/").some((segment) => segment === ".." || segment === "."))
|
|
466
|
+
throw new import_core2.TileConversionError(
|
|
467
|
+
"EXTERNAL_RESOURCE_UNSUPPORTED",
|
|
468
|
+
"Dependency paths cannot escape the selected input"
|
|
469
|
+
);
|
|
470
|
+
return path;
|
|
471
|
+
}
|
|
472
|
+
async function openLocalFile(path, maxInputBytes, signal, reserveRead) {
|
|
473
|
+
signal.throwIfAborted();
|
|
474
|
+
const handle = await (0, import_promises.open)(path, "r");
|
|
475
|
+
try {
|
|
476
|
+
signal.throwIfAborted();
|
|
477
|
+
const information = await handle.stat();
|
|
478
|
+
if (!information.isFile() || !Number.isSafeInteger(information.size) || information.size > maxInputBytes)
|
|
479
|
+
throw new import_core2.TileConversionError(
|
|
480
|
+
"INPUT_RESOURCE_TOO_LARGE",
|
|
481
|
+
"Local input must be a regular file within the byte budget"
|
|
482
|
+
);
|
|
483
|
+
const size = information.size;
|
|
484
|
+
return {
|
|
485
|
+
handle,
|
|
486
|
+
size,
|
|
487
|
+
bigsize: BigInt(size),
|
|
488
|
+
url: path,
|
|
489
|
+
/** Reads a bounded range, checking cancellation and unexpected EOF between native reads. */
|
|
490
|
+
async read(start = 0, length = size - Number(start), readSignal) {
|
|
491
|
+
const operationSignal = readSignal ? AbortSignal.any([signal, readSignal]) : signal;
|
|
492
|
+
operationSignal.throwIfAborted();
|
|
493
|
+
const offset = Number(start);
|
|
494
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(length) || length < 0 || offset > size - length)
|
|
495
|
+
throw new import_core2.TileConversionError(
|
|
496
|
+
"INVALID_INPUT_RANGE",
|
|
497
|
+
"Local file reads require a range inside the file"
|
|
498
|
+
);
|
|
499
|
+
reserveRead(length);
|
|
500
|
+
const bytes = new Uint8Array(length);
|
|
501
|
+
let readBytes = 0;
|
|
502
|
+
while (readBytes < length) {
|
|
503
|
+
operationSignal.throwIfAborted();
|
|
504
|
+
const result = await handle.read(
|
|
505
|
+
bytes,
|
|
506
|
+
readBytes,
|
|
507
|
+
length - readBytes,
|
|
508
|
+
offset + readBytes
|
|
509
|
+
);
|
|
510
|
+
if (result.bytesRead === 0)
|
|
511
|
+
throw new import_core2.TileConversionError(
|
|
512
|
+
"INPUT_FILE_TRUNCATED",
|
|
513
|
+
"Local input ended before its declared size"
|
|
514
|
+
);
|
|
515
|
+
readBytes += result.bytesRead;
|
|
516
|
+
}
|
|
517
|
+
operationSignal.throwIfAborted();
|
|
518
|
+
return bytes.buffer;
|
|
519
|
+
},
|
|
520
|
+
/** Releases the Node file handle. */
|
|
521
|
+
close: () => handle.close()
|
|
522
|
+
};
|
|
523
|
+
} catch (error) {
|
|
524
|
+
await handle.close();
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// src/v5/node-conversion.ts
|
|
530
|
+
var import_promises3 = require("node:fs/promises");
|
|
531
|
+
var import_node_path3 = require("node:path");
|
|
532
|
+
var import_core4 = require("@loaders.gl/tile-converter/v5/core");
|
|
533
|
+
var import_adapters2 = require("@loaders.gl/tile-converter/v5/adapters");
|
|
534
|
+
|
|
535
|
+
// src/v5/node-i3s-source.ts
|
|
536
|
+
var import_promises2 = require("node:fs/promises");
|
|
537
|
+
var import_node_url2 = require("node:url");
|
|
538
|
+
var import_node_path2 = require("node:path");
|
|
539
|
+
var import_core3 = require("@loaders.gl/tile-converter/v5/core");
|
|
540
|
+
var import_adapters = require("@loaders.gl/tile-converter/v5/adapters");
|
|
541
|
+
async function createNodeI3SConversionSource(options) {
|
|
542
|
+
const maxInputBytes = options.maxInputBytes ?? NODE_TILESET_LIMITS.maxInputBytes;
|
|
543
|
+
const maxInputResources = options.maxInputResources ?? NODE_TILESET_LIMITS.maxInputResources;
|
|
544
|
+
if (!["i3s", "slpk"].includes(options.format) || !Number.isSafeInteger(maxInputBytes) || maxInputBytes < 1 || maxInputBytes > NODE_TILESET_LIMITS.maxInputBytes || !Number.isSafeInteger(maxInputResources) || maxInputResources < 1 || maxInputResources > NODE_TILESET_LIMITS.maxInputResources)
|
|
545
|
+
throw new import_core3.TileConversionError(
|
|
546
|
+
"INVALID_NODE_SOURCE_OPTIONS",
|
|
547
|
+
"I3S sources require positive budgets within the bounded input profile"
|
|
548
|
+
);
|
|
549
|
+
const controller = new AbortController();
|
|
550
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
551
|
+
signal.throwIfAborted();
|
|
552
|
+
let release;
|
|
553
|
+
let closePromise;
|
|
554
|
+
const close = async () => {
|
|
555
|
+
controller.abort(new import_core3.TileConversionError("NODE_SOURCE_CLOSED", "Node source is closed"));
|
|
556
|
+
closePromise ??= release?.() ?? Promise.resolve();
|
|
557
|
+
await closePromise;
|
|
558
|
+
};
|
|
559
|
+
try {
|
|
560
|
+
const remote = /^https?:/i.test(options.input);
|
|
561
|
+
if (options.format === "i3s" && !remote)
|
|
562
|
+
throw new import_core3.TileConversionError(
|
|
563
|
+
"UNSUPPORTED_NODE_SOURCE",
|
|
564
|
+
"I3S input requires an HTTP(S) layer URL; use slpk for a local archive"
|
|
565
|
+
);
|
|
566
|
+
let rootUrl;
|
|
567
|
+
let transport;
|
|
568
|
+
if (options.format === "slpk") {
|
|
569
|
+
const archive = remote ? await (0, import_adapters.openTileConversionArchive)(
|
|
570
|
+
{ input: options.input, format: "slpk" },
|
|
571
|
+
maxInputBytes,
|
|
572
|
+
signal,
|
|
573
|
+
options.fetcher
|
|
574
|
+
) : await (0, import_adapters.createTileConversionArchiveTransport)(
|
|
575
|
+
await openLocalFile(
|
|
576
|
+
await (0, import_promises2.realpath)(
|
|
577
|
+
/^file:/i.test(options.input) ? (0, import_node_url2.fileURLToPath)(options.input) : (0, import_node_path2.resolve)(options.input)
|
|
578
|
+
),
|
|
579
|
+
maxInputBytes,
|
|
580
|
+
signal,
|
|
581
|
+
() => {
|
|
582
|
+
}
|
|
583
|
+
),
|
|
584
|
+
"slpk",
|
|
585
|
+
maxInputBytes,
|
|
586
|
+
signal
|
|
587
|
+
);
|
|
588
|
+
release = archive.close;
|
|
589
|
+
rootUrl = archive.rootUrl;
|
|
590
|
+
transport = archive.fetcher;
|
|
591
|
+
} else {
|
|
592
|
+
rootUrl = new URL(options.input).href;
|
|
593
|
+
transport = options.fetcher ?? fetch;
|
|
594
|
+
}
|
|
595
|
+
const bounded = createTileConversionResourceFetcher({
|
|
596
|
+
maxInputBytes,
|
|
597
|
+
signal,
|
|
598
|
+
fetcher: transport
|
|
599
|
+
});
|
|
600
|
+
let metadataBytes = 0;
|
|
601
|
+
const root = new URL(rootUrl);
|
|
602
|
+
const fetcher = async (input, requestOptions) => {
|
|
603
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
604
|
+
if (url.origin === root.origin && (url.pathname === root.pathname || url.pathname.startsWith(`${root.pathname}/`))) {
|
|
605
|
+
for (const [key, value] of root.searchParams)
|
|
606
|
+
if (!url.searchParams.has(key)) url.searchParams.set(key, value);
|
|
607
|
+
}
|
|
608
|
+
const response = await bounded(
|
|
609
|
+
input instanceof Request ? new Request(url, input) : url,
|
|
610
|
+
requestOptions
|
|
611
|
+
);
|
|
612
|
+
if (response.url && response.url !== url.href)
|
|
613
|
+
throw new import_core3.TileConversionError(
|
|
614
|
+
"I3S_REDIRECT_UNSUPPORTED",
|
|
615
|
+
"Use the final I3S resource URL; redirected resource bases are not qualified"
|
|
616
|
+
);
|
|
617
|
+
const bytes = await response.arrayBuffer();
|
|
618
|
+
metadataBytes += bytes.byteLength;
|
|
619
|
+
const result = new Response(bytes, { status: response.status, headers: response.headers });
|
|
620
|
+
Object.defineProperty(result, "url", { value: response.url });
|
|
621
|
+
return result;
|
|
622
|
+
};
|
|
623
|
+
const selectionOptions = {
|
|
624
|
+
rootUrl,
|
|
625
|
+
fetcher,
|
|
626
|
+
signal,
|
|
627
|
+
maxInputBytes,
|
|
628
|
+
maxInputResources,
|
|
629
|
+
dracoLibraryOptions: { decoderProfile: "full" }
|
|
630
|
+
};
|
|
631
|
+
return {
|
|
632
|
+
close,
|
|
633
|
+
/** Reads only layer and node metadata, under the same lifetime-wide budget as conversion. */
|
|
634
|
+
async inspect(operationSignal) {
|
|
635
|
+
signal.throwIfAborted();
|
|
636
|
+
operationSignal?.throwIfAborted();
|
|
637
|
+
const activeSignal = operationSignal ? AbortSignal.any([signal, operationSignal]) : signal;
|
|
638
|
+
const operationFetcher = (input, requestOptions) => fetcher(input, {
|
|
639
|
+
...requestOptions,
|
|
640
|
+
signal: requestOptions?.signal ? AbortSignal.any([activeSignal, requestOptions.signal]) : activeSignal
|
|
641
|
+
});
|
|
642
|
+
const inspection = await (0, import_adapters.inspectI3SMeshConversionSource)({
|
|
643
|
+
...selectionOptions,
|
|
644
|
+
fetcher: operationFetcher,
|
|
645
|
+
signal: activeSignal
|
|
646
|
+
});
|
|
647
|
+
return { ...inspection, metadataBytes };
|
|
648
|
+
},
|
|
649
|
+
/** Creates an original-coordinate selected mesh source using application-provided Draco assets. */
|
|
650
|
+
select: (inspection, resourceIds, dracoLibraryOptions, features) => {
|
|
651
|
+
signal.throwIfAborted();
|
|
652
|
+
return (0, import_adapters.createSelectedI3SMeshConversionSource)(inspection, {
|
|
653
|
+
...selectionOptions,
|
|
654
|
+
resourceIds,
|
|
655
|
+
dracoLibraryOptions,
|
|
656
|
+
features
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
} catch (error) {
|
|
661
|
+
await close();
|
|
662
|
+
throw error;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// src/v5/node-conversion.ts
|
|
667
|
+
async function convertNodeTileset(options) {
|
|
668
|
+
const started = options.profile ? performance.now() : 0;
|
|
669
|
+
let setupMilliseconds = 0;
|
|
670
|
+
const signal = options.signal ?? new AbortController().signal;
|
|
671
|
+
const maxOutputBytes = options.maxOutputBytes ?? import_adapters2.SELECTED_MESH_LIMITS.maxOutputBytes;
|
|
672
|
+
if (!["3d-tiles", "3tz", "slpk"].includes(options.outputFormat) || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1 || maxOutputBytes > import_adapters2.SELECTED_MESH_LIMITS.maxOutputBytes)
|
|
673
|
+
throw new import_core4.TileConversionError(
|
|
674
|
+
"INVALID_NODE_CONVERSION_OPTIONS",
|
|
675
|
+
"Select a supported output profile and a positive output budget at most 32 MiB"
|
|
676
|
+
);
|
|
677
|
+
const i3s = options.format === "i3s" || options.format === "slpk";
|
|
678
|
+
if (options.preserveHierarchy !== void 0 && typeof options.preserveHierarchy !== "boolean" || options.preserveHierarchy && (i3s || options.outputFormat === "slpk"))
|
|
679
|
+
throw new import_core4.TileConversionError(
|
|
680
|
+
"MESH_HIERARCHY_UNSUPPORTED",
|
|
681
|
+
"Hierarchy preservation requires 3D Tiles/3TZ input and output"
|
|
682
|
+
);
|
|
683
|
+
if (i3s && (options.outputFormat !== "slpk" && options.geometricError === void 0 || options.geometricError !== void 0 && (typeof options.geometricError !== "number" || !Number.isFinite(options.geometricError) || options.geometricError < 0)))
|
|
684
|
+
throw new import_core4.TileConversionError(
|
|
685
|
+
"MESH_GEOMETRIC_ERROR_INVALID",
|
|
686
|
+
"I3S to 3D Tiles requires an explicit nonnegative geometric error in meters"
|
|
687
|
+
);
|
|
688
|
+
if (i3s ? options.features !== void 0 : options.i3sFeatures !== void 0 || options.integratedMeshPolicy !== void 0 || options.geometricError !== void 0)
|
|
689
|
+
throw new import_core4.TileConversionError(
|
|
690
|
+
"INVALID_NODE_CONVERSION_OPTIONS",
|
|
691
|
+
"Feature and layer policies must match the selected input format"
|
|
692
|
+
);
|
|
693
|
+
const sourceOptions = {
|
|
694
|
+
...options,
|
|
695
|
+
signal,
|
|
696
|
+
inspectNestedTilesets: true,
|
|
697
|
+
maxTilesetDocuments: options.maxTilesetDocuments ?? import_adapters2.SELECTED_MESH_LIMITS.maxTilesetDocuments
|
|
698
|
+
};
|
|
699
|
+
const rawSource = i3s ? await createNodeI3SConversionSource({
|
|
700
|
+
...sourceOptions,
|
|
701
|
+
format: options.format
|
|
702
|
+
}) : await createNodeTilesetConversionSource({
|
|
703
|
+
...sourceOptions,
|
|
704
|
+
format: options.format
|
|
705
|
+
});
|
|
706
|
+
let files;
|
|
707
|
+
let report;
|
|
708
|
+
try {
|
|
709
|
+
const inspection = await rawSource.inspect(signal);
|
|
710
|
+
await import("@loaders.gl/core/polyfills");
|
|
711
|
+
const { getDracoLibraryOptions } = await import("@loaders.gl/draco/bundled");
|
|
712
|
+
const dracoLibraryOptions = getDracoLibraryOptions({
|
|
713
|
+
decoderProfile: "full",
|
|
714
|
+
useLocalLibraries: true
|
|
715
|
+
});
|
|
716
|
+
const selection = "select" in rawSource ? {
|
|
717
|
+
...await rawSource.select(
|
|
718
|
+
inspection,
|
|
719
|
+
options.resourceIds,
|
|
720
|
+
dracoLibraryOptions,
|
|
721
|
+
options.i3sFeatures
|
|
722
|
+
),
|
|
723
|
+
geometricError: (options.geometricError ?? 0) + import_adapters2.SELECTED_MESH_LIMITS.maxPositionError
|
|
724
|
+
} : await (0, import_adapters2.createSelectedMeshConversionSource)(
|
|
725
|
+
inspection,
|
|
726
|
+
{
|
|
727
|
+
resourceIds: options.resourceIds,
|
|
728
|
+
preserveHierarchy: options.preserveHierarchy,
|
|
729
|
+
features: options.features,
|
|
730
|
+
signal,
|
|
731
|
+
source: rawSource,
|
|
732
|
+
fetcher: rawSource.fetcher,
|
|
733
|
+
dracoLibraryOptions,
|
|
734
|
+
maxInputBytes: options.maxInputBytes
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
const { source, spatialContext, geometricError } = selection;
|
|
738
|
+
setupMilliseconds = options.profile ? performance.now() - started : 0;
|
|
739
|
+
const common = {
|
|
740
|
+
profile: options.profile,
|
|
741
|
+
source,
|
|
742
|
+
signal,
|
|
743
|
+
measureInputBytes: import_adapters2.measureMeshBytes,
|
|
744
|
+
maxInputBytes: options.maxInputBytes ?? import_adapters2.SELECTED_MESH_LIMITS.maxInputBytes,
|
|
745
|
+
maxOutputBytes,
|
|
746
|
+
maxInputResources: import_adapters2.SELECTED_MESH_LIMITS.maxMeshResources,
|
|
747
|
+
maxOutputResources: import_adapters2.SELECTED_MESH_LIMITS.maxMeshResources
|
|
748
|
+
};
|
|
749
|
+
const codecOptions = {
|
|
750
|
+
spatialContext,
|
|
751
|
+
autoOrigin: i3s,
|
|
752
|
+
integratedMeshPolicy: options.integratedMeshPolicy,
|
|
753
|
+
dracoLibraryOptions,
|
|
754
|
+
maxPositionError: import_adapters2.SELECTED_MESH_LIMITS.maxPositionError
|
|
755
|
+
};
|
|
756
|
+
if (options.outputFormat === "slpk") {
|
|
757
|
+
const sink = (0, import_adapters2.createI3SMeshSink)({
|
|
758
|
+
maxTotalBytes: maxOutputBytes,
|
|
759
|
+
maxMeshes: import_adapters2.SELECTED_MESH_LIMITS.maxMeshResources,
|
|
760
|
+
maxResourceBytes: maxOutputBytes
|
|
761
|
+
});
|
|
762
|
+
report = await (0, import_core4.convertTileset)({
|
|
763
|
+
...common,
|
|
764
|
+
sink,
|
|
765
|
+
codec: (0, import_adapters2.createI3SMeshConversionCodec)({ ...codecOptions, maxResourceBytes: maxOutputBytes }),
|
|
766
|
+
measureOutputBytes: (resource) => Object.values(resource.files).reduce((total, bytes) => total + bytes.byteLength, 0)
|
|
767
|
+
});
|
|
768
|
+
files = sink.getFiles();
|
|
769
|
+
} else {
|
|
770
|
+
const sink = options.preserveHierarchy ? (0, import_adapters2.createMeshHierarchyTilesetSink)({
|
|
771
|
+
inspection,
|
|
772
|
+
resourceIds: options.resourceIds,
|
|
773
|
+
maxTotalBytes: maxOutputBytes,
|
|
774
|
+
maxPositionError: import_adapters2.SELECTED_MESH_LIMITS.maxPositionError
|
|
775
|
+
}) : (0, import_adapters2.createMeshTilesetSink)({
|
|
776
|
+
maxTotalBytes: maxOutputBytes,
|
|
777
|
+
maxMeshes: import_adapters2.SELECTED_MESH_LIMITS.maxMeshResources,
|
|
778
|
+
geometricError
|
|
779
|
+
});
|
|
780
|
+
report = await (0, import_core4.convertTileset)({
|
|
781
|
+
...common,
|
|
782
|
+
sink,
|
|
783
|
+
codec: (0, import_adapters2.createMeshConversionCodec)(codecOptions),
|
|
784
|
+
measureOutputBytes: (resource) => resource.glb.byteLength
|
|
785
|
+
});
|
|
786
|
+
files = sink.getFiles();
|
|
787
|
+
}
|
|
788
|
+
} finally {
|
|
789
|
+
await rawSource.close();
|
|
790
|
+
}
|
|
791
|
+
const outputStarted = options.profile ? performance.now() : 0;
|
|
792
|
+
signal.throwIfAborted();
|
|
793
|
+
if (options.outputFormat !== "3d-tiles" && !options.output) {
|
|
794
|
+
for await (const chunk of (0, import_adapters2.encodeTileConversionArchiveInBatches)(files, {
|
|
795
|
+
format: options.outputFormat,
|
|
796
|
+
maxArchiveBytes: maxOutputBytes,
|
|
797
|
+
signal
|
|
798
|
+
}))
|
|
799
|
+
void chunk;
|
|
800
|
+
}
|
|
801
|
+
if (options.output)
|
|
802
|
+
await writeNodeTileConversionOutput(
|
|
803
|
+
files,
|
|
804
|
+
options.output,
|
|
805
|
+
options.outputFormat,
|
|
806
|
+
maxOutputBytes,
|
|
807
|
+
signal
|
|
808
|
+
);
|
|
809
|
+
return report.timings ? {
|
|
810
|
+
...report,
|
|
811
|
+
timings: {
|
|
812
|
+
...report.timings,
|
|
813
|
+
setupMilliseconds,
|
|
814
|
+
outputMilliseconds: performance.now() - outputStarted,
|
|
815
|
+
totalMilliseconds: performance.now() - started
|
|
816
|
+
}
|
|
817
|
+
} : report;
|
|
818
|
+
}
|
|
819
|
+
async function writeNodeTileConversionOutput(files, output, format, maxOutputBytes, signal) {
|
|
820
|
+
signal.throwIfAborted();
|
|
821
|
+
if (!["3d-tiles", "3tz", "slpk"].includes(format) || !Number.isSafeInteger(maxOutputBytes) || maxOutputBytes < 1)
|
|
822
|
+
throw new import_core4.TileConversionError(
|
|
823
|
+
"INVALID_NODE_OUTPUT",
|
|
824
|
+
"Output requires a supported format and positive safe byte limit"
|
|
825
|
+
);
|
|
826
|
+
let bytes = 0;
|
|
827
|
+
const names = /* @__PURE__ */ new Set();
|
|
828
|
+
for (const file of files) {
|
|
829
|
+
const name = file.resourceId;
|
|
830
|
+
if (!name || name.includes("\\") || name.includes("\0") || name.includes(":") || name.split("/").some((segment) => !segment || segment === "." || segment === "..") || names.has(name))
|
|
831
|
+
throw new import_core4.TileConversionError(
|
|
832
|
+
"INVALID_NODE_OUTPUT",
|
|
833
|
+
"Output names must be unique confined relative resource paths"
|
|
834
|
+
);
|
|
835
|
+
names.add(name);
|
|
836
|
+
if (file.blob.size > maxOutputBytes - bytes)
|
|
837
|
+
throw new import_core4.TileConversionError(
|
|
838
|
+
"OUTPUT_BYTES_LIMIT_EXCEEDED",
|
|
839
|
+
"Output files exceed the byte budget"
|
|
840
|
+
);
|
|
841
|
+
bytes += file.blob.size;
|
|
842
|
+
}
|
|
843
|
+
const destination = (0, import_node_path3.resolve)(output);
|
|
844
|
+
if (format === "3d-tiles") {
|
|
845
|
+
await (0, import_promises3.mkdir)(destination);
|
|
846
|
+
try {
|
|
847
|
+
for (const file of files) {
|
|
848
|
+
signal.throwIfAborted();
|
|
849
|
+
const path = (0, import_node_path3.join)(destination, file.resourceId);
|
|
850
|
+
await (0, import_promises3.mkdir)((0, import_node_path3.dirname)(path), { recursive: true });
|
|
851
|
+
const handle = await (0, import_promises3.open)(path, "wx");
|
|
852
|
+
try {
|
|
853
|
+
const reader = file.blob.stream().getReader();
|
|
854
|
+
try {
|
|
855
|
+
while (true) {
|
|
856
|
+
signal.throwIfAborted();
|
|
857
|
+
const { value, done } = await reader.read();
|
|
858
|
+
if (done) break;
|
|
859
|
+
await writeFileBytes(handle, value, signal);
|
|
860
|
+
}
|
|
861
|
+
} finally {
|
|
862
|
+
await reader.cancel();
|
|
863
|
+
reader.releaseLock();
|
|
864
|
+
}
|
|
865
|
+
} finally {
|
|
866
|
+
await handle.close();
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
signal.throwIfAborted();
|
|
870
|
+
} catch (error) {
|
|
871
|
+
await (0, import_promises3.rm)(destination, { recursive: true, force: true });
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
} else {
|
|
875
|
+
const temporaryDirectory = await (0, import_promises3.mkdtemp)((0, import_node_path3.join)((0, import_node_path3.dirname)(destination), ".tile-converter-"));
|
|
876
|
+
try {
|
|
877
|
+
const path = (0, import_node_path3.join)(temporaryDirectory, "archive");
|
|
878
|
+
const handle = await (0, import_promises3.open)(path, "wx");
|
|
879
|
+
try {
|
|
880
|
+
for await (const chunk of (0, import_adapters2.encodeTileConversionArchiveInBatches)(files, {
|
|
881
|
+
format,
|
|
882
|
+
maxArchiveBytes: maxOutputBytes,
|
|
883
|
+
signal
|
|
884
|
+
}))
|
|
885
|
+
await writeFileBytes(handle, chunk, signal);
|
|
886
|
+
} finally {
|
|
887
|
+
await handle.close();
|
|
888
|
+
}
|
|
889
|
+
signal.throwIfAborted();
|
|
890
|
+
await (0, import_promises3.link)(path, destination);
|
|
891
|
+
} finally {
|
|
892
|
+
await (0, import_promises3.rm)(temporaryDirectory, { recursive: true, force: true });
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async function writeFileBytes(handle, bytes, signal) {
|
|
897
|
+
let offset = 0;
|
|
898
|
+
while (offset < bytes.byteLength) {
|
|
899
|
+
signal.throwIfAborted();
|
|
900
|
+
const result = await handle.write(bytes, offset, bytes.byteLength - offset);
|
|
901
|
+
if (!result.bytesWritten)
|
|
902
|
+
throw new import_core4.TileConversionError("NODE_OUTPUT_WRITE_FAILED", "Output write made no progress");
|
|
903
|
+
offset += result.bytesWritten;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
907
|
+
0 && (module.exports = {
|
|
908
|
+
NODE_TILESET_LIMITS,
|
|
909
|
+
convertNodeTileset,
|
|
910
|
+
createNodeI3SConversionSource,
|
|
911
|
+
createNodeTilesetConversionSource,
|
|
912
|
+
writeNodeTileConversionOutput
|
|
913
|
+
});
|