importosimportjsonimportnumpyasnpfrom..importutilfrom..importresolversfrom..baseimportTrimeshfrom..parentimportGeometryfrom..pointsimportPointCloudfrom..scene.sceneimportScene,append_scenesfrom..utilimportlog,nowfrom..exceptionsimportExceptionWrapperfrom.importmiscfrom.xyzimport_xyz_loadersfrom.plyimport_ply_loadersfrom.stlimport_stl_loadersfrom.daeimport_collada_loadersfrom.objimport_obj_loadersfrom.offimport_off_loadersfrom.miscimport_misc_loadersfrom.gltfimport_gltf_loadersfrom.xamlimport_xaml_loadersfrom.binvoximport_binvox_loadersfrom.threemfimport_three_loadersfrom.openctmimport_ctm_loadersfrom.threedxmlimport_threedxml_loaderstry:from..path.exchange.loadimportload_path,path_formatsexceptBaseExceptionasE:# save a traceback to see why path didn't importload_path=ExceptionWrapper(E)# no path formats availabledefpath_formats():returnset()defmesh_formats():""" Get a list of mesh formats available to load. Returns ----------- loaders : list Extensions of available mesh loaders, i.e. 'stl', 'ply', etc. """# filter out exceptionmodule loadersreturnset(kfork,vinmesh_loaders.items()ifnotisinstance(v,ExceptionWrapper))
[docs]defavailable_formats():""" Get a list of all available loaders Returns ----------- loaders : list Extensions of available loaders i.e. 'stl', 'ply', 'dxf', etc. """# setloaders=mesh_formats()loaders.update(path_formats())loaders.update(compressed_loaders.keys())returnloaders
[docs]defload(file_obj,file_type=None,resolver=None,force=None,**kwargs):""" Load a mesh or vectorized path into objects like Trimesh, Path2D, Path3D, Scene Parameters ----------- file_obj : str, or file- like object The source of the data to be loadeded file_type: str What kind of file type do we have (eg: 'stl') resolver : trimesh.visual.Resolver Object to load referenced assets like materials and textures force : None or str For 'mesh': try to coerce scenes into a single mesh For 'scene': try to coerce everything into a scene kwargs : dict Passed to geometry __init__ Returns --------- geometry : Trimesh, Path2D, Path3D, Scene Loaded geometry as trimesh classes """# check to see if we're trying to load something# that is already a native trimesh Geometry subclassifisinstance(file_obj,Geometry):log.info('Load called on %s object, returning input',file_obj.__class__.__name__)returnfile_obj# parse the file arguments into clean loadable form(file_obj,# file- like objectfile_type,# str, what kind of filemetadata,# dict, any metadata from file nameopened,# bool, did we open the file ourselvesresolver# object to load referenced resources)=parse_file_args(file_obj=file_obj,file_type=file_type,resolver=resolver)try:ifisinstance(file_obj,dict):# if we've been passed a dict treat it as kwargskwargs.update(file_obj)loaded=load_kwargs(kwargs)eliffile_typeinpath_formats():# path formats get loaded with path loaderloaded=load_path(file_obj,file_type=file_type,**kwargs)eliffile_typeinmesh_loaders:# mesh loaders use mesh loaderloaded=load_mesh(file_obj,file_type=file_type,resolver=resolver,**kwargs)eliffile_typeincompressed_loaders:# for archives, like ZIP filesloaded=load_compressed(file_obj,file_type=file_type,**kwargs)eliffile_typeinvoxel_loaders:loaded=voxel_loaders[file_type](file_obj,file_type=file_type,resolver=resolver,**kwargs)else:iffile_typein['svg','dxf']:# call the dummy function to raise the import error# this prevents the exception from being super opaqueload_path()else:raiseValueError('File type: %s not supported'%file_type)finally:# close any opened files even if we crashed outifopened:file_obj.close()# add load metadata ('file_name') to each loaded geometryforiinutil.make_sequence(loaded):i.metadata.update(metadata)# if we opened the file in this function ourselves from a# file name clean up after ourselves by closing itifopened:file_obj.close()# combine a scene into a single meshifforce=='mesh'andisinstance(loaded,Scene):returnutil.concatenate(loaded.dump())ifforce=='scene'andnotisinstance(loaded,Scene):returnScene(loaded)returnloaded
[docs]defload_mesh(file_obj,file_type=None,resolver=None,**kwargs):""" Load a mesh file into a Trimesh object Parameters ----------- file_obj : str or file object File name or file with mesh data file_type : str or None Which file type, e.g. 'stl' kwargs : dict Passed to Trimesh constructor Returns ---------- mesh : trimesh.Trimesh or trimesh.Scene Loaded geometry data """# parse the file arguments into clean loadable form(file_obj,# file- like objectfile_type,# str, what kind of filemetadata,# dict, any metadata from file nameopened,# bool, did we open the file ourselvesresolver# object to load referenced resources)=parse_file_args(file_obj=file_obj,file_type=file_type,resolver=resolver)try:# make sure we keep passed kwargs to loader# but also make sure loader keys override passed keysloader=mesh_loaders[file_type]tic=now()results=loader(file_obj,file_type=file_type,resolver=resolver,**kwargs)ifnotisinstance(results,list):results=[results]loaded=[]forresultinresults:kwargs.update(result)loaded.append(load_kwargs(kwargs))loaded[-1].metadata.update(metadata)iflen(loaded)==1:loaded=loaded[0]# show the repr for loaded, loader used, and timelog.debug('loaded {} using `{}` in {:0.4f}s'.format(str(loaded),loader.__name__,now()-tic))finally:# if we failed to load close fileifopened:file_obj.close()returnloaded
defload_compressed(file_obj,file_type=None,resolver=None,mixed=False,**kwargs):""" Given a compressed archive load all the geometry that we can from it. Parameters ---------- file_obj : open file-like object Containing compressed data file_type : str Type of the archive file mixed : bool If False, for archives containing both 2D and 3D data will only load the 3D data into the Scene. Returns ---------- scene : trimesh.Scene Geometry loaded in to a Scene object """# parse the file arguments into clean loadable form(file_obj,# file- like objectfile_type,# str, what kind of filemetadata,# dict, any metadata from file nameopened,# bool, did we open the file ourselvesresolver# object to load referenced resources)=parse_file_args(file_obj=file_obj,file_type=file_type,resolver=resolver)try:# a dict of 'name' : file-like objectfiles=util.decompress(file_obj=file_obj,file_type=file_type)# store loaded geometries as a listgeometries=[]# so loaders can access textures/etcresolver=resolvers.ZipResolver(files)# try to save the files with meaningful metadataif'file_path'inmetadata:archive_name=metadata['file_path']else:archive_name='archive'# populate our available formatsifmixed:available=available_formats()else:# all types contained in ZIP archivecontains=set(util.split_extension(n).lower()forninfiles.keys())# if there are no mesh formats availableifcontains.isdisjoint(mesh_formats()):available=path_formats()else:available=mesh_formats()meta_archive={}forname,datainfiles.items():try:# only load formats that we supportcompressed_type=util.split_extension(name).lower()# if file has metadata type include itifcompressed_typein'yaml':importyamlmeta_archive[name]=yaml.safe_load(data)elifcompressed_typein'json':importjsonmeta_archive[name]=json.loads(data)ifcompressed_typenotinavailable:# don't raise an exception, just try the next onecontinue# store the file name relative to the archivemetadata['file_name']=(archive_name+'/'+os.path.basename(name))# load the individual geometryloaded=load(file_obj=data,file_type=compressed_type,resolver=resolver,metadata=metadata,**kwargs)# some loaders return multiple geometriesifutil.is_sequence(loaded):# if the loader has returned a list of meshesgeometries.extend(loaded)else:# if the loader has returned a single geometrygeometries.append(loaded)exceptBaseException:log.debug('failed to load file in zip',exc_info=True)finally:# if we opened the file in this function# clean up after ourselvesifopened:file_obj.close()# append meshes or scenes into a single Scene objectresult=append_scenes(geometries)# append any archive metadata filesifisinstance(result,Scene):result.metadata.update(meta_archive)returnresult
[docs]defload_remote(url,**kwargs):""" Load a mesh at a remote URL into a local trimesh object. This must be called explicitly rather than automatically from trimesh.load to ensure users don't accidentally make network requests. Parameters ------------ url : string URL containing mesh file **kwargs : passed to `load` Returns ------------ loaded : Trimesh, Path, Scene Loaded result """# import here to keep requirement softimportrequests# download the meshresponse=requests.get(url)# wrap as file objectfile_obj=util.wrap_as_stream(response.content)# so loaders can access textures/etcresolver=resolvers.WebResolver(url)try:# if we have a bunch of query parameters the type# will be wrong so try to clean up the URL# urllib is Python 3 onlyimporturllib# remove the url-safe encoding then split off query paramsfile_type=urllib.parse.unquote(url).split('?',1)[0].split('/')[-1].strip()exceptBaseException:# otherwise just use the last chunk of URLfile_type=url.split('/')[-1].split('?',1)[0]# actually load the data from the retrieved bytesloaded=load(file_obj=file_obj,file_type=file_type,resolver=resolver,**kwargs)returnloaded
defload_kwargs(*args,**kwargs):""" Load geometry from a properly formatted dict or kwargs """defhandle_scene():""" Load a scene from our kwargs. class: Scene geometry: dict, name: Trimesh kwargs graph: list of dict, kwargs for scene.graph.update base_frame: str, base frame of graph """graph=kwargs.get('graph',None)geometry={k:load_kwargs(v)fork,vinkwargs['geometry'].items()}ifgraphisnotNone:scene=Scene()scene.geometry.update(geometry)forkingraph:ifisinstance(k,dict):scene.graph.update(**k)elifutil.is_sequence(k)andlen(k)==3:scene.graph.update(k[1],k[0],**k[2])else:scene=Scene(geometry)if'base_frame'inkwargs:scene.graph.base_frame=kwargs['base_frame']metadata=kwargs.get('metadata')ifisinstance(metadata,dict):scene.metadata.update(kwargs['metadata'])elifisinstance(metadata,str):# some ways someone might have encoded a string# note that these aren't evaluated until we# actually call the lambda in the loopcandidates=[lambda:json.loads(metadata),lambda:json.loads(metadata.replace("'",'"'))]forcincandidates:try:scene.metadata.update(c())breakexceptBaseException:passelifmetadataisnotNone:log.warning('unloadable metadata')returnscenedefhandle_mesh():""" Handle the keyword arguments for a Trimesh object """# if they've been serialized as a dictif(isinstance(kwargs['vertices'],dict)orisinstance(kwargs['faces'],dict)):returnTrimesh(**misc.load_dict(kwargs))# otherwise just load that puppyreturnTrimesh(**kwargs)defhandle_export():""" Handle an exported mesh. """data,file_type=kwargs['data'],kwargs['file_type']ifnotisinstance(data,dict):data=util.wrap_as_stream(data)k=mesh_loaders[file_type](data,file_type=file_type)returnTrimesh(**k)defhandle_path():from..pathimportPath2D,Path3Dshape=np.shape(kwargs['vertices'])iflen(shape)<2:returnPath2D()ifshape[1]==2:returnPath2D(**kwargs)elifshape[1]==3:returnPath3D(**kwargs)else:raiseValueError('Vertices must be 2D or 3D!')defhandle_pointcloud():returnPointCloud(**kwargs)# if we've been passed a single dict instead of kwargs# substitute the dict for kwargsif(len(kwargs)==0andlen(args)==1andisinstance(args[0],dict)):kwargs=args[0]# (function, tuple of expected keys)# order is importanthandlers=((handle_scene,('geometry',)),(handle_mesh,('vertices','faces')),(handle_path,('entities','vertices')),(handle_pointcloud,('vertices',)),(handle_export,('file_type','data')))# filter out keys with a value of Nonekwargs={k:vfork,vinkwargs.items()ifvisnotNone}# loop through handler functions and expected keyforfunc,expectedinhandlers:ifall(iinkwargsforiinexpected):# all expected kwargs existhandler=func# exit the loop as we found onebreakelse:raiseValueError('unable to determine type: {}'.format(kwargs.keys()))returnhandler()defparse_file_args(file_obj,file_type,resolver=None,**kwargs):""" Given a file_obj and a file_type try to magically convert arguments to a file-like object and a lowercase string of file type. Parameters ----------- file_obj : str if string represents a file path, returns: file_obj: an 'rb' opened file object of the path file_type: the extension from the file path if string is NOT a path, but has JSON-like special characters: file_obj: the same string passed as file_obj file_type: set to 'json' if string is a valid-looking URL file_obj: an open 'rb' file object with retrieved data file_type: from the extension if string is none of those: raise ValueError as we can't do anything with input if file like object: ValueError will be raised if file_type is None file_obj: same as input file_type: same as input if other object: like a shapely.geometry.Polygon, etc: file_obj: same as input file_type: if None initially, set to the class name (in lower case), otherwise passed through file_type : str type of file and handled according to above Returns ----------- file_obj : file-like object Contains data file_type : str Lower case of the type of file (eg 'stl', 'dae', etc) metadata : dict Any metadata gathered opened : bool Did we open the file or not resolver : trimesh.visual.Resolver Resolver to load other assets """metadata={}opened=Falseif('metadata'inkwargsandisinstance(kwargs['metadata'],dict)):metadata.update(kwargs['metadata'])ifutil.is_pathlib(file_obj):# convert pathlib objects to stringfile_obj=str(file_obj.absolute())ifutil.is_file(file_obj)andfile_typeisNone:raiseValueError('file_type must be set for file objects!')ifutil.is_string(file_obj):try:# os.path.isfile will return False incorrectly# if we don't give it an absolute pathfile_path=os.path.expanduser(file_obj)file_path=os.path.abspath(file_path)exists=os.path.isfile(file_path)exceptBaseException:exists=False# file obj is a string which exists on filesystmifexists:# if not passed create a resolver to find other filesifresolverisNone:resolver=resolvers.FilePathResolver(file_path)# save the file name and path to metadatametadata['file_path']=file_pathmetadata['file_name']=os.path.basename(file_obj)# if file_obj is a path that exists use extension as file_typeiffile_typeisNone:file_type=util.split_extension(file_path,special=['tar.gz','tar.bz2'])# actually open the filefile_obj=open(file_path,'rb')opened=Trueelse:if'{'infile_obj:# if a dict bracket is in the string, its probably a straight# JSONfile_type='json'elif'https://'infile_objor'http://'infile_obj:# we've been passed a URL, warn to use explicit function# and don't do network calls via magical pipelineraiseValueError('use load_remote to load URL: {}'.format(file_obj))eliffile_typeisNone:raiseValueError('string is not a file: {}'.format(file_obj))iffile_typeisNone:file_type=file_obj.__class__.__name__ifutil.is_string(file_type)and'.'infile_type:# if someone has passed the whole filename as the file_type# use the file extension as the file_typeif'file_path'notinmetadata:metadata['file_path']=file_typemetadata['file_name']=os.path.basename(file_type)file_type=util.split_extension(file_type)ifresolverisNoneandos.path.exists(file_type):resolver=resolvers.FilePathResolver(file_type)# all our stored extensions reference in lower casefile_type=file_type.lower()# if we still have no resolver try using file_obj nameif(resolverisNoneandhasattr(file_obj,'name')andfile_obj.nameisnotNoneandlen(file_obj.name)>0):resolver=resolvers.FilePathResolver(file_obj.name)returnfile_obj,file_type,metadata,opened,resolver# loader functions for compressed extensionscompressed_loaders={'zip':load_compressed,'tar.bz2':load_compressed,'tar.gz':load_compressed}# map file_type to loader functionmesh_loaders={}mesh_loaders.update(_misc_loaders)mesh_loaders.update(_stl_loaders)mesh_loaders.update(_ctm_loaders)mesh_loaders.update(_ply_loaders)mesh_loaders.update(_obj_loaders)mesh_loaders.update(_off_loaders)mesh_loaders.update(_collada_loaders)mesh_loaders.update(_gltf_loaders)mesh_loaders.update(_xaml_loaders)mesh_loaders.update(_threedxml_loaders)mesh_loaders.update(_three_loaders)mesh_loaders.update(_xyz_loaders)# collect loaders which return voxel typesvoxel_loaders={}voxel_loaders.update(_binvox_loaders)