From ac3bebb825f269fa55114b7a60b1dd9e774b1840 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 7 Oct 2024 16:19:15 -0400 Subject: [PATCH 1/4] rough draft of external file mesh loading --- trimesh/exchange/threemf.py | 66 +++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/trimesh/exchange/threemf.py b/trimesh/exchange/threemf.py index f1c024496..1d2a47c98 100644 --- a/trimesh/exchange/threemf.py +++ b/trimesh/exchange/threemf.py @@ -7,6 +7,7 @@ from .. import graph, util from ..constants import log +from ..util import unique_name def load_3MF(file_obj, postprocess=True, **kwargs): @@ -23,6 +24,30 @@ def load_3MF(file_obj, postprocess=True, **kwargs): kwargs : dict Constructor arguments for `trimesh.Scene` """ + + def read_mesh(mesh, index): + 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) + # dict, {name in archive: BytesIo} archive = util.decompress(file_obj, file_type="zip") # get model with case-insensitive keys @@ -61,38 +86,16 @@ def load_3MF(file_obj, postprocess=True, **kwargs): index = obj.attrib["id"] # start with stored name - name = obj.attrib.get("name", str(index)) # apparently some exporters name multiple meshes # the same thing so check to see if it's been used - if name in consumed_names: - name = name + str(index) + name = unique_name(obj.attrib.get("name", str(index)), consumed_names) consumed_names.add(name) # store name reference on the index id_name[index] = name # if the object has actual geometry data parse here 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) + read_mesh(mesh, index) # components are references to other geometries for c in obj.iter("{*}component"): @@ -100,6 +103,18 @@ def load_3MF(file_obj, postprocess=True, **kwargs): transform = _attrib_to_transform(c.attrib) components[index].append((mesh_index, transform)) + # if this references another file as the `path` attrib + path = next((v for k, v in c.attrib.items() if k.endswith("path")), None) + if path is not None: + path = path.strip("/").strip() + if path in archive: + [ + read_mesh(m, mesh_index) + for _, m in etree.iterparse( + archive[path], tag=("{*}mesh"), events=("start",) + ) + ] + # parse build if "build" in obj.tag: # scene graph information stored here, aka "build" the scene @@ -158,6 +173,9 @@ def load_3MF(file_obj, postprocess=True, **kwargs): # if someone included an undefined component, skip it if last not in id_name: log.debug(f"id {last} included but not defined!") + from IPython import embed + + embed() continue # frame names unique name = id_name[last] + util.unique_id() From 39ebfac4ef78fe7880b18dd563d78f4c638d1182 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Tue, 8 Oct 2024 17:06:27 -0400 Subject: [PATCH 2/4] working on example file --- tests/test_3mf.py | 2 +- trimesh/exchange/threemf.py | 104 ++++++++++++++++++++---------------- 2 files changed, 59 insertions(+), 47 deletions(-) diff --git a/tests/test_3mf.py b/tests/test_3mf.py index 1604b23dc..135ac75d9 100644 --- a/tests/test_3mf.py +++ b/tests/test_3mf.py @@ -60,7 +60,7 @@ def test_roundtrip(self): file_type="3mf", ) - assert set(s.geometry.keys()) == set(r.geometry.keys()) + assert set(s.geometry.keys()) == set(r.geometry.keys()), (s.geometry.keys(), r.geometry.keys()) assert g.np.allclose(s.bounds, r.bounds) assert g.np.isclose(s.area, r.area, rtol=1e-3) diff --git a/trimesh/exchange/threemf.py b/trimesh/exchange/threemf.py index 1d2a47c98..23c0fa100 100644 --- a/trimesh/exchange/threemf.py +++ b/trimesh/exchange/threemf.py @@ -1,7 +1,7 @@ -import collections import io import uuid import zipfile +from collections import defaultdict import numpy as np @@ -10,6 +10,28 @@ from ..util import unique_name +def _read_mesh(mesh): + vertices = mesh.find("{*}vertices") + v_array = np.array( + [ + [i.attrib["x"], i.attrib["y"], i.attrib["z"]] + for i in vertices.iter("{*}vertex") + ], + dtype=np.float64, + ) + + faces = mesh.find("{*}triangles") + f_array = np.array( + [ + [i.attrib["v1"], i.attrib["v2"], i.attrib["v3"]] + for i in faces.iter("{*}triangle") + ], + dtype=np.int64, + ) + + return v_array, f_array + + def load_3MF(file_obj, postprocess=True, **kwargs): """ Load a 3MF formatted file into a Trimesh scene. @@ -25,29 +47,6 @@ def load_3MF(file_obj, postprocess=True, **kwargs): Constructor arguments for `trimesh.Scene` """ - def read_mesh(mesh, index): - 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) - # dict, {name in archive: BytesIo} archive = util.decompress(file_obj, file_type="zip") # get model with case-insensitive keys @@ -65,12 +64,12 @@ def read_mesh(mesh, index): # { mesh id : mesh name} id_name = {} # { mesh id: (n,3) float vertices} - v_seq = {} + v_seq = defaultdict(list) # { mesh id: (n,3) int faces} - f_seq = {} + f_seq = defaultdict(list) # components are objects that contain other objects # {id : [other ids]} - components = collections.defaultdict(list) + components = defaultdict(list) # load information about the scene graph # each instance is a single geometry build_items = [] @@ -79,7 +78,7 @@ def read_mesh(mesh, index): # iterate the XML object and build elements with an LXML iterator # loaded elements are cleared to avoid ballooning memory model.seek(0) - for _, obj in etree.iterparse(model, tag=("{*}object", "{*}build")): + for _, obj in etree.iterparse(model, tag=("{*}object", "{*}build"), events=("end",)): # parse objects if "object" in obj.tag: # id is mandatory @@ -95,25 +94,39 @@ def read_mesh(mesh, index): # if the object has actual geometry data parse here for mesh in obj.iter("{*}mesh"): - read_mesh(mesh, index) + v, f = _read_mesh(mesh) + v_seq[index].append(v) + f_seq[index].append(f) # 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)) # if this references another file as the `path` attrib - path = next((v for k, v in c.attrib.items() if k.endswith("path")), None) - if path is not None: - path = path.strip("/").strip() - if path in archive: - [ - read_mesh(m, mesh_index) - for _, m in etree.iterparse( - archive[path], tag=("{*}mesh"), events=("start",) - ) - ] + path = next( + (v.strip("/") for k, v in c.attrib.items() if k.endswith("path")), + None, + ) + if path in archive: + archive[path].seek(0) + name = unique_name( + obj.attrib.get("name", str(mesh_index)), consumed_names + ) + consumed_names.add(name) + # store name reference on the index + id_name[mesh_index] = name + + for _, m in etree.iterparse( + archive[path], tag=("{*}mesh"), events=("end",) + ): + v, f = _read_mesh(m) + v_seq[mesh_index].append(v) + f_seq[mesh_index].append(f) # parse build if "build" in obj.tag: @@ -133,10 +146,11 @@ def read_mesh(mesh, index): # one mesh per geometry ID, store as kwargs for the object meshes = {} for gid in v_seq.keys(): + v, f = util.append_faces(v_seq[gid], f_seq[gid]) name = id_name[gid] meshes[name] = { - "vertices": v_seq[gid], - "faces": f_seq[gid], + "vertices": v, + "faces": f, "metadata": metadata.copy(), } meshes[name].update(kwargs) @@ -158,7 +172,7 @@ def read_mesh(mesh, index): # flatten the scene structure and simplify to # a single unique node per instance graph_args = [] - parents = collections.defaultdict(set) + parents = 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") @@ -172,11 +186,9 @@ def read_mesh(mesh, index): last = path[-1][0] # if someone included an undefined component, skip it if last not in id_name: - log.debug(f"id {last} included but not defined!") - from IPython import embed - - embed() + log.warning(f"id {last} included but not defined!") continue + # frame names unique name = id_name[last] + util.unique_id() # index in meshes From 21a1591bd3a22ccb8c2917ccad863c5accf9a260 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 14 Oct 2024 14:58:15 -0400 Subject: [PATCH 3/4] check for None --- tests/test_3mf.py | 5 ++++- trimesh/exchange/threemf.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_3mf.py b/tests/test_3mf.py index 135ac75d9..adb40b267 100644 --- a/tests/test_3mf.py +++ b/tests/test_3mf.py @@ -60,7 +60,10 @@ def test_roundtrip(self): file_type="3mf", ) - assert set(s.geometry.keys()) == set(r.geometry.keys()), (s.geometry.keys(), r.geometry.keys()) + assert set(s.geometry.keys()) == set(r.geometry.keys()), ( + s.geometry.keys(), + r.geometry.keys(), + ) assert g.np.allclose(s.bounds, r.bounds) assert g.np.isclose(s.area, r.area, rtol=1e-3) diff --git a/trimesh/exchange/threemf.py b/trimesh/exchange/threemf.py index 23c0fa100..8cacbb588 100644 --- a/trimesh/exchange/threemf.py +++ b/trimesh/exchange/threemf.py @@ -112,7 +112,7 @@ def load_3MF(file_obj, postprocess=True, **kwargs): (v.strip("/") for k, v in c.attrib.items() if k.endswith("path")), None, ) - if path in archive: + if path is not None and path in archive: archive[path].seek(0) name = unique_name( obj.attrib.get("name", str(mesh_index)), consumed_names From af820f8aac08a67583716a136005278bb9aafcb3 Mon Sep 17 00:00:00 2001 From: Michael Dawson-Haggerty Date: Mon, 14 Oct 2024 15:36:40 -0400 Subject: [PATCH 4/4] fix a deadlock in 3MF --- trimesh/exchange/threemf.py | 19 +++++++++---------- trimesh/path/polygons.py | 33 ++++++++++++++------------------- 2 files changed, 23 insertions(+), 29 deletions(-) diff --git a/trimesh/exchange/threemf.py b/trimesh/exchange/threemf.py index 8cacbb588..554398e39 100644 --- a/trimesh/exchange/threemf.py +++ b/trimesh/exchange/threemf.py @@ -74,7 +74,10 @@ def load_3MF(file_obj, postprocess=True, **kwargs): # each instance is a single geometry build_items = [] + # keep track of names we can use + consumed_counts = {} consumed_names = set() + # iterate the XML object and build elements with an LXML iterator # loaded elements are cleared to avoid ballooning memory model.seek(0) @@ -87,7 +90,9 @@ def load_3MF(file_obj, postprocess=True, **kwargs): # start with stored name # apparently some exporters name multiple meshes # the same thing so check to see if it's been used - name = unique_name(obj.attrib.get("name", str(index)), consumed_names) + name = unique_name( + obj.attrib.get("name", str(index)), consumed_names, consumed_counts + ) consumed_names.add(name) # store name reference on the index id_name[index] = name @@ -101,9 +106,6 @@ def load_3MF(file_obj, postprocess=True, **kwargs): # 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)) @@ -115,7 +117,9 @@ def load_3MF(file_obj, postprocess=True, **kwargs): if path is not None and path in archive: archive[path].seek(0) name = unique_name( - obj.attrib.get("name", str(mesh_index)), consumed_names + obj.attrib.get("name", str(mesh_index)), + consumed_names, + consumed_counts, ) consumed_names.add(name) # store name reference on the index @@ -137,11 +141,6 @@ def load_3MF(file_obj, postprocess=True, **kwargs): # 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 = {} diff --git a/trimesh/path/polygons.py b/trimesh/path/polygons.py index 2ff79f951..780f82f7f 100644 --- a/trimesh/path/polygons.py +++ b/trimesh/path/polygons.py @@ -3,6 +3,7 @@ from shapely.geometry import Polygon from .. import bounds, geometry, graph, grouping +from ..boolean import reduce_cascade from ..constants import log from ..constants import tol_path as tol from ..transformations import transform_points @@ -162,14 +163,14 @@ def edges_to_polygons(edges: NDArray[int64], vertices: NDArray[float64]): # find which polygons contain which other polygons roots, tree = enclosure_tree(polygons) - # generate list of polygons with proper interiors - complete = [] - for root in roots: - interior = list(tree[root].keys()) - shell = polygons[root].exterior.coords - holes = [polygons[i].exterior.coords for i in interior] - complete.append(Polygon(shell=shell, holes=holes)) - return complete + # generate polygons with proper interiors + return [ + Polygon( + shell=polygons[root.exterior], + holes=[polygons[i].exterior for i in tree[root].keys()], + ) + for root in roots + ] def polygons_obb(polygons: Iterable[Polygon]): @@ -864,17 +865,11 @@ def projected( return polygons[0] elif len(polygons) == 0: return None - # inflate each polygon before unioning to remove zero-size - # gaps then deflate the result after unioning by the same amount - # note the following provides a 25% speedup but needs - # more testing to see if it deflates to a decent looking - # result: - # polygon = ops.unary_union( - # [p.buffer(padding, - # join_style=2, - # mitre_limit=1.5) - # for p in polygons]).buffer(-padding) - return ops.unary_union([p.buffer(padding) for p in polygons]).buffer(-padding) + + # in my tests this was substantially faster than `shapely.ops.unary_union` + return ( + reduce_cascade(lambda a, b: a.union(b), polygons).buffer(padding).buffer(-padding) + ) def second_moments(polygon: Polygon, return_centered=False):