/** * LDraw object packer * * Usage: * * - Download official parts library from LDraw.org and unzip in a directory (e.g. ldraw/) * * - Download your desired model file and place in the ldraw/models/ subfolder. * * - Place this script also in ldraw/ * * - Issue command 'node packLDrawModel models/' * * The packed object will be in ldraw/models/_Packed.mpd and will contain all the object subtree as embedded files. * * */ const ldrawPath = './'; const materialsFileName = 'LDConfig.ldr'; import fs from 'fs'; import path from 'path'; if ( process.argv.length !== 3 ) { console.log( 'Usage: node packLDrawModel ' ); process.exit( 0 ); } const fileName = process.argv[ 2 ]; const materialsFilePath = path.join( ldrawPath, materialsFileName ); console.log( 'Loading materials file "' + materialsFilePath + '"...' ); const materialsContent = fs.readFileSync( materialsFilePath, { encoding: 'utf8' } ); console.log( 'Packing "' + fileName + '"...' ); const objectsPaths = []; const objectsContents = []; const pathMap = {}; const listOfNotFound = []; // Parse object tree parseObject( fileName, true ); // Check if previously files not found are found now // (if so, probably they were already embedded) let someNotFound = false; for ( let i = 0; i < listOfNotFound.length; i ++ ) { if ( ! pathMap[ listOfNotFound[ i ] ] ) { someNotFound = true; console.log( 'Error: File object not found: "' + fileName + '".' ); } } if ( someNotFound ) { console.log( 'Some files were not found, aborting.' ); process.exit( - 1 ); } // Obtain packed content let packedContent = materialsContent + '\n'; for ( let i = objectsPaths.length - 1; i >= 0; i -- ) { packedContent += objectsContents[ i ]; } packedContent += '\n'; // Save output file const outPath = fileName + '_Packed.mpd'; console.log( 'Writing "' + outPath + '"...' ); fs.writeFileSync( outPath, packedContent ); console.log( 'Done.' ); // function parseObject( fileName, isRoot ) { // Returns the located path for fileName or null if not found console.log( 'Adding "' + fileName + '".' ); const originalFileName = fileName; let prefix = ''; let objectContent = null; for ( let attempt = 0; attempt < 2; attempt ++ ) { prefix = ''; if ( attempt === 1 ) { fileName = fileName.toLowerCase(); } if ( fileName.startsWith( '48/' ) ) { prefix = 'p/'; } else if ( fileName.startsWith( 's/' ) ) { prefix = 'parts/'; } let absoluteObjectPath = path.join( ldrawPath, fileName ); try { objectContent = fs.readFileSync( absoluteObjectPath, { encoding: 'utf8' } ); break; } catch ( e ) { prefix = 'parts/'; absoluteObjectPath = path.join( ldrawPath, prefix, fileName ); try { objectContent = fs.readFileSync( absoluteObjectPath, { encoding: 'utf8' } ); break; } catch ( e ) { prefix = 'p/'; absoluteObjectPath = path.join( ldrawPath, prefix, fileName ); try { objectContent = fs.readFileSync( absoluteObjectPath, { encoding: 'utf8' } ); break; } catch ( e ) { try { prefix = 'models/'; absoluteObjectPath = path.join( ldrawPath, prefix, fileName ); objectContent = fs.readFileSync( absoluteObjectPath, { encoding: 'utf8' } ); break; } catch ( e ) { if ( attempt === 1 ) { // The file has not been found, add to list of not found listOfNotFound.push( originalFileName ); } } } } } } const objectPath = path.join( prefix, fileName ).trim().replace( /\\/g, '/' ); if ( ! objectContent ) { // File was not found, but could be a referenced embedded file. return null; } if ( objectContent.indexOf( '\r\n' ) !== - 1 ) { // This is faster than String.split with regex that splits on both objectContent = objectContent.replace( /\r\n/g, '\n' ); } let processedObjectContent = isRoot ? '' : '0 FILE ' + objectPath + '\n'; const lines = objectContent.split( '\n' ); for ( let i = 0, n = lines.length; i < n; i ++ ) { let line = lines[ i ]; let lineLength = line.length; // Skip spaces/tabs let charIndex = 0; while ( ( line.charAt( charIndex ) === ' ' || line.charAt( charIndex ) === '\t' ) && charIndex < lineLength ) { charIndex ++; } line = line.substring( charIndex ); lineLength = line.length; charIndex = 0; if ( line.startsWith( '0 FILE ' ) ) { if ( i === 0 ) { // Ignore first line FILE meta directive continue; } // Embedded object was found, add to path map const subobjectFileName = line.substring( charIndex ).trim().replace( /\\/g, '/' ); if ( subobjectFileName ) { // Find name in path cache const subobjectPath = pathMap[ subobjectFileName ]; if ( ! subobjectPath ) { pathMap[ subobjectFileName ] = subobjectFileName; } } } if ( line.startsWith( '1 ' ) ) { // Subobject, add it charIndex = 2; // Skip material, position and transform for ( let token = 0; token < 13 && charIndex < lineLength; token ++ ) { // Skip token while ( line.charAt( charIndex ) !== ' ' && line.charAt( charIndex ) !== '\t' && charIndex < lineLength ) { charIndex ++; } // Skip spaces/tabs while ( ( line.charAt( charIndex ) === ' ' || line.charAt( charIndex ) === '\t' ) && charIndex < lineLength ) { charIndex ++; } } const subobjectFileName = line.substring( charIndex ).trim().replace( /\\/g, '/' ); if ( subobjectFileName ) { // Find name in path cache let subobjectPath = pathMap[ subobjectFileName ]; if ( ! subobjectPath ) { // Add new object subobjectPath = parseObject( subobjectFileName ); } pathMap[ subobjectFileName ] = subobjectPath ? subobjectPath : subobjectFileName; processedObjectContent += line.substring( 0, charIndex ) + pathMap[ subobjectFileName ] + '\n'; } } else { processedObjectContent += line + '\n'; } } if ( objectsPaths.indexOf( objectPath ) < 0 ) { objectsPaths.push( objectPath ); objectsContents.push( processedObjectContent ); } return objectPath; }