|
4 | 4 | import generic as g |
5 | 5 |
|
6 | 6 |
|
7 | | -class RepairTests(g.unittest.TestCase): |
8 | | - def test_fill_holes(self): |
9 | | - for mesh_name in [ |
| 7 | +def test_fill_holes(): |
| 8 | + for mesh_name in [ |
| 9 | + "unit_cube.STL", |
| 10 | + "machinist.XAML", |
| 11 | + "round.stl", |
| 12 | + "sphere.ply", |
| 13 | + "teapot.stl", |
| 14 | + "soup.stl", |
| 15 | + "featuretype.STL", |
| 16 | + "angle_block.STL", |
| 17 | + "quadknot.obj", |
| 18 | + ]: |
| 19 | + mesh = g.get_mesh(mesh_name) |
| 20 | + if not mesh.is_watertight: |
| 21 | + # output of fill_holes should match watertight status |
| 22 | + returned = mesh.fill_holes() |
| 23 | + assert returned == mesh.is_watertight |
| 24 | + continue |
| 25 | + |
| 26 | + hashes = [{mesh._data.__hash__(), hash(mesh)}] |
| 27 | + |
| 28 | + mesh.faces = mesh.faces[1:-1] |
| 29 | + assert not mesh.is_watertight |
| 30 | + assert not mesh.is_volume |
| 31 | + |
| 32 | + # color some faces |
| 33 | + g.trimesh.repair.broken_faces(mesh, color=[255, 0, 0, 255]) |
| 34 | + |
| 35 | + hashes.append({mesh._data.__hash__(), hash(mesh)}) |
| 36 | + |
| 37 | + assert hashes[0] != hashes[1] |
| 38 | + |
| 39 | + # run the fill holes operation should succeed |
| 40 | + assert mesh.fill_holes() |
| 41 | + # should be a superset of the last two |
| 42 | + assert mesh.is_volume |
| 43 | + assert mesh.is_watertight |
| 44 | + assert mesh.is_winding_consistent |
| 45 | + |
| 46 | + hashes.append({mesh._data.__hash__(), hash(mesh)}) |
| 47 | + assert hashes[1] != hashes[2] |
| 48 | + |
| 49 | + # try broken faces on a watertight mesh |
| 50 | + g.trimesh.repair.broken_faces(mesh, color=[255, 255, 0, 255]) |
| 51 | + |
| 52 | + |
| 53 | +def test_fix_normals(): |
| 54 | + for mesh in g.get_meshes(5): |
| 55 | + mesh.fix_normals() |
| 56 | + |
| 57 | + |
| 58 | +def test_winding(): |
| 59 | + """ |
| 60 | + Reverse some faces and make sure fix_face_winding flips |
| 61 | + them back. |
| 62 | + """ |
| 63 | + |
| 64 | + meshes = [ |
| 65 | + g.get_mesh(i) |
| 66 | + for i in [ |
10 | 67 | "unit_cube.STL", |
11 | 68 | "machinist.XAML", |
12 | 69 | "round.stl", |
13 | | - "sphere.ply", |
14 | | - "teapot.stl", |
15 | | - "soup.stl", |
16 | | - "featuretype.STL", |
17 | | - "angle_block.STL", |
18 | 70 | "quadknot.obj", |
19 | | - ]: |
20 | | - mesh = g.get_mesh(mesh_name) |
21 | | - if not mesh.is_watertight: |
22 | | - # output of fill_holes should match watertight status |
23 | | - returned = mesh.fill_holes() |
24 | | - assert returned == mesh.is_watertight |
25 | | - continue |
26 | | - |
27 | | - hashes = [{mesh._data.__hash__(), hash(mesh)}] |
28 | | - |
29 | | - mesh.faces = mesh.faces[1:-1] |
30 | | - assert not mesh.is_watertight |
31 | | - assert not mesh.is_volume |
32 | | - |
33 | | - # color some faces |
34 | | - g.trimesh.repair.broken_faces(mesh, color=[255, 0, 0, 255]) |
35 | | - |
36 | | - hashes.append({mesh._data.__hash__(), hash(mesh)}) |
37 | | - |
38 | | - assert hashes[0] != hashes[1] |
39 | | - |
40 | | - # run the fill holes operation should succeed |
41 | | - assert mesh.fill_holes() |
42 | | - # should be a superset of the last two |
43 | | - assert mesh.is_volume |
44 | | - assert mesh.is_watertight |
45 | | - assert mesh.is_winding_consistent |
46 | | - |
47 | | - hashes.append({mesh._data.__hash__(), hash(mesh)}) |
48 | | - assert hashes[1] != hashes[2] |
49 | | - |
50 | | - # try broken faces on a watertight mesh |
51 | | - g.trimesh.repair.broken_faces(mesh, color=[255, 255, 0, 255]) |
52 | | - |
53 | | - def test_fix_normals(self): |
54 | | - for mesh in g.get_meshes(5): |
55 | | - mesh.fix_normals() |
56 | | - |
57 | | - def test_winding(self): |
58 | | - """ |
59 | | - Reverse some faces and make sure fix_face_winding flips |
60 | | - them back. |
61 | | - """ |
62 | | - |
63 | | - meshes = [ |
64 | | - g.get_mesh(i) |
65 | | - for i in [ |
66 | | - "unit_cube.STL", |
67 | | - "machinist.XAML", |
68 | | - "round.stl", |
69 | | - "quadknot.obj", |
70 | | - "soup.stl", |
71 | | - ] |
| 71 | + "soup.stl", |
72 | 72 | ] |
73 | | - |
74 | | - for i, mesh in enumerate(meshes): |
75 | | - # turn scenes into multibody meshes |
76 | | - if g.trimesh.util.is_instance_named(mesh, "Scene"): |
77 | | - meta = mesh.metadata |
78 | | - meshes[i] = mesh.dump().sum() |
79 | | - meshes[i].metadata = meta |
80 | | - |
81 | | - timing = {} |
82 | | - for mesh in meshes: |
83 | | - # save the initial state |
84 | | - is_volume = mesh.is_volume |
85 | | - winding = mesh.is_winding_consistent |
86 | | - |
87 | | - tic = g.time.time() |
88 | | - # flip faces to break winding |
89 | | - mesh.faces[:4] = g.np.fliplr(mesh.faces[:4]) |
90 | | - |
91 | | - # run the operation |
92 | | - mesh.fix_normals() |
93 | | - |
94 | | - # make sure mesh is repaired to former glory |
95 | | - assert mesh.is_volume == is_volume |
96 | | - assert mesh.is_winding_consistent == winding |
97 | | - |
98 | | - # save timings |
99 | | - timing[mesh.source.file_name] = g.time.time() - tic |
100 | | - # print timings as a warning |
101 | | - g.log.warning(g.json.dumps(timing, indent=4)) |
102 | | - |
103 | | - def test_inversion(self): |
104 | | - """Make sure fix_inversion switches all reversed faces back""" |
105 | | - orig_mesh = g.get_mesh("unit_cube.STL") |
106 | | - orig_verts = orig_mesh.vertices.copy() |
107 | | - orig_faces = orig_mesh.faces.copy() |
108 | | - |
109 | | - mesh = g.Trimesh(orig_verts, orig_faces[:, ::-1]) |
110 | | - inv_faces = mesh.faces.copy() |
111 | | - # check not fixed on the way in |
112 | | - assert not g.np.allclose(inv_faces, orig_faces) |
113 | | - |
114 | | - g.trimesh.repair.fix_inversion(mesh) |
115 | | - assert not g.np.allclose(mesh.faces, inv_faces) |
116 | | - assert g.np.allclose(mesh.faces, orig_faces) |
117 | | - |
118 | | - def test_multi(self): |
119 | | - """ |
120 | | - Try repairing a multibody geometry |
121 | | - """ |
122 | | - # create a multibody mesh with two cubes |
123 | | - a = g.get_mesh("unit_cube.STL") |
124 | | - b = a.copy() |
125 | | - b.apply_translation([2, 0, 0]) |
126 | | - m = a + b |
127 | | - # should be a volume: watertight, correct winding |
128 | | - assert m.is_volume |
129 | | - |
130 | | - # flip one face of A |
131 | | - a.faces[:1] = g.np.fliplr(a.faces[:1]) |
132 | | - # flip every face of A |
133 | | - a.invert() |
134 | | - # flip one face of B |
135 | | - b.faces[:1] = g.np.fliplr(b.faces[:1]) |
136 | | - m = a + b |
137 | | - |
138 | | - # not a volume |
139 | | - assert not m.is_volume |
140 | | - |
141 | | - m.fix_normals(multibody=False) |
142 | | - |
143 | | - # shouldn't fix inversion of one cube |
144 | | - assert not m.is_volume |
145 | | - |
146 | | - # run fix normal with multibody mode |
147 | | - m.fix_normals() |
148 | | - |
149 | | - # should be volume again |
150 | | - assert m.is_volume |
151 | | - |
152 | | - # mesh should be volume of two boxes, and positive |
153 | | - assert g.np.isclose(m.volume, 2.0) |
154 | | - |
155 | | - def test_flip(self): |
156 | | - # create two spheres |
157 | | - a = g.trimesh.creation.icosphere() |
158 | | - b = g.trimesh.creation.icosphere().apply_translation([2, 3, 0]) |
159 | | - # invert the second sphere |
160 | | - b.faces = g.np.fliplr(b.faces) |
161 | | - m = a + b |
162 | | - # make sure normals are in cache |
163 | | - assert m.face_normals.shape == m.faces.shape |
164 | | - m.fix_normals(multibody=True) |
165 | | - assert g.np.isclose(m.volume, a.volume * 2.0) |
166 | | - |
167 | | - def test_fan(self): |
168 | | - # start by creating an icosphere and removing |
169 | | - # all faces that include a single vertex to make |
170 | | - # a nice hole in the mesh |
171 | | - m = g.trimesh.creation.icosphere() |
172 | | - clip = m.vertex_faces[0] |
173 | | - clip = clip[clip >= 0] |
174 | | - assert len(clip) > 4 |
175 | | - mask = g.np.ones(len(m.faces), dtype=bool) |
176 | | - mask[clip] = False |
177 | | - |
178 | | - # should have been watertight |
179 | | - assert m.is_watertight |
180 | | - assert m.is_winding_consistent |
181 | | - m.update_faces(mask) |
182 | | - # now should not be watertight |
183 | | - assert not m.is_watertight |
184 | | - assert m.is_winding_consistent |
185 | | - |
186 | | - # create a triangle fan to cover the hole |
187 | | - stitch = g.trimesh.repair.stitch(m) |
188 | | - # should be an (n, 3) int |
189 | | - assert len(stitch.shape) == 2 |
190 | | - assert stitch.shape[1] == 3 |
191 | | - assert stitch.dtype.kind == "i" |
192 | | - |
193 | | - # now check our stitch to see if it handled the hole |
194 | | - repair = g.trimesh.Trimesh( |
195 | | - vertices=m.vertices.copy(), faces=g.np.vstack((m.faces, stitch)) |
196 | | - ) |
197 | | - assert repair.is_watertight |
198 | | - assert repair.is_winding_consistent |
| 73 | + ] |
| 74 | + |
| 75 | + for i, mesh in enumerate(meshes): |
| 76 | + # turn scenes into multibody meshes |
| 77 | + if g.trimesh.util.is_instance_named(mesh, "Scene"): |
| 78 | + meta = mesh.metadata |
| 79 | + meshes[i] = mesh.dump().sum() |
| 80 | + meshes[i].metadata = meta |
| 81 | + |
| 82 | + timing = {} |
| 83 | + for mesh in meshes: |
| 84 | + # save the initial state |
| 85 | + is_volume = mesh.is_volume |
| 86 | + winding = mesh.is_winding_consistent |
| 87 | + |
| 88 | + tic = g.time.time() |
| 89 | + # flip faces to break winding |
| 90 | + mesh.faces[:4] = g.np.fliplr(mesh.faces[:4]) |
| 91 | + |
| 92 | + # run the operation |
| 93 | + mesh.fix_normals() |
| 94 | + |
| 95 | + # make sure mesh is repaired to former glory |
| 96 | + assert mesh.is_volume == is_volume |
| 97 | + assert mesh.is_winding_consistent == winding |
| 98 | + |
| 99 | + # save timings |
| 100 | + timing[mesh.source.file_name] = g.time.time() - tic |
| 101 | + # print timings as a warning |
| 102 | + g.log.warning(g.json.dumps(timing, indent=4)) |
| 103 | + |
| 104 | + |
| 105 | +def test_inversion(): |
| 106 | + """Make sure fix_inversion switches all reversed faces back""" |
| 107 | + orig_mesh = g.get_mesh("unit_cube.STL") |
| 108 | + orig_verts = orig_mesh.vertices.copy() |
| 109 | + orig_faces = orig_mesh.faces.copy() |
| 110 | + |
| 111 | + mesh = g.Trimesh(orig_verts, orig_faces[:, ::-1]) |
| 112 | + inv_faces = mesh.faces.copy() |
| 113 | + # check not fixed on the way in |
| 114 | + assert not g.np.allclose(inv_faces, orig_faces) |
| 115 | + |
| 116 | + g.trimesh.repair.fix_inversion(mesh) |
| 117 | + assert not g.np.allclose(mesh.faces, inv_faces) |
| 118 | + assert g.np.allclose(mesh.faces, orig_faces) |
| 119 | + |
| 120 | + |
| 121 | +def test_multi(): |
| 122 | + """ |
| 123 | + Try repairing a multibody geometry |
| 124 | + """ |
| 125 | + # create a multibody mesh with two cubes |
| 126 | + a = g.get_mesh("unit_cube.STL") |
| 127 | + b = a.copy() |
| 128 | + b.apply_translation([2, 0, 0]) |
| 129 | + m = a + b |
| 130 | + # should be a volume: watertight, correct winding |
| 131 | + assert m.is_volume |
| 132 | + |
| 133 | + # flip one face of A |
| 134 | + a.faces[:1] = g.np.fliplr(a.faces[:1]) |
| 135 | + # flip every face of A |
| 136 | + a.invert() |
| 137 | + # flip one face of B |
| 138 | + b.faces[:1] = g.np.fliplr(b.faces[:1]) |
| 139 | + m = a + b |
| 140 | + |
| 141 | + # not a volume |
| 142 | + assert not m.is_volume |
| 143 | + |
| 144 | + m.fix_normals(multibody=False) |
| 145 | + |
| 146 | + # shouldn't fix inversion of one cube |
| 147 | + assert not m.is_volume |
| 148 | + |
| 149 | + # run fix normal with multibody mode |
| 150 | + m.fix_normals() |
| 151 | + |
| 152 | + # should be volume again |
| 153 | + assert m.is_volume |
| 154 | + |
| 155 | + # mesh should be volume of two boxes, and positive |
| 156 | + assert g.np.isclose(m.volume, 2.0) |
| 157 | + |
| 158 | + |
| 159 | +def test_flip(): |
| 160 | + # create two spheres |
| 161 | + a = g.trimesh.creation.icosphere() |
| 162 | + b = g.trimesh.creation.icosphere().apply_translation([2, 3, 0]) |
| 163 | + # invert the second sphere |
| 164 | + b.faces = g.np.fliplr(b.faces) |
| 165 | + m = a + b |
| 166 | + # make sure normals are in cache |
| 167 | + assert m.face_normals.shape == m.faces.shape |
| 168 | + m.fix_normals(multibody=True) |
| 169 | + assert g.np.isclose(m.volume, a.volume * 2.0) |
| 170 | + |
| 171 | + |
| 172 | +def test_fan(): |
| 173 | + # start by creating an icosphere and removing |
| 174 | + # all faces that include a single vertex to make |
| 175 | + # a nice hole in the mesh |
| 176 | + m = g.trimesh.creation.icosphere() |
| 177 | + clip = m.vertex_faces[0] |
| 178 | + clip = clip[clip >= 0] |
| 179 | + assert len(clip) > 4 |
| 180 | + mask = g.np.ones(len(m.faces), dtype=bool) |
| 181 | + mask[clip] = False |
| 182 | + |
| 183 | + # should have been watertight |
| 184 | + assert m.is_watertight |
| 185 | + assert m.is_winding_consistent |
| 186 | + m.update_faces(mask) |
| 187 | + # now should not be watertight |
| 188 | + assert not m.is_watertight |
| 189 | + assert m.is_winding_consistent |
| 190 | + |
| 191 | + # create a triangle fan to cover the hole |
| 192 | + stitch = g.trimesh.repair.stitch(m) |
| 193 | + # should be an (n, 3) int |
| 194 | + assert len(stitch.shape) == 2 |
| 195 | + assert stitch.shape[1] == 3 |
| 196 | + assert stitch.dtype.kind == "i" |
| 197 | + |
| 198 | + # now check our stitch to see if it handled the hole |
| 199 | + repair = g.trimesh.Trimesh( |
| 200 | + vertices=m.vertices.copy(), faces=g.np.vstack((m.faces, stitch)) |
| 201 | + ) |
| 202 | + assert repair.is_watertight |
| 203 | + assert repair.is_winding_consistent |
199 | 204 |
|
200 | 205 |
|
201 | 206 | if __name__ == "__main__": |
|
0 commit comments