root2cad
Version:
CERN ROOT geometry export to CAD
304 lines (238 loc) • 10.1 kB
JavaScript
import * as jsroot from "jsroot";
import * as geom from "jsroot/geom"
import { program } from "commander";
import * as fs from "fs";
import { GLTFExporter } from './GLTFExporter.js';
import thisPackage from './package.json' assert { type: 'json' };
const description = `
CERN ROOT geometry converter to GLTF (CAD) format
Examples:
convert geometry in a root file:
xvfb-run root2cad file.root geo_obj_name -o output.gltf
convert subpart of geometry:
xvfb-run root2cad file.root geo_obj_name subpart_name -o out.gltf
convert from gdml (CERN ROOT has to be installed):
root -e 'TGeoManager::Import("my.gdml")->Export("my.root")'
xvfb-run root2cad my.root default -o my.gltf
convert to other cad formats (install assimp):
assimp export drich.gltf drich.obj
list file contents:
xvfb-run root2cad --ls file.root
list geometry hierarchy:
xvfb-run root2cad --ls --ls-depth=1 file.root geo_obj_name
`;
let gVerbose = false;
function printTree(nameOrFile) {
if (typeof nameOrFile === 'string' || nameOrFile instanceof String) {
// it's a string
jsroot.openFile(nameOrFile).then(file => printTree(file));
}
else {
let file = nameOrFile; // it is file
// console.dir(file)
if(gVerbose) console.log(`List of objects in '${file.fFileName}' `);
console.log(`'Name' [type] description:`);
// Step 1: Find max lengths
let maxFNameLength = 0;
let maxFClassNameLength = 0;
let maxFTitleLength = 0;
for (let key of file.fKeys) {
maxFNameLength = Math.max(maxFNameLength, key.fName.length);
maxFClassNameLength = Math.max(maxFClassNameLength, key.fClassName.length);
maxFTitleLength = Math.max(maxFTitleLength, key.fTitle.length);
}
// Step 2: Print headers
console.log(
`${"Name".padEnd(maxFNameLength)} |${"Type".padEnd(maxFClassNameLength)}| ${"Description".padEnd(maxFTitleLength)}`
);
console.log("-".repeat(maxFNameLength + maxFClassNameLength + maxFTitleLength + 4)); // 4 for spaces and brackets
// Step 2: Log with padding
for (let key of file.fKeys) {
const fNamePadded = key.fName.padEnd(maxFNameLength);
const fClassNamePadded = key.fClassName.padEnd(maxFClassNameLength);
const fTitlePadded = key.fTitle.padEnd(maxFTitleLength);
console.log(`${fNamePadded} |${fClassNamePadded}| ${fTitlePadded}`);
}
}
}
function printGeometry(fileName, objectName, listDepth, how="volume") {
jsroot.openFile(fileName)
.then(file => file.readObject(objectName))
.then(rootObject => {
// Check if such object exists
if(!rootObject) {
console.error(`NO OBJECT '${objectName}' IN FILE!`);
printTree(tree);
return 1;
}
if(gVerbose) console.log(`Opened '${objectName}' object`);
if(how==="volume"){
printVolumeRecursive(rootObject.fNodes.arr[0], listDepth, 0, "");
}
else {
printNodeRecursive(rootObject.fNodes.arr[0], listDepth, 0, "");
}
})
.catch(error => {
console.log(error.message);
});
}
/// Prints geometry structure
function printNodeRecursive(node, maxLevel=0, level=0, path="") {
const nodeName = node.fName;
console.log(path + "/" + nodeName);
if(level>=maxLevel) return;
if (node.fVolume.fNodes) {
for (let j = 0; j < node.fVolume.fNodes.arr.length; j++) {
const childNode = node.fVolume.fNodes.arr[j];
printNodeRecursive(childNode, maxLevel, level + 1, path + "/" + nodeName);
}
}
}
function printVolumeRecursive(node, maxLevel=0, level=0, path="") {
let volume = node._typename === "TGeoManager"? node.fMasterVolume : node.fVolume;
const nodeName = node.fName;
const volumeName = volume.fName;
console.log(path + "/" + volumeName);
if(level>=maxLevel) return;
if (node.fVolume.fNodes) {
for (const childNode of node.fVolume.fNodes.arr) {
printVolumeRecursive(childNode, maxLevel, level + 1, path + "/" + volumeName);
}
}
}
/// Find geometry with name
function findNodeByName(node, findName) {
let volume = node._typename === "TGeoManager"? node.fMasterVolume : node.fVolume;
if(findName===volume.fName) {
return node;
}
if (volume.fNodes) {
for (const childNode of volume.fNodes.arr) {
const result = findNodeByName(childNode, findName);
if(result) {
return result;
}
}
}
return null;
}
function exportFile(fileName, objectName, outFileName, subGeoName=null) {
jsroot.openFile(fileName)
.then(file => file.readObject(objectName))
.then(rootObject => {
// Check if such object exists
if(!rootObject) {
console.error(`NO OBJECT '${objectName}' IN FILE!`);
printTree(tree);
return 1;
}
// Open success! (reporing)
console.log(`Opened '${objectName}' object`);
// Exctracting geometry
let rootGeo = rootObject;
// Do wee need to look for subcomponent?
if(subGeoName) {
console.log(`Searching for '${subGeoName}`)
rootGeo = findNodeByName(rootObject, subGeoName);
// Check if we found subcomponent
if(!rootGeo) {
console.error(`NO SUB-COMPONENT '${subGeoName}' in geometry! Use --ls command to see subcomponents`);
return 1;
}
console.log(`Found ${subGeoName}`);
}
// Load jsroot geometry package
console.log(`Converting to THREEJS`);
// Create threejs from root geometry
let obj3d = geom.build(rootGeo, { numfaces: 5000000, numnodes: 50000, dflt_colors: true, vislevel: 4});
// EXPORT!
console.log("Converting to GLTF format");
const gltfExporter = new GLTFExporter();
let exportOpts = {};
gltfExporter.parse(obj3d, function(gltf) {
console.log(`Conversion done! Saving to file: ${outFileName}`);
// Save to file
try {
const gltfText = JSON.stringify(gltf);
fs.writeFileSync(outFileName, gltfText, {encoding:'utf8'});
console.log("Done.");
} catch(err) {
console.error(err);
}
}, exportOpts);
})
.catch(error => {
console.error('Error during GLTF export:');
console.error(error.message);
});
}
function main() {
program
.name('root2cad')
.description(description)
.option('-o, --output <string>', 'Output file name. "exported.gltf" if not set')
.option('--ls', 'Lists all objects in file or geometry (same as --ls-vol)')
.option('--ls-vol', 'Lists geometry hierarchy of VOLUME names. See also --list-depth')
.option('--ls-node', 'Lists geometry hierarchy of NODE names. See also --list-depth')
.option('--ls-depth <int>', 'Works with --list, defines the level to print. Default 0')
.option('-v, --verbose', 'Use verbose output')
.version(thisPackage.version)
//.version('1.1.1')
.argument('[file]', 'File name to open (CERN ROOT files)')
.argument('[object]', 'Geometry object name in ROOT file to open')
.argument('[volname]', 'Volume name in geometry hierarchy')
program.parse();
const options = program.opts();
const listCommand = options.ls || options.lsVol || options.lsNode;
const listDepth = options.lsDepth ? options.lsDepth : 0;
const outFileName = options.output ? options.output : "exported.gltf"
gVerbose = !!options.verbose;
if(gVerbose) {
console.log("Verbose output is ON");
}
// console.dir(program);
// List something
if(listCommand) {
console.log("Listing...");
// List ROOT file structure
if(program.args.length === 1) {
printTree(program.args[0]);
return 0;
}
// List of geometry structure
if(program.args.length === 2) {
if(gVerbose) {
console.log(`Listing '${program.args[0]}' geometry contents of '${program.args[1]}'`);
console.log(`List depth is: ${listDepth} (controlled with --ls-depth flag)`);
}
let how = options.ls || options.lsVol? "volume": "node";
printGeometry(program.args[0], program.args[1], listDepth, how);
return 0;
}
console.log("Incorrect 'list' parameters. See --help for info and examples");
return 0;
}
// Do conversion
if(program.args.length >= 2) {
if(program.args.length === 2) {
console.log(`Converting ${program.args[0]} / ${program.args[1]} to ${outFileName}`);
exportFile(program.args[0], program.args[1], outFileName);
} else {
console.log(`Converting sub-geometry ${program.args[2]} in ${program.args[0]} / ${program.args[1]} to ${outFileName}`);
exportFile(program.args[0], program.args[1], outFileName, program.args[2]);
}
return 0;
}
// Print help
program.help();
return 1;
}
// MAIN HERE
try {
main();
}
catch(err) {
console.error(err);
}