import _ from './util' ; const blackColor = { name : 'Black' , color : '#05131D' , edge : '#595959' , alpha : 0 } ; const api = { LDrawPath : '../ldraw/' , async loadRemotePart ( url , progressCallback ) { return await loadPart ( url , null , progressCallback ) ; } , async loadModelContent ( content , fn , progressCallback ) { function fixPartColors ( part ) { if ( part . parts ) { part . parts . forEach ( innerPart => { if ( ! ( innerPart . colorCode in api . colorTable ) ) { innerPart . colorCode = 0 ; } } ) ; } } const part = await loadPart ( null , content , progressCallback ) ; if ( part && fn && part . filename == null ) { part . filename = fn ; api . partDictionary [ fn ] = part ; if ( api . partDictionary . null ) { delete api . partDictionary . null ; } } fixPartColors ( part ) ; Object . values ( api . partDictionary ) . filter ( innerPart => innerPart && innerPart . isSubModel ) . forEach ( fixPartColors ) ; return part ; } , async loadPartContent ( content , progressCallback ) { return await loadPart ( null , content , progressCallback ) ; } , async loadLDConfig ( url = api . LDrawPath + 'LDConfig.ldr' ) { let content = await fetch ( url ) ; if ( ! content || ! content . ok ) { return { } ; } content = await content . text ( ) || '' ; const colors = { } ; const lineList = content . split ( '\n' ) ; for ( let i = 0 ; i < lineList . length ; i ++ ) { const line = lineList [ i ] . trim ( ) . replace ( /\s\s+/g , ' ' ) . split ( ' ' ) ; if ( line && line [ 1 ] === '!COLOUR' ) { const alpha = line [ 10 ] ? parseInt ( line [ 10 ] , 10 ) : 0 ; colors [ line [ 4 ] ] = { name : line [ 2 ] . split ( '' ) . join ( '' ) , color : line [ 6 ] , edge : line [ 8 ] , alpha , rgba : _ . color . toVec4 ( line [ 6 ] , alpha ) , edgeRgba : _ . color . toVec4 ( line [ 8 ] , 0 ) , } ; } } api . colorTable = colors ; return colors ; } , partDictionary : { } , setPartDictionary ( dict ) { api . partDictionary = dict ; } , missingParts : { } , studFaceColorCode : 987 , colorTable : { } , setColorTable ( dict ) { api . colorTable = dict ; return dict ; } , customColorTable : { } , setCustomColorTable ( dict ) { api . customColorTable = dict ; return dict ; } , getColor ( colorCode , type = 'color' ) { if ( colorCode == null ) { return null ; } else if ( colorCode in api . customColorTable && api . customColorTable [ colorCode ] [ type ] ) { return api . customColorTable [ colorCode ] [ type ] ; } else if ( colorCode in api . colorTable ) { return api . colorTable [ colorCode ] [ type ] ; } else if ( typeof colorCode === 'string' && colorCode . startsWith ( '#' ) && ( type === 'color' || type === 'name' ) ) { return colorCode ; } return blackColor [ type || 'color' ] ; } , isValidColor ( colorCode ) { return typeof colorCode === 'string' || ( typeof colorCode === 'number' && colorCode >= 0 ) ; } , model : { isSubmodel ( filename ) { return api . partDictionary [ filename ] . isSubModel ; } , removeMissingParts ( ) { function removeOnePartFromPart ( part , missingFilename ) { const indicesToDelete = [ ] ; part . parts . forEach ( ( innerPart , idx ) => { if ( innerPart . filename === missingFilename ) { indicesToDelete . push ( idx ) ; } } ) ; indicesToDelete . reverse ( ) ; indicesToDelete . forEach ( idx => { removeOnePart ( part , idx ) ; } ) ; } function removeOnePart ( part , partIdxToRemove ) { part . parts . splice ( partIdxToRemove , 1 ) ; if ( part . steps ) { part . steps . forEach ( step => { const newParts = [ ] ; step . parts . forEach ( partId => { if ( partId < partIdxToRemove ) { newParts . push ( partId ) ; } else if ( partId > partIdxToRemove ) { newParts . push ( partId - 1 ) ; } } ) ; step . parts = newParts ; } ) ; part . steps = part . steps . filter ( step => step . parts . length ) ; } } Object . keys ( api . missingParts ) . forEach ( missingFilename => { Object . values ( api . partDictionary ) . forEach ( part => { if ( part . parts && part . parts . length ) { removeOnePartFromPart ( part , missingFilename ) ; } } ) ; } ) ; api . missingParts = { } ; } , get : { partCount ( model ) { if ( ! model || ! Array . isArray ( model . parts ) || model . parts . length <= 0 ) { return 0 ; } return model . parts . reduce ( ( acc , p ) => { if ( ! p || ! p . filename || ! api . partDictionary [ p . filename ] ) { return acc ; } p = api . partDictionary [ p . filename ] ; return ( p . isSubModel ? p . parts . length : 1 ) + acc ; } , 0 ) ; } , abstractPart ( filename ) { return api . partDictionary [ filename ] ; } , partFromID ( partID , filename ) { return ( api . partDictionary [ filename ] . parts || [ ] ) [ partID ] ; } , submodels ( model ) { return ( model . parts || [ ] ) . map ( p => { p = api . partDictionary [ p . filename ] ; return p . isSubModel ? p : null ; } ) . filter ( p => p != null ) ; } , } , } , getAction : { partColor ( opts ) { return actionBuilder ( opts . filename , opts . partID , 'colorCode' , opts . color ) ; } , matrix ( opts ) { return actionBuilder ( opts . filename , opts . partID , 'matrix' , opts . matrix ) ; } , filename ( opts ) { return actionBuilder ( opts . filename , opts . partID , 'filename' , opts . newFilename ) ; } , addPart ( opts ) { const root = api . partDictionary ; const idx = root [ opts . filename ] . parts . length ; const path = `/${ opts . filename }/parts/${ idx }` ; return { redo : [ { root , op : 'add' , path , value : opts . part } ] , undo : [ { root , op : 'remove' , path } ] , } ; } , removePart ( opts ) { const root = api . partDictionary ; const part = root [ opts . filename ] . parts [ opts . partID ] ; const path = `/${ opts . filename }/parts/${ opts . partID }` ; return { redo : [ { root , op : 'remove' , path } ] , undo : [ { root , op : 'add' , path , value : part } ] , } ; } , } , } ; function actionBuilder ( filename , partID , property , newValue ) { const root = api . partDictionary , op = 'replace' ; const originalValue = root [ filename ] . parts [ partID ] [ property ] ; const path = `/${ filename }/parts/${ partID }/${ property }` ; return { redo : [ { root , op , path , value : newValue } ] , undo : [ { root , op , path , value : originalValue } ] , } ; } async function requestPart ( fn ) { if ( ! fn || typeof fn !== 'string' ) { return '' ; } fn = fn . replace ( /\\/g , '/' ) . toLowerCase ( ) ; const qualifiedFn = fn . startsWith ( './' ) ? fn : api . LDrawPath + 'parts/' + fn ; let resp = await fetch ( qualifiedFn ) ; if ( resp == null || ! resp . ok ) { let pathsToTry ; if ( fn . startsWith ( '8/' ) || fn . startsWith ( '48/' ) ) { pathsToTry = [ 'p/' , 'parts/' , 'constraints/' , 'models/' ] ; } else if ( fn . endsWith ( 'mpd' ) || fn . endsWith ( 'ldr' ) ) { pathsToTry = [ 'models/' , 'parts/' , 'p/' , 'constraints/' ] ; } else { pathsToTry = [ 'parts/' , 'p/' , 'constraints/' , 'models/' ] ; } for ( let i = 0 ; i < pathsToTry . length ; i ++ ) { if ( resp == null || ! resp . ok ) { resp = await fetch ( api . LDrawPath + pathsToTry [ i ] + fn ) ; } } } if ( resp == null || ! resp . ok ) { console . log ( `   *** FAILED TO LOAD: ${ fn }` ) ; return null ; } return await resp . text ( ) ; } const unloadedSubModels = { } ; function forceBlack ( colorCode , abstractPartName , partName ) { partName = String ( partName ) . toLowerCase ( ) ; abstractPartName = String ( abstractPartName ) . toLowerCase ( ) ; if ( partName === '4-4cyli.dat' ) { if ( abstractPartName === 'stud.dat' || abstractPartName === 'stud2.dat' || abstractPartName === 'stud2a.dat' || abstractPartName === 'stod3.dat' ) { return api . studFaceColorCode ; } } return colorCode ; } function parseFloatList ( a ) { for ( let i = 0 ; i < a . length ; i ++ ) { a [ i ] = parseFloat ( a [ i ] ) ; } return a ; } function parseComment ( abstractPart , line ) { const command = line [ 1 ] ; if ( command === 'FILE' ) { } else if ( command === 'ROTSTEP' || command === 'STEP' ) { parseStep ( abstractPart , command , line ) ; } else if ( command === '!LEOCAD' ) { if ( line . length >= 4 ) { const annotationKey1 = line [ 2 ] ; const annotationKey2 = line [ 3 ] ; const annotationValue = line . slice ( 4 ) . join ( ' ' ) ; if ( annotationKey1 === 'MODEL' ) { switch ( annotationKey2 ) { case 'AUTHOR' : abstractPart . author = annotationValue ; break ; case 'DESCRIPTION' : abstractPart . description = annotationValue ; break ; } } } } } function parseStep ( abstractPart , command , line ) { if ( abstractPart . steps == null ) { abstractPart . steps = [ ] ; abstractPart . steps . lastPart = 0 ; } if ( abstractPart . parts ) { let rotation = null ; if ( command === 'ROTSTEP' ) { rotation = { x : parseFloat ( line [ 2 ] ) , y : parseFloat ( line [ 3 ] ) , z : parseFloat ( line [ 4 ] ) , type : line [ 5 ] , } ; } abstractPart . steps . push ( { parts : abstractPart . parts . slice ( abstractPart . steps . lastPart ) . map ( ( v , i ) => i + abstractPart . steps . lastPart ) , rotation : rotation , } ) ; abstractPart . steps . lastPart = abstractPart . parts . length ; } } function parseColorCode ( code ) { if ( typeof code === 'string' && code . includes ( 'x' ) ) { return '#' + code . slice ( - 6 ) ; } code = parseInt ( code , 10 ) ; return ( code === 16 || code === 24 ) ? null : code ; } async function parsePart ( abstractPartParent , line ) { const filename = line . slice ( 14 ) . join ( ' ' ) ; await loadPart ( filename ) ; const newPart = { filename , matrix : parseFloatList ( line . slice ( 2 , 14 ) ) , } ; let colorCode = parseColorCode ( line [ 1 ] ) ; colorCode = forceBlack ( colorCode , abstractPartParent . filename , filename ) ; if ( colorCode != null ) { newPart . colorCode = colorCode ; } if ( ! abstractPartParent . parts ) { abstractPartParent . parts = [ ] ; } abstractPartParent . parts . push ( newPart ) ; } function parseLine ( abstractPart , line ) { if ( ! abstractPart . primitives ) { abstractPart . primitives = [ ] ; } abstractPart . primitives . push ( [ parseFloat ( line [ 2 ] ) , parseFloat ( line [ 3 ] ) , parseFloat ( line [ 4 ] ) , parseFloat ( line [ 5 ] ) , parseFloat ( line [ 6 ] ) , parseFloat ( line [ 7 ] ) , ] ) ; } function parseTriangle ( abstractPart , line ) { let points = [ parseFloat ( line [ 2 ] ) , parseFloat ( line [ 3 ] ) , parseFloat ( line [ 4 ] ) , parseFloat ( line [ 5 ] ) , parseFloat ( line [ 6 ] ) , parseFloat ( line [ 7 ] ) , parseFloat ( line [ 8 ] ) , parseFloat ( line [ 9 ] ) , parseFloat ( line [ 10 ] ) , ] ; if ( parseColorCode ( line [ 1 ] ) != null ) { points = { p : points , c : parseColorCode ( line [ 1 ] ) } ; } if ( ! abstractPart . primitives ) { abstractPart . primitives = [ ] ; } abstractPart . primitives . push ( points ) ; } function parseQuad ( abstractPart , line ) { let points = [ parseFloat ( line [ 2 ] ) , parseFloat ( line [ 3 ] ) , parseFloat ( line [ 4 ] ) , parseFloat ( line [ 5 ] ) , parseFloat ( line [ 6 ] ) , parseFloat ( line [ 7 ] ) , parseFloat ( line [ 8 ] ) , parseFloat ( line [ 9 ] ) , parseFloat ( line [ 10 ] ) , parseFloat ( line [ 11 ] ) , parseFloat ( line [ 12 ] ) , parseFloat ( line [ 13 ] ) , ] ; if ( parseColorCode ( line [ 1 ] ) != null ) { points = { p : points , c : parseColorCode ( line [ 1 ] ) } ; } if ( ! abstractPart . primitives ) { abstractPart . primitives = [ ] ; } abstractPart . primitives . push ( points ) ; } function parseCondLine ( abstractPart , line ) { if ( ! abstractPart . primitives ) { abstractPart . primitives = [ ] ; } abstractPart . primitives . push ( { p : [ parseFloat ( line [ 2 ] ) , parseFloat ( line [ 3 ] ) , parseFloat ( line [ 4 ] ) , parseFloat ( line [ 5 ] ) , parseFloat ( line [ 6 ] ) , parseFloat ( line [ 7 ] ) , ] , cp : [ parseFloat ( line [ 8 ] ) , parseFloat ( line [ 9 ] ) , parseFloat ( line [ 10 ] ) , parseFloat ( line [ 11 ] ) , parseFloat ( line [ 12 ] ) , parseFloat ( line [ 13 ] ) , ] , } ) ; } const lineParsers = { 0 : parseComment , 1 : parsePart , 2 : parseLine , 3 : parseTriangle , 4 : parseQuad , 5 : parseCondLine , } ; async function lineListToAbstractPart ( filename , lineList , progressCallback ) { if ( filename && filename . includes ( '/' ) ) { filename = filename . slice ( filename . lastIndexOf ( '/' ) + 1 ) ; } const abstractPart = { filename , name : '' } ; if ( lineList [ 0 ] && lineList [ 0 ] [ 0 ] === '0' ) { if ( lineList [ 0 ] [ 1 ] === 'FILE' ) { abstractPart . name = lineList [ 0 ] . slice ( 2 ) . join ( ' ' ) ; } else { abstractPart . name = lineList [ 0 ] . slice ( 1 ) . join ( ' ' ) ; abstractPart . name = abstractPart . name . replace ( /^Name:\s*/ig , '' ) ; } } if ( ! filename && lineList [ 1 ] && lineList [ 1 ] [ 0 ] === '0' && lineList [ 1 ] [ 1 ] === 'Name:' && lineList [ 1 ] [ 2 ] ) { abstractPart . filename = lineList [ 1 ] [ 2 ] ; } for ( let i = 0 ; i < lineList . length ; i ++ ) { const line = lineList [ i ] ; if ( line && line [ 0 ] in lineParsers ) { await lineParsers [ line [ 0 ] ] ( abstractPart , line ) ; if ( progressCallback && line [ 0 ] === '1' ) { progressCallback ( ) ; } } } return abstractPart ; } async function loadSubModels ( lineList , progressCallback ) { const models = [ ] ; let lastFileLine , i , partName ; for ( i = 0 ; i < lineList . length ; i ++ ) { const line = lineList [ i ] ; if ( line && line [ 0 ] === '0' && line [ 1 ] === 'FILE' ) { if ( lastFileLine != null ) { models . push ( { start : lastFileLine , end : i - 1 } ) ; } lastFileLine = i ; } } if ( lastFileLine != null ) { models . push ( { start : lastFileLine , end : i - 1 } ) ; } if ( models . length ) { for ( i = 1 ; i < models . length ; i ++ ) { const model = models [ i ] ; partName = lineList [ model . start ] . slice ( 2 ) . join ( ' ' ) . toLowerCase ( ) ; if ( partName in api . partDictionary ) { delete api . partDictionary [ partName ] ; } unloadedSubModels [ partName ] = lineList . slice ( model . start , model . end + 1 ) ; } partName = lineList [ models [ 0 ] . start ] . slice ( 2 ) . join ( ' ' ) ; const lines = lineList . slice ( models [ 0 ] . start , models [ 0 ] . end + 1 ) ; return await lineListToAbstractPart ( partName , lines , progressCallback ) ; } return null ; } async function loadPart ( fn , content , progressCallback ) { let part ; if ( fn && fn in api . partDictionary ) { return api . partDictionary [ fn ] ; } else if ( fn && fn in api . missingParts ) { api . missingParts [ fn ] += 1 ; return null ; } else if ( fn && fn . toLowerCase ( ) in unloadedSubModels ) { const fnLower = fn . toLowerCase ( ) ; part = await lineListToAbstractPart ( fn , unloadedSubModels [ fnLower ] , progressCallback ) ; part . isSubModel = true ; delete unloadedSubModels [ fnLower ] ; } else { if ( ! content ) { content = await requestPart ( fn ) ; } if ( ! content ) { api . missingParts [ fn ] = 1 ; return null ; } const lineList = [ ] , tmpList = content . split ( '\n' ) ; let partCount = 0 ; for ( let i = 0 ; i < tmpList . length ; i ++ ) { const line = tmpList [ i ] . trim ( ) . replace ( /\s\s+/g , ' ' ) . split ( ' ' ) ; if ( line && line . length > 1 ) { lineList . push ( line ) ; if ( line [ 0 ] === '1' ) { partCount ++ ; } } } if ( lineList . length < 1 ) { api . missingParts [ fn ] = 1 ; return null ; } if ( progressCallback ) { progressCallback ( { stepCount : partCount } ) ; } if ( ! fn || fn . endsWith ( 'mpd' ) ) { part = await loadSubModels ( lineList , progressCallback ) ; } if ( part == null ) { part = await lineListToAbstractPart ( fn , lineList , progressCallback ) ; } } api . partDictionary [ part . filename ] = part ; if ( part . filename in api . missingParts ) { delete api . missingParts [ part . filename ] ; } if ( part . steps && part . parts ) { if ( part . steps . lastPart < part . parts . length ) { const skippedParts = part . parts . map ( ( el , idx ) => idx ) . slice ( part . steps . lastPart ) ; part . steps . push ( { parts : skippedParts } ) ; } delete part . steps . lastPart ; } return part ; } export default api ;