-
Notifications
You must be signed in to change notification settings - Fork 58
/
Copy pathconvert2xkt.js
executable file
·452 lines (400 loc) · 18 KB
/
convert2xkt.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
import {XKT_INFO} from "./XKT_INFO.js";
import {XKTModel} from "./XKTModel/XKTModel.js";
import {parseCityJSONIntoXKTModel} from "./parsers/parseCityJSONIntoXKTModel.js";
import {parseGLTFIntoXKTModel} from "./parsers/parseGLTFIntoXKTModel.js";
import {parseIFCIntoXKTModel} from "./parsers/parseIFCIntoXKTModel.js";
import {parseLASIntoXKTModel} from "./parsers/parseLASIntoXKTModel.js";
import {parsePCDIntoXKTModel} from "./parsers/parsePCDIntoXKTModel.js";
import {parsePLYIntoXKTModel} from "./parsers/parsePLYIntoXKTModel.js";
import {parseSTLIntoXKTModel} from "./parsers/parseSTLIntoXKTModel.js";
import {writeXKTModelToArrayBuffer} from "./XKTModel/writeXKTModelToArrayBuffer.js";
import {toArrayBuffer} from "./XKTModel/lib/toArraybuffer";
const fs = require('fs');
const path = require("path");
/**
* Converts model files into xeokit's native XKT format.
*
* Supported source formats are: IFC, CityJSON, glTF, LAZ and LAS.
*
* **Only bundled in xeokit-convert.cjs.js.**
*
* ## Usage
*
* ````javascript
* const convert2xkt = require("@xeokit/xeokit-convert/dist/convert2xkt.cjs.js");
* const fs = require('fs');
*
* convert2xkt({
* sourceData: fs.readFileSync("rme_advanced_sample_project.ifc"),
* outputXKT: (xtkArrayBuffer) => {
* fs.writeFileSync("rme_advanced_sample_project.ifc.xkt", xtkArrayBuffer);
* }
* }).then(() => {
* console.log("Converted.");
* }, (errMsg) => {
* console.error("Conversion failed: " + errMsg)
* });
````
* @param {Object} params Conversion parameters.
* @param {Object} params.WebIFC The WebIFC library. We pass this in as an external dependency, in order to give the
* caller the choice of whether to use the Browser or NodeJS version.
* @param {*} [params.configs] Configurations.
* @param {String} [params.source] Path to source file. Alternative to ````sourceData````.
* @param {ArrayBuffer|JSON} [params.sourceData] Source file data. Alternative to ````source````.
* @param {String} [params.sourceFormat] Format of source file/data. Always needed with ````sourceData````, but not normally needed with ````source````, because convert2xkt will determine the format automatically from the file extension of ````source````.
* @param {String} [params.metaModelDataStr] Source file data. Overrides metadata from ````metaModelSource````, ````sourceData```` and ````source````.
* @param {String} [params.metaModelSource] Path to source metaModel file. Overrides metadata from ````sourceData```` and ````source````. Overridden by ````metaModelData````.
* @param {String} [params.output] Path to destination XKT file. Directories on this path are automatically created if not existing.
* @param {Function} [params.outputXKTModel] Callback to collect the ````XKTModel```` that is internally build by this method.
* @param {Function} [params.outputXKT] Callback to collect XKT file data.
* @param {String[]} [params.includeTypes] Option to only convert objects of these types.
* @param {String[]} [params.excludeTypes] Option to never convert objects of these types.
* @param {Object} [stats] Collects conversion statistics. Statistics are attached to this object if provided.
* @param {Function} [params.outputStats] Callback to collect statistics.
* @param {Boolean} [params.rotateX=false] Whether to rotate the model 90 degrees about the X axis to make the Y axis "up", if necessary. Applies to CityJSON and LAS/LAZ models.
* @param {Boolean} [params.reuseGeometries=true] When true, will enable geometry reuse within the XKT. When false,
* will automatically "expand" all reused geometries into duplicate copies. This has the drawback of increasing the XKT
* file size (~10-30% for typical models), but can make the model more responsive in the xeokit Viewer, especially if the model
* has excessive geometry reuse. An example of excessive geometry reuse would be when a model (eg. glTF) has 4000 geometries that are
* shared amongst 2000 objects, ie. a large number of geometries with a low amount of reuse, which can present a
* pathological performance case for xeokit's underlying graphics APIs (WebGL, WebGPU etc).
* @param {Boolean} [params.includeTextures=true] Whether to convert textures. Only works for ````glTF```` models.
* @param {Boolean} [params.includeNormals=true] Whether to convert normals. When false, the parser will ignore
* geometry normals, and the modelwill rely on the xeokit ````Viewer```` to automatically generate them. This has
* the limitation that the normals will be face-aligned, and therefore the ````Viewer```` will only be able to render
* a flat-shaded non-PBR representation of the model.
* @param {Number} [params.minTileSize=200] Minimum RTC coordinate tile size. Set this to a value between 100 and 10000,
* depending on how far from the coordinate origin the model's vertex positions are; specify larger tile sizes when close
* to the origin, and smaller sizes when distant. This compensates for decreasing precision as floats get bigger.
* @param {Function} [params.log] Logging callback.
* @return {Promise<number>}
*/
function convert2xkt({
WebIFC,
configs = {},
source,
sourceData,
sourceFormat,
metaModelSource,
metaModelDataStr,
modelAABB,
output,
outputXKTModel,
outputXKT,
includeTypes,
excludeTypes,
reuseGeometries = true,
minTileSize = 200,
stats = {},
outputStats,
rotateX = false,
includeTextures = true,
includeNormals = true,
zip = false,
log = function (msg) {
}
}) {
stats.sourceFormat = "";
stats.schemaVersion = "";
stats.title = "";
stats.author = "";
stats.created = "";
stats.numMetaObjects = 0;
stats.numPropertySets = 0;
stats.numTriangles = 0;
stats.numVertices = 0;
stats.numNormals = 0;
stats.numUVs = 0;
stats.numTextures = 0;
stats.numTextureSets = 0;
stats.numObjects = 0;
stats.numGeometries = 0;
stats.sourceSize = 0;
stats.xktSize = 0;
stats.texturesSize = 0;
stats.xktVersion = "";
stats.compressionRatio = 0;
stats.conversionTime = 0;
stats.aabb = null;
function getFileExtension(fileName) {
let ext = path.extname(fileName);
if (ext.charAt(0) === ".") {
ext = ext.substring(1);
}
return ext;
}
return new Promise(function (resolve, reject) {
const _log = log;
log = (msg) => {
_log(`[convert2xkt] ${msg}`)
}
if (!source && !sourceData) {
reject("Argument expected: source or sourceData");
return;
}
if (!sourceFormat && sourceData) {
reject("Argument expected: sourceFormat is required with sourceData");
return;
}
if (!output && !outputXKTModel && !outputXKT) {
reject("Argument expected: output, outputXKTModel or outputXKT");
return;
}
if (source) {
log('Reading input file: ' + source);
}
const startTime = new Date();
const sourceConfigs = configs.sourceConfigs || {};
const ext = sourceFormat || getFileExtension(source);
log(`Input file extension: "${ext}"`);
let fileTypeConfigs = sourceConfigs[ext];
if (!fileTypeConfigs) {
log(`[WARNING] Could not find configs sourceConfigs entry for source format "${ext}". This is derived from the source file name extension. Will use internal default configs.`);
fileTypeConfigs = {};
}
function overrideOption(option1, option2) {
if (option1 !== undefined) {
return option1;
}
return option2;
}
if (!sourceData) {
try {
sourceData = fs.readFileSync(source);
} catch (err) {
reject(err);
return;
}
}
const sourceFileSizeBytes = sourceData.byteLength;
log("Input file size: " + (sourceFileSizeBytes / 1000).toFixed(2) + " kB");
if (!metaModelDataStr && metaModelSource) {
log('Reading input metadata file: ' + metaModelSource);
try {
metaModelDataStr = fs.readFileSync(metaModelSource);
} catch (err) {
reject(err);
return;
}
} else {
log(`Not embedding metadata in XKT`);
}
let metaModelJSON;
if (metaModelDataStr) {
try {
metaModelJSON = JSON.parse(metaModelDataStr);
} catch (e) {
metaModelJSON = {};
log(`Error parsing metadata JSON: ${e}`);
}
}
minTileSize = overrideOption(fileTypeConfigs.minTileSize, minTileSize);
rotateX = overrideOption(fileTypeConfigs.rotateX, rotateX);
reuseGeometries = overrideOption(fileTypeConfigs.reuseGeometries, reuseGeometries);
includeTextures = overrideOption(fileTypeConfigs.includeTextures, includeTextures);
includeNormals = overrideOption(fileTypeConfigs.includeNormals, includeNormals);
includeTypes = overrideOption(fileTypeConfigs.includeTypes, includeTypes);
excludeTypes = overrideOption(fileTypeConfigs.excludeTypes, excludeTypes);
if (reuseGeometries === false) {
log("Geometry reuse is disabled");
}
const xktModel = new XKTModel({
minTileSize,
modelAABB
});
switch (ext) {
case "json":
convert(parseCityJSONIntoXKTModel, {
data: JSON.parse(sourceData),
xktModel,
stats,
rotateX,
center: fileTypeConfigs.center,
transform: fileTypeConfigs.transform,
log
});
break;
case "glb":
sourceData = toArrayBuffer(sourceData);
convert(parseGLTFIntoXKTModel, {
data: sourceData,
reuseGeometries,
includeTextures: true,
includeNormals,
metaModelData: metaModelJSON,
xktModel,
stats,
log
});
break;
case "gltf":
sourceData = toArrayBuffer(sourceData);
const gltfBasePath = source ? path.dirname(source) : "";
convert(parseGLTFIntoXKTModel, {
baseUri: gltfBasePath,
data: sourceData,
reuseGeometries,
includeTextures: true,
includeNormals,
metaModelData: metaModelJSON,
xktModel,
stats,
log
});
break;
// case "gltf":
// const gltfJSON = JSON.parse(sourceData);
// const gltfBasePath = source ? getBasePath(source) : "";
// convert(parseGLTFIntoXKTModel, {
// baseUri: gltfBasePath,
// data: gltfJSON,
// reuseGeometries,
// includeTextures,
// includeNormals,
// metaModelData: metaModelJSON,
// xktModel,
// getAttachment: async (name) => {
// const filePath = gltfBasePath + name;
// log(`Reading attachment file: ${filePath}`);
// const buffer = fs.readFileSync(filePath);
// const arrayBuf = toArrayBuffer(buffer);
// return arrayBuf;
// },
// stats,
// log
// });
// break;
case "ifc":
convert(parseIFCIntoXKTModel, {
WebIFC,
data: sourceData,
xktModel,
wasmPath: "./",
includeTypes,
excludeTypes,
stats,
log
});
break;
case "laz":
convert(parseLASIntoXKTModel, {
data: sourceData,
xktModel,
stats,
fp64: fileTypeConfigs.fp64,
colorDepth: fileTypeConfigs.colorDepth,
center: fileTypeConfigs.center,
transform: fileTypeConfigs.transform,
skip: overrideOption(fileTypeConfigs.skip, 1),
log
});
break;
case "las":
convert(parseLASIntoXKTModel, {
data: sourceData,
xktModel,
stats,
fp64: fileTypeConfigs.fp64,
colorDepth: fileTypeConfigs.colorDepth,
center: fileTypeConfigs.center,
transform: fileTypeConfigs.transform,
skip: overrideOption(fileTypeConfigs.skip, 1),
log
});
break;
case "pcd":
convert(parsePCDIntoXKTModel, {
data: sourceData,
xktModel,
stats,
log
});
break;
case "ply":
convert(parsePLYIntoXKTModel, {
data: sourceData,
xktModel,
stats,
log
});
break;
case "stl":
convert(parseSTLIntoXKTModel, {
data: sourceData,
xktModel,
stats,
log
});
break;
default:
reject(`Error: unsupported source format: "${ext}".`);
return;
}
function convert(parser, converterParams) {
parser(converterParams).then(() => {
if (!metaModelJSON) {
log("Creating default metamodel in XKT");
xktModel.createDefaultMetaObjects();
}
log("Input file parsed OK. Building XKT document...");
xktModel.finalize().then(() => {
log("XKT document built OK. Writing to XKT file...");
const xktArrayBuffer = writeXKTModelToArrayBuffer(xktModel, metaModelJSON, stats, {zip: zip});
const xktContent = Buffer.from(xktArrayBuffer);
const targetFileSizeBytes = xktArrayBuffer.byteLength;
stats.minTileSize = minTileSize || 200;
stats.sourceSize = (sourceFileSizeBytes / 1000).toFixed(2);
stats.xktSize = (targetFileSizeBytes / 1000).toFixed(2);
stats.xktVersion = XKT_INFO.xktVersion;
stats.compressionRatio = (sourceFileSizeBytes / targetFileSizeBytes).toFixed(2);
stats.conversionTime = ((new Date() - startTime) / 1000.0).toFixed(2);
stats.aabb = xktModel.aabb;
log(`Converted to: XKT v${stats.xktVersion}`);
if (includeTypes) {
log("Include types: " + (includeTypes ? includeTypes : "(include all)"));
}
if (excludeTypes) {
log("Exclude types: " + (excludeTypes ? excludeTypes : "(exclude none)"));
}
log("XKT size: " + stats.xktSize + " kB");
log("XKT textures size: " + (stats.texturesSize / 1000).toFixed(2) + "kB");
log("Compression ratio: " + stats.compressionRatio);
log("Conversion time: " + stats.conversionTime + " s");
log("Converted metaobjects: " + stats.numMetaObjects);
log("Converted property sets: " + stats.numPropertySets);
log("Converted drawable objects: " + stats.numObjects);
log("Converted geometries: " + stats.numGeometries);
log("Converted textures: " + stats.numTextures);
log("Converted textureSets: " + stats.numTextureSets);
log("Converted triangles: " + stats.numTriangles);
log("Converted vertices: " + stats.numVertices);
log("Converted UVs: " + stats.numUVs);
log("Converted normals: " + stats.numNormals);
log("Converted tiles: " + xktModel.tilesList.length);
log("minTileSize: " + stats.minTileSize);
if (output) {
const outputDir = path.dirname(output);
if (outputDir !== "" && !fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, {recursive: true});
}
log('Writing XKT file: ' + output);
fs.writeFileSync(output, xktContent);
}
if (outputXKTModel) {
outputXKTModel(xktModel);
}
if (outputXKT) {
outputXKT(xktContent);
}
if (outputStats) {
outputStats(stats);
}
resolve();
});
}, (err) => {
reject(err);
});
}
});
}
export {convert2xkt};