@xeokit/xeokit-convert
Version:
JavaScript utilities to create .XKT files
341 lines (299 loc) • 12.2 kB
JavaScript
// Set up polyfills and environment
import { TextEncoder, TextDecoder } from 'node:util';
global.TextEncoder = TextEncoder;
global.TextDecoder = TextDecoder;
import '@loaders.gl/polyfills';
import { installFilePolyfills } from '@loaders.gl/polyfills';
installFilePolyfills();
// Import dependencies
import WebIFC from "web-ifc";
import * as fs from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { dirname, resolve } from 'node:path';
import { Command } from 'commander';
// Import converter functionality
import { convert2xkt } from './src/convert2xkt.js';
import defaultConfigs from './convert2xkt.conf.js';
import { XKT_INFO } from "./src/XKT_INFO.js";
// CLI functionality
/**
* Creates and configures the command line interface
* @param {Object} packageInfo - The package.json content
* @returns {Command} The configured Command object
*/
function setupCLI(packageInfo) {
const program = new Command();
program.version(packageInfo.version, '-v, --version');
program
.option('-c, --configs [file]', 'optional path to JSON configs file; overrides convert2xkt.conf.js')
.option('-s, --source [file]', 'path to source file')
.option('-a, --sourcemanifest [file]', 'path to source manifest file (for converting split file output from ifcgltf -s)')
.option('-f, --format [string]', 'source file format (optional); supported formats are gltf, ifc, laz, las, pcd, ply, stl and cityjson')
.option('-m, --metamodel [file]', 'path to source metamodel JSON file (optional)')
.option('-i, --include [types]', 'only convert these types (optional)')
.option('-x, --exclude [types]', 'never convert these types (optional)')
.option('-r, --rotatex', 'rotate model 90 degrees about X axis (for las and cityjson)')
.option('-g, --disablegeoreuse', 'disable geometry reuse (optional)')
.option('-z, --minTileSize [number]', 'minimum diagonal tile size (optional, default 500)')
.option('-t, --disabletextures', 'ignore textures (optional)')
.option('-n, --disablenormals', 'ignore normals (optional)')
.option('-b, --compressBuffers', 'compress buffers (optional)')
.option('-e, --maxIndicesForEdge [number]', 'max number of idicies in a mesh (effectivly triangles) above edges are not calculated (optional, default 100000)')
.option('-o, --output [file]', 'path to target .xkt file when -s option given, or JSON manifest for multiple .xkt files when source manifest file given with -a; creates directories on path automatically if not existing')
.option('-l, --log', 'enable logging (optional)');
program.on('--help', () => {
console.log(`\n\nXKT version: ${XKT_INFO.xktVersion}`);
});
return program;
}
/**
* Validates command line options
* @param {Object} options - The command line options
* @param {Command} program - The commander program object
*/
function validateOptions(options, program) {
if (options.source === undefined && options.sourcemanifest === undefined) {
console.error('[convert2xkt] [ERROR]: Please specify path to source file or manifest.');
program.help();
process.exit(1);
}
if (options.source !== undefined && options.sourcemanifest !== undefined) {
console.error('[convert2xkt] [ERROR]: Can\'t specify path to source file AND manifest - only one of these params allowed.');
program.help();
process.exit(1);
}
if (options.output === undefined) {
console.error('[convert2xkt] [ERROR]: Please specify target xkt file path.');
program.help();
process.exit(1);
}
}
/**
* Creates a logging function
* @param {Object} options - The command line options
* @returns {Function} Logging function
*/
function createLogger(options) {
return function log(msg) {
if (options.log) {
console.log(msg);
}
};
}
/**
* Gets the file extension from a file path
* @param {string} fileName - The file path
* @returns {string} The file extension without dot
*/
function getFileExtension(fileName) {
let ext = path.extname(fileName);
if (ext.charAt(0) === ".") {
ext = ext.substring(1);
}
return ext;
}
/**
* Gets the file name without extension
* @param {string} filePath - The file path
* @returns {string} The file name without extension
*/
function getFileNameWithoutExtension(filePath) {
const baseName = path.basename(filePath);
const fileNameWithoutExtension = path.parse(baseName).name;
return fileNameWithoutExtension;
}
/**
* Loads configs from file or uses defaults
* @param {Object} options - Command line options
* @param {Object} defaultConfigs - Default configs object
* @param {Function} log - Logging function
* @returns {Object} The loaded configs
*/
function loadConfigs(options, defaultConfigs, log) {
let configs = defaultConfigs;
if (options.configs !== undefined) {
log(`[convert2xkt] Using JSON configs file: ${options.configs}`);
try {
let configsData = fs.readFileSync(options.configs);
configs = JSON.parse(configsData);
} catch (e) {
console.error(`[convert2xkt] [ERROR]: Failed to load custom configs file (specified with -c or --configs) - ${e}`);
process.exit(1);
}
} else {
log(`[convert2xkt] Using configs in ./convert2xkt.conf.js`);
}
configs.sourceConfigs ||= {};
return configs;
}
/**
* Main convert function for handling both single files and manifests
* @param {Object} params - Parameters for conversion
* @param {Object} params.options - Command line options
* @param {Object} params.configs - Configuration object
* @param {Function} params.log - Logging function
* @param {Object} params.WebIFC - WebIFC implementation
* @param {Function} params.convert2xkt - The convert2xkt function
* @param {Object} params.packageInfo - Package information
*/
async function mainConvert({ options, configs, log, WebIFC, convert2xkt, packageInfo }) {
log(`[convert2xkt] Running convert2xkt v${packageInfo.version}...`);
configs = loadConfigs(options, configs, log);
if (options.sourcemanifest) {
await convertManifest({ options, configs, log, WebIFC, convert2xkt, packageInfo });
} else {
await convertSingleFile({ options, configs, log, WebIFC, convert2xkt });
}
}
/**
* Convert files specified in a manifest
* @param {Object} params - Parameters for conversion
*/
async function convertManifest({ options, configs, log, WebIFC, convert2xkt, packageInfo }) {
log(`[convert2xkt] Converting glTF files in manifest ${options.sourcemanifest}...`);
let manifestData = fs.readFileSync(options.sourcemanifest);
let manifest = JSON.parse(manifestData);
if (!manifest.gltfOutFiles) {
console.error(`[convert2xkt] [ERROR]: Input manifest invalid - missing field: gltfOutFiles`);
process.exit(1);
}
const numInputFiles = manifest.gltfOutFiles.length;
if (numInputFiles === 0) {
console.error(`[convert2xkt] [ERROR]: Input manifest invalid - gltfOutFiles is zero length`);
process.exit(1);
}
const outputDir = path.dirname(options.output);
if (outputDir !== "" && !fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
function formatDate(date) {
return date.toISOString();
}
const xktManifest = {
inputFile: options.sourcemanifest,
converterApplication: "convert2xkt",
converterApplicationVersion: `v${packageInfo.version}`,
conversionDate: formatDate(new Date()),
outputDir,
xktFiles: []
};
const sourceConfigs = configs.sourceConfigs || {};
const formatConfig = sourceConfigs["gltf"] || configs["glb"] || {};
const externalMetadata = (!!formatConfig.externalMetadata);
if (externalMetadata) {
xktManifest.metaModelFiles = [];
for (let i = 0, len = manifest.metadataOutFiles.length; i < len; i++) {
const metadataSource = manifest.metadataOutFiles[i];
xktManifest.metaModelFiles.push(path.basename(metadataSource));
}
}
let i = 0;
const convertNextFile = () => {
const source = manifest.gltfOutFiles[i];
const metaModelSource = (i < manifest.metadataOutFiles.length) ? manifest.metadataOutFiles[i] : null;
const outputFileName = getFileNameWithoutExtension(source);
const outputFileNameXKT = `${outputFileName}.xkt`;
const ext = getFileExtension(source);
let modelAABB;
return convert2xkt({
WebIFC,
configs,
source,
format: ext,
metaModelSource: (!externalMetadata) ? metaModelSource : null,
modelAABB,
output: path.join(outputDir, outputFileNameXKT),
includeTypes: options.include ? options.include.slice(",") : null,
excludeTypes: options.exclude ? options.exclude.slice(",") : null,
rotateX: options.rotatex,
reuseGeometries: (options.disablegeoreuse !== true),
minTileSize: options.minTileSize,
includeTextures: !options.disabletextures,
includeNormals: !options.disablenormals,
zip: options.compressBuffers,
maxIndicesForEdge: options.maxIndicesForEdge,
log
}).then(() => {
i++;
log(`[convert2xkt] Converted model${i}.xkt (${i} of ${numInputFiles})`);
xktManifest.xktFiles.push(outputFileNameXKT);
if (i === numInputFiles) {
fs.writeFileSync(options.output, JSON.stringify(xktManifest));
log(`[convert2xkt] Done.`);
process.exit(0);
} else {
return convertNextFile();
}
});
};
try {
await convertNextFile();
} catch (err) {
console.error(`[convert2xkt] [ERROR]: ${err}`);
process.exit(1);
}
}
/**
* Convert a single file
* @param {Object} params - Parameters for conversion
*/
async function convertSingleFile({ options, configs, log, WebIFC, convert2xkt }) {
if (options.output) {
const outputDir = path.dirname(options.output);
if (outputDir !== "" && !fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
}
log(`[convert2xkt] Converting single input file ${options.source}...`);
try {
await convert2xkt({
WebIFC,
configs,
source: options.source,
format: options.format,
metaModelSource: options.metamodel,
output: options.output,
includeTypes: options.include ? options.include.slice(",") : null,
excludeTypes: options.exclude ? options.exclude.slice(",") : null,
rotateX: options.rotatex,
reuseGeometries: (options.disablegeoreuse !== true),
minTileSize: options.minTileSize,
includeTextures: !options.disabletextures,
includeNormals: !options.disablenormals,
maxIndicesForEdge: options.maxIndicesForEdge,
zip: options.compressBuffers,
log
});
log(`[convert2xkt] Done.`);
process.exit(0);
} catch (err) {
console.error(`[convert2xkt] [ERROR]: ${err}`);
process.exit(1);
}
}
// Setup environment
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageInfo = JSON.parse(fs.readFileSync(path.resolve(__dirname, './package.json'), 'utf8'));
// Setup CLI
const program = setupCLI(packageInfo);
program.parse(process.argv);
const options = program.opts();
// Validate options
validateOptions(options, program);
// Create logger
const log = createLogger(options);
// Run conversion
mainConvert({
options,
configs: defaultConfigs,
log,
WebIFC,
convert2xkt,
packageInfo
}).catch(err => {
console.error('Error:', err);
process.exit(1);
});