hub/venv/lib/python3.7/site-packages/trimesh/exchange/threemf.py

235 lines
8.1 KiB
Python

import collections
import numpy as np
from .. import util
from .. import graph
from ..constants import log
try:
import networkx as nx
except BaseException as E:
# create a dummy module which will raise the ImportError
# or other exception only when someone tries to use networkx
from ..exceptions import ExceptionModule
nx = ExceptionModule(E)
def load_3MF(file_obj,
postprocess=True,
**kwargs):
"""
Load a 3MF formatted file into a Trimesh scene.
Parameters
------------
file_obj: file object
Returns
------------
kwargs: dict, with keys 'graph', 'geometry', 'base_frame'
"""
# dict, {name in archive: BytesIo}
archive = util.decompress(file_obj, file_type='zip')
# get model file
model = archive['3D/3dmodel.model']
# read root attributes only from XML first
event, root = next(etree.iterparse(model, tag=('{*}model'), events=('start',)))
# collect unit information from the tree
if 'unit' in root.attrib:
metadata = {'units': root.attrib['unit']}
else:
# the default units, defined by the specification
metadata = {'units': 'millimeters'}
# { mesh id : mesh name}
id_name = {}
# { mesh id: (n,3) float vertices}
v_seq = {}
# { mesh id: (n,3) int faces}
f_seq = {}
# components are objects that contain other objects
# {id : [other ids]}
components = collections.defaultdict(list)
# load information about the scene graph
# each instance is a single geometry
build_items = []
# iterate the XML object and build elements with an LXML iterator
# loaded elements are cleared to avoid ballooning memory
model.seek(0)
for event, obj in etree.iterparse(model, tag=('{*}object', '{*}build')):
# parse objects
if 'object' in obj.tag:
# id is mandatory
index = obj.attrib['id']
# not required, so use a get call which will return None
# if the tag isn't populated
if 'name' in obj.attrib:
name = obj.attrib['name']
else:
name = str(index)
# store the name by index
id_name[index] = name
# if the object has actual geometry data, store it
for mesh in obj.iter('{*}mesh'):
vertices = mesh.find('{*}vertices')
v_seq[index] = np.array([[i.attrib['x'],
i.attrib['y'],
i.attrib['z']] for
i in vertices.iter('{*}vertex')],
dtype=np.float64)
vertices.clear()
vertices.getparent().remove(vertices)
faces = mesh.find('{*}triangles')
f_seq[index] = np.array([[i.attrib['v1'],
i.attrib['v2'],
i.attrib['v3']] for
i in faces.iter('{*}triangle')],
dtype=np.int64)
faces.clear()
faces.getparent().remove(faces)
# components are references to other geometries
for c in obj.iter('{*}component'):
mesh_index = c.attrib['objectid']
transform = _attrib_to_transform(c.attrib)
components[index].append((mesh_index, transform))
# parse build
if 'build' in obj.tag:
# scene graph information stored here, aka "build" the scene
for item in obj.iter('{*}item'):
# get a transform from the item's attributes
transform = _attrib_to_transform(item.attrib)
# the index of the geometry this item instantiates
build_items.append((item.attrib['objectid'], transform))
# free resources
obj.clear()
obj.getparent().remove(obj)
del obj
# have one mesh per 3MF object
# one mesh per geometry ID, store as kwargs for the object
meshes = {}
for gid in v_seq.keys():
name = id_name[gid]
meshes[name] = {'vertices': v_seq[gid],
'faces': f_seq[gid],
'metadata': metadata.copy()}
# turn the item / component representation into
# a MultiDiGraph to compound our pain
g = nx.MultiDiGraph()
# build items are the only things that exist according to 3MF
# so we accomplish that by linking them to the base frame
for gid, tf in build_items:
g.add_edge('world', gid, matrix=tf)
# components are instances which need to be linked to base
# frame by a build_item
for start, group in components.items():
for i, (gid, tf) in enumerate(group):
g.add_edge(start, gid, matrix=tf)
# turn the graph into kwargs for a scene graph
# flatten the scene structure and simplify to
# a single unique node per instance
graph_args = []
parents = collections.defaultdict(set)
for path in graph.multigraph_paths(G=g,
source='world'):
# collect all the transform on the path
transforms = graph.multigraph_collect(G=g,
traversal=path,
attrib='matrix')
# combine them into a single transform
if len(transforms) == 1:
transform = transforms[0]
else:
transform = util.multi_dot(transforms)
# the last element of the path should be the geometry
last = path[-1][0]
# if someone included an undefined component, skip it
if last not in id_name:
log.debug('id {} included but not defined!'.format(last))
continue
# frame names unique
name = id_name[last] + util.unique_id()
# index in meshes
geom = id_name[last]
# collect parents if we want to combine later
if len(path) > 2:
parent = path[-2][0]
parents[parent].add(last)
graph_args.append({'frame_from': 'world',
'frame_to': name,
'matrix': transform,
'geometry': geom})
# solidworks will export each body as its own mesh with the part
# name as the parent so optionally rename and combine these bodies
if postprocess and all('body' in i.lower() for i in meshes.keys()):
# don't rename by default
rename = {k: k for k in meshes.keys()}
for parent, mesh_name in parents.items():
# only handle the case where a parent has a single child
# if there are multiple children we would do a combine op
if len(mesh_name) != 1:
continue
# rename the part
rename[id_name[next(iter(mesh_name))]] = id_name[parent].split(
'(')[0]
# apply the rename operation meshes
meshes = {rename[k]: m for k, m in meshes.items()}
# rename geometry references in the scene graph
for arg in graph_args:
if 'geometry' in arg:
arg['geometry'] = rename[arg['geometry']]
# construct the kwargs to load the scene
kwargs = {'base_frame': 'world',
'graph': graph_args,
'geometry': meshes,
'metadata': metadata}
return kwargs
def _attrib_to_transform(attrib):
"""
Extract a homogeneous transform from a dictionary.
Parameters
------------
attrib: dict, optionally containing 'transform'
Returns
------------
transform: (4, 4) float, homogeonous transformation
"""
transform = np.eye(4, dtype=np.float64)
if 'transform' in attrib:
# wangle their transform format
values = np.array(
attrib['transform'].split(),
dtype=np.float64).reshape((4, 3)).T
transform[:3, :4] = values
return transform
# do import here to keep lxml a soft dependency
try:
from lxml import etree
_three_loaders = {'3mf': load_3MF}
except ImportError:
_three_loaders = {}