def test_concat(self): a = g.get_mesh('ballA.off') b = g.get_mesh('ballB.off') hA = a.md5() hB = b.md5() # make sure we're not mutating original mesh for i in range(4): c = a + b assert g.np.isclose(c.volume, a.volume + b.volume) assert a.md5() == hA assert b.md5() == hB count = 5 meshes = [] for i in range(count): m = a.copy() m.apply_translation([a.scale, 0, 0]) meshes.append(m) # do a multimesh concatenate r = g.trimesh.util.concatenate(meshes) assert g.np.isclose(r.volume, a.volume * count) assert a.md5() == hA
def test_ply(self): m = g.get_mesh('machinist.XAML') assert m.visual.kind == 'face' assert m.visual.face_colors.ptp(axis=0).max() > 0 export = m.export(file_type='ply') reconstructed = g.trimesh.load(g.trimesh.util.wrap_as_stream(export), file_type='ply') assert reconstructed.visual.kind == 'face' assert g.np.allclose(reconstructed.visual.face_colors, m.visual.face_colors) m = g.get_mesh('reference.ply') assert m.visual.kind == 'vertex' assert m.visual.vertex_colors.ptp(axis=0).max() > 0 export = m.export(file_type='ply') reconstructed = g.trimesh.load(g.trimesh.util.wrap_as_stream(export), file_type='ply') assert reconstructed.visual.kind == 'vertex' assert g.np.allclose(reconstructed.visual.vertex_colors, m.visual.vertex_colors)
def test_components(self): # a soup of random triangles, with no adjacent pairs soup = g.get_mesh('soup.stl') # a mesh with multiple watertight bodies mult = g.get_mesh('cycloidal.ply') # a mesh with a single watertight body sing = g.get_mesh('featuretype.STL') # mesh with a single tetrahedron tet = g.get_mesh('tet.ply') for engine in self.engines: # without requiring watertight the split should be into every face split = soup.split(only_watertight=False, engine=engine) assert len(split) == len(soup.faces) # with watertight there should be an empty list split = soup.split(only_watertight=True, engine=engine) assert len(split) == 0 split = mult.split(only_watertight=False, engine=engine) assert len(split) >= 119 split = mult.split(only_watertight=True, engine=engine) assert len(split) >= 117 # random triangles should have no facets facets = g.trimesh.graph.facets(mesh=soup, engine=engine) assert len(facets) == 0 facets = g.trimesh.graph.facets(mesh=mult, engine=engine) assert all(len(i) >= 2 for i in facets) assert len(facets) >= 8654 split = sing.split(only_watertight=False, engine=engine) assert len(split) == 1 assert split[0].is_watertight assert split[0].is_winding_consistent split = sing.split(only_watertight=True, engine=engine) assert len(split) == 1 assert split[0].is_watertight assert split[0].is_winding_consistent # single tetrahedron assert tet.is_volume assert tet.body_count == 1 # regardless of method or flag we should have one body result split = tet.split(only_watertight=True, engine=engine) assert len(split) == 1 split = tet.split(only_watertight=False, engine=engine) assert len(split) == 1
def test_3MF(self): # an assembly with instancing s = g.get_mesh('counterXP.3MF') # should be 2 unique meshes assert len(s.geometry) == 2 # should be 6 instances around the scene assert len(s.graph.nodes_geometry) == 6 # a single body 3MF assembly s = g.get_mesh('featuretype.3MF') # should be 2 unique meshes assert len(s.geometry) == 1 # should be 6 instances around the scene assert len(s.graph.nodes_geometry) == 1
def test_vertex_adjacency_graph(self): f = g.trimesh.graph.vertex_adjacency_graph # a mesh with a single watertight body sing = g.get_mesh('featuretype.STL') vert_adj_g = f(sing) assert len(sing.vertices) == len(vert_adj_g)
def setUp(self): # inertia numbers pulled from solidworks self.truth = g.data['mass_properties'] self.meshes = dict() for data in self.truth: filename = data['filename'] self.meshes[filename] = g.get_mesh(filename)
def test_text(self): # load file with a single text entity original = g.get_mesh('2D/text.dxf') # export then reload roundtrip = g.trimesh.load( file_obj=g.io_wrap(original.export(file_type='dxf')), file_type='dxf') for d in [original, roundtrip]: # should contain a single Text entity assert len(d.entities) == 1 # shouldn't crash anything assert len(d.polygons_closed) == 0 assert len(d.polygons_full) == 0 assert len(d.discrete) == 0 assert len(d.paths) == 0 # make sure it preserved case and special chars assert d.entities[0].text == "HEY WHAT's poppin" # height should 1.0 assert g.np.isclose(d.entities[0].height, 1.0) # get the 2D rotation of the text angle = d.entities[0].angle(d.vertices) # angle should be 30 degrees assert g.np.isclose(angle, g.np.radians(30.0))
def test_scene_id(self): """ A scene has a nicely constructed transform tree, so make sure transforming meshes around it doesn't change the nuts of their identifier hash. """ scenes = [g.get_mesh('cycloidal.3DXML')] for s in scenes: for geom_name, mesh in s.geometry.items(): meshes = [] for node in s.graph.nodes_geometry: T, geo = s.graph[node] if geom_name != geo: continue m = s.geometry[geo].copy() m.apply_transform(T) meshes.append(m) if not all(meshes[0].identifier_md5 == i.identifier_md5 for i in meshes): raise ValueError( '{} differs after transform!'.format(geom_name)) assert (scenes[0].geometry['disc_cam_B'].identifier_md5 != scenes[0].geometry['disc_cam_A'].identifier_md5)
def test_units(self): fake_units = 'blorbs' self.assertFalse(g.trimesh.units.validate(fake_units)) m = g.get_mesh('featuretype.STL') self.assertTrue(m.units is None) with self.assertRaises(ValueError): m.units = fake_units self.assertTrue(m.units is None) m.units = 'in' self.assertTrue(m.units == 'in') extents_pre = m.extents with self.assertRaises(ValueError): m.convert_units(fake_units) self.assertTrue(m.units == 'in') m.convert_units('mm') scale = g.np.divide(m.extents, extents_pre) self.assertTrue(g.np.allclose(scale, 25.4)) self.assertTrue(m.units == 'mm')
def test_split(self): for fn in ['2D/ChuteHolderPrint.DXF', '2D/tray-easy1.dxf', '2D/sliding-base.dxf', '2D/wrench.dxf', '2D/spline_1.dxf']: p = g.get_mesh(fn) # make sure something was loaded assert len(p.root) > 0 # split by connected split = p.split() # make sure split parts have same area as source assert g.np.isclose(p.area, sum(i.area for i in split)) # make sure concatenation doesn't break that assert g.np.isclose(p.area, g.np.sum(split).area) # check that cache didn't screw things up for s in split: assert len(s.root) == 1 assert len(s.path_valid) == len(s.paths) assert len(s.paths) == len(s.discrete) assert s.path_valid.sum() == len(s.polygons_closed) g.check_path2D(s)
def test_vhacd(self): # exit if no VHACD if not g.trimesh.interfaces.vhacd.exists and not g.all_dep: g.log.warning( 'not testing convex decomposition (no vhacd)!') return g.log.info('testing convex decomposition using vhacd') mesh = g.get_mesh('bunny.ply') # run a convex decomposition using vhacd decomposed = mesh.convex_decomposition(maxhulls=10) # it should return the correct number of meshes assert len(decomposed) == 10 # make sure everything is convex # also this will fail if the type is returned incorrectly assert all(i.is_convex for i in decomposed) # make sure every result is actually a volume # ie watertight, consistent winding, positive nonzero volume assert all(i.is_volume for i in decomposed)
def test_section(self): mesh = g.get_mesh('tube.obj') # check the CCW correctness with a normal in both directions for sign in [1.0, -1.0]: # get a cross section of the tube section = mesh.section(plane_origin=mesh.center_mass, plane_normal=[0.0, sign, 0.0]) # Path3D -> Path2D planar, T = section.to_planar() # tube should have one closed polygon assert len(planar.polygons_full) == 1 polygon = planar.polygons_full[0] # closed polygon should have one interior assert len(polygon.interiors) == 1 # the exterior SHOULD be counterclockwise assert g.trimesh.path.util.is_ccw( polygon.exterior.coords) # the interior should NOT be counterclockwise assert not g.trimesh.path.util.is_ccw( polygon.interiors[0].coords) # should be a valid Path2D g.check_path2D(planar)
def test_cylinder(self): """ Check bounding cylinders on basically a cuboid """ # not rotationally symmetric mesh = g.get_mesh('featuretype.STL') height = 10.0 radius = 1.0 # spherical coordinates to loop through sphere = g.trimesh.util.grid_linspace( [[0, 0], [g.np.pi * 2, g.np.pi * 2]], 5) for s in sphere: T = g.trimesh.transformations.spherical_matrix(*s) p = g.trimesh.creation.cylinder(radius=radius, height=height, transform=T) assert g.np.isclose(radius, p.bounding_cylinder.primitive.radius, rtol=.01) assert g.np.isclose(height, p.bounding_cylinder.primitive.height, rtol=.01) # regular mesh should have the same bounding cylinder # regardless of transform copied = mesh.copy() copied.apply_transform(T) assert g.np.isclose(mesh.bounding_cylinder.volume, copied.bounding_cylinder.volume, rtol=.05)
def test_multiple(self): for mesh in [g.trimesh.creation.icosahedron(), g.get_mesh('unit_cube.STL')]: vectors = g.trimesh.util.grid_linspace([[0.0, 0], [1, 1.0]], 5)[1:] vectors = g.trimesh.unitize(g.np.column_stack( (vectors, g.np.ones(len(vectors))))) for vector, angle in zip( vectors, g.np.linspace(0.0, g.np.pi, len(vectors))): matrix = g.trimesh.transformations.rotation_matrix( angle, vector) copied = mesh.copy() copied.apply_transform(matrix) # Compute the stable poses of the icosahedron trans, probs = copied.compute_stable_poses() # we are only testing primitives with point symmetry # AKA 3 principal components of inertia are the same facet_count = len(mesh.facets) if facet_count == 0: facet_count = len(mesh.faces) probability = 1.0 / float(facet_count) assert g.np.allclose(g.np.array(probs) - probability, 0.0)
def get_readonly(model_name): """ Get a mesh and make vertices and faces read only. Parameters ------------ model_name : str Model name in models directory Returns ----------- mesh : trimesh.Trimesh Geometry with read-only data verts : (n, 3) float Read- only vertices faces : (m, 3) int Read- only faces """ original = g.get_mesh(model_name) # get the original data from the mesh verts = original.vertices faces = original.faces # use the buffer interface to generate read-only arrays verts = g.np.ndarray(verts.shape, verts.dtype, bytes(verts.tostring())) faces = g.np.ndarray(faces.shape, faces.dtype, bytes(faces.tostring())) # everything should be read only now assert not verts.flags['WRITEABLE'] assert not faces.flags['WRITEABLE'] mesh = g.trimesh.Trimesh(verts, faces, process=False, validate=False) assert not mesh.vertices.flags['WRITEABLE'] assert not mesh.faces.flags['WRITEABLE'] # return the mesh, and read-only vertices and faces return mesh, verts, faces
def test_gltf(self): # split a multibody mesh into a scene scene = g.trimesh.scene.split_scene( g.get_mesh('cycloidal.ply')) # should be 117 geometries assert len(scene.geometry) >= 117 # a dict with {file name: str} export = scene.export('gltf') # load from just resolver r = g.trimesh.load(file_obj=None, file_type='gltf', resolver=export) # will assert round trip is roughly equal g.scene_equal(r, scene) # try loading from a ZIP archive zipped = g.trimesh.util.compress(export) r = g.trimesh.load( file_obj=g.trimesh.util.wrap_as_stream(zipped), file_type='zip') # try loading from a file name # will require a file path resolver with g.TemporaryDirectory() as d: for file_name, data in export.items(): with open(g.os.path.join(d, file_name), 'wb') as f: f.write(data) # load from file path of header GLTF rd = g.trimesh.load( g.os.path.join(d, 'model.gltf')) # will assert round trip is roughly equal g.scene_equal(rd, scene)
def test_edges(self): """ Test edges_to_polygon """ m = g.get_mesh('featuretype.STL') # get a polygon for the second largest facet index = m.facets_area.argsort()[-2] normal = m.facets_normal[index] origin = m._cache['facets_origin'][index] T = g.trimesh.geometry.plane_transform(origin, normal) vertices = g.trimesh.transform_points(m.vertices, T)[:, :2] # find boundary edges for the facet edges = m.edges_sorted.reshape( (-1, 6))[m.facets[index]].reshape((-1, 2)) group = g.trimesh.grouping.group_rows(edges, require_count=1) # run the polygon conversion polygon = g.trimesh.path.polygons.edges_to_polygons( edges=edges[group], vertices=vertices) assert len(polygon) == 1 assert g.np.isclose(polygon[0].area, m.facets_area[index]) # try transforming the polygon around M = g.np.eye(3) M[0][2] = 10.0 P2 = g.trimesh.path.polygons.transform_polygon(polygon[0], M) distance = g.np.array(P2.centroid) - g.np.array(polygon[0].centroid) assert g.np.allclose(distance, [10.0, 0])
def test_obj_quad(self): mesh = g.get_mesh('quadknot.obj') # make sure some data got loaded assert g.trimesh.util.is_shape(mesh.faces, (-1, 3)) assert g.trimesh.util.is_shape(mesh.vertices, (-1, 3)) assert mesh.is_watertight assert mesh.is_winding_consistent
def test_dict(self): mesh = g.get_mesh('machinist.XAML') assert mesh.visual.kind == 'face' mesh.visual.vertex_colors = mesh.visual.vertex_colors assert mesh.visual.kind == 'vertex' as_dict = mesh.to_dict() back = g.trimesh.Trimesh(**as_dict) # NOQA
def test_shoulder(self): if collada is None: g.log.error('no pycollada to test!') return scene = g.get_mesh('shoulder.zae') assert len(scene.geometry) == 3 assert len(scene.graph.nodes_geometry) == 3
def test_triangulate_plumbing(self): """ Check the plumbing of path triangulation """ if len(self.engines) == 0: return p = g.get_mesh('2D/ChuteHolderPrint.DXF') v, f = p.triangulate() check_triangulation(v, f, p.area)
def test_on_edge(self): for use_embree in [True, False]: m = g.get_mesh('7_8ths_cube.stl') points = [[4.5, 0, -23], [4.5, 0, -2], [0, 0, -1e-6], [0, 0, -1]] truth = [False, True, True, True] result = g.trimesh.ray.ray_util.contains_points(m.ray, points) assert (result == truth).all()
def test_obj(self): m = g.get_mesh('textured_tetrahedron.obj', process=False) export = m.export(file_type='obj') reconstructed = g.trimesh.load(g.trimesh.util.wrap_as_stream(export), file_type='obj', process=False) # test that we get at least the same number of normals and texcoords out; # the loader may reorder vertices, so we shouldn't check direct # equality assert m.vertex_normals.shape == reconstructed.vertex_normals.shape
def test_scene(self): try: import fcl # NOQA except ImportError: return scene = g.get_mesh('cycloidal.3DXML') manager, objects = g.trimesh.collision.scene_to_collision(scene) assert manager.in_collision_internal()
def test_obj_split_attributes(self): # test a wavefront file where pos/uv/norm have different indices # and where multiple objects share vertices # Note 'process=False' to avoid merging vertices meshes = g.get_mesh('joined_tetrahedra.obj', process=False) self.assertTrue(len(meshes) == 2) assert g.trimesh.util.is_shape(meshes[0].faces, (4, 3)) assert g.trimesh.util.is_shape(meshes[0].vertices, (9, 3)) assert g.trimesh.util.is_shape(meshes[1].faces, (4, 3)) assert g.trimesh.util.is_shape(meshes[1].vertices, (9, 3))
def test_contains(self): mesh = g.get_mesh('unit_cube.STL') scale = 1+(g.trimesh.constants.tol.merge*2) test_on = mesh.contains(mesh.vertices) test_in = mesh.contains(mesh.vertices * (1.0/scale)) test_out = mesh.contains(mesh.vertices * scale) #assert test_on.all() self.assertTrue(test_in.all()) self.assertFalse(test_out.any())
def test_tex_export(self): # load textured PLY mesh = g.get_mesh('fuze.ply') assert hasattr(mesh.visual, 'uv') # make sure export as GLB doesn't crash on scenes export = mesh.scene().export(file_type='glb') assert len(export) > 0 # make sure it works on meshes export = mesh.export(file_type='glb') assert len(export) > 0
def test_facet(self): m = g.get_mesh('featuretype.STL') assert len(m.facets) > 0 assert len(m.facets) == len(m.facets_boundary) assert len(m.facets) == len(m.facets_normal) assert len(m.facets) == len(m.facets_area) assert len(m.facets) == len(m.facets_on_hull) # this mesh should have 8 facets on the convex hull assert m.facets_on_hull.astype(int).sum() == 8
def test_box(self): """ Run box- ray intersection along Z and make sure XY match ray origin XY. """ for kwargs in [{'use_embree': True}, {'use_embree': False}]: mesh = g.get_mesh('unit_cube.STL', **kwargs) # grid is across meshes XY profile origins = g.trimesh.util.grid_linspace(mesh.bounds[:, :2] + g.np.reshape( [-.02, .02], (-1, 1)), 100) origins = g.np.column_stack(( origins, g.np.ones(len(origins)) * -100)) # all vectors are along Z axis vectors = g.np.ones((len(origins), 3)) * [0, 0, 1.0] # (n,3) float intersection position in space # (n,) int, index of original ray # (m,) int, index of mesh.faces pos, ray, tri = mesh.ray.intersects_location( ray_origins=origins, ray_directions=vectors) for p, r in zip(pos, ray): # intersect location XY should match ray origin XY assert g.np.allclose(p[:2], origins[r][:2]) # the Z of the hit should be on the cube's # top or bottom face assert g.np.isclose(p[2], mesh.bounds[:, 2]).any() def test_broken(self): """ Test a mesh with badly defined face normals """ ray_origins = g.np.array([[0.12801793, 24.5030052, -5.], [0.12801793, 24.5030052, -5.]]) ray_directions = g.np.array([[-0.13590759, -0.98042506, 0.], [0.13590759, 0.98042506, -0.]]) for kwargs in [{'use_embree': True}, {'use_embree': False}]: mesh = g.get_mesh('broken.STL', **kwargs) locations, index_ray, index_tri = mesh.ray.intersects_location( ray_origins=ray_origins, ray_directions=ray_directions) # should be same number of location hits assert len(locations) == len(ray_origins)
def test_path(self): p = g.get_mesh('2D/tray-easy1.dxf') # should be inches assert 'in' in p.units extents_pre = p.extents p.convert_units('mm') # should have converted in -> mm 25.4 # extents should scale exactly with unit conversion assert g.np.allclose(p.extents / extents_pre, 25.4, atol=.01)
def test_winding(self): """ Reverse some faces and make sure fix_face_winding flips them back. """ meshes = [g.get_mesh(i) for i in ['unit_cube.STL', 'machinist.XAML', 'round.stl', 'quadknot.obj', 'soup.stl']] for i, mesh in enumerate(meshes): # turn scenes into multibody meshes if g.trimesh.util.is_instance_named(mesh, 'Scene'): meta = mesh.metadata meshes[i] = mesh.dump().sum() meshes[i].metadata = meta timing = {} for mesh in meshes: # save the initial state is_volume = mesh.is_volume winding = mesh.is_winding_consistent tic = g.time.time() # flip faces to break winding mesh.faces[:4] = g.np.fliplr(mesh.faces[:4]) # run the operation mesh.fix_normals() # make sure mesh is repaired to former glory assert mesh.is_volume == is_volume assert mesh.is_winding_consistent == winding # save timings timing[mesh.metadata['file_name']] = g.time.time() - tic # print timings as a warning g.log.warning(g.json.dumps(timing, indent=4))
def test_header(self): m = g.get_mesh('featuretype.STL') # make sure we have the right mesh assert g.np.isclose(m.volume, 11.627733431196749, atol=1e-6) # should have saved the header from the STL file assert len(m.metadata['header']) > 0 # should have saved the STL face attributes assert len(m.face_attributes['stl']) == len(m.faces) assert len(m.faces) > 1000 # add a non-correlated face attribute, which should be ignored m.face_attributes['nah'] = 10 # remove all faces except three random ones m.update_faces([1, 3, 4]) # faces and face attributes should be untouched assert len(m.faces) == 3 assert len(m.face_attributes['stl']) == 3 # attribute that wasn't len(m.faces) shouldn't have been touched assert m.face_attributes['nah'] == 10
def test_contains(self): scale = 1.5 for use_embree in [True, False]: mesh = g.get_mesh('unit_cube.STL', use_embree=use_embree) g.log.info('Contains test ray engine: ' + str(mesh.ray.__class__)) test_on = mesh.ray.contains_points(mesh.vertices) test_in = mesh.ray.contains_points(mesh.vertices * (1.0 / scale)) assert test_in.all() test_out = mesh.ray.contains_points(mesh.vertices * scale) assert not test_out.any() points_way_out = ( g.np.random.random( (30, 3)) * 100) + 1.0 + mesh.bounds[1] test_way_out = mesh.ray.contains_points(points_way_out) assert not test_way_out.any() test_centroid = mesh.ray.contains_points([mesh.center_mass]) assert test_centroid.all()
def test_hash(self): setup = 'import numpy, trimesh;' setup += 'd = numpy.random.random((10000,3));' setup += 't = trimesh.caching.tracked_array(d)' count = 10000 g.timeit.timeit(setup=setup, stmt='t._modified_m=True;t.md5()', number=count) g.timeit.timeit(setup=setup, stmt='t._modified_c=True;t.crc()', number=count) g.timeit.timeit(setup=setup, stmt='t._modified_x=True;t.fast_hash()', number=count) m = g.get_mesh('featuretype.STL') # log result values g.log.info('\nResult\nMD5:\n{}\nCRC:\n{}\nXX:\n{}'.format( m.vertices.md5(), m.vertices.crc(), m.vertices.fast_hash()))
def test_successors(self): s = g.get_mesh('CesiumMilkTruck.glb') assert len(s.graph.nodes_geometry) == 5 # world should be root frame assert (s.graph.transforms.successors( s.graph.base_frame) == s.graph.nodes) for n in s.graph.nodes: # successors should always return subset of nodes succ = s.graph.transforms.successors(n) assert succ.issubset(s.graph.nodes) # we self-include node in successors assert n in succ # test getting a subscene from successors ss = s.subscene('3') assert len(ss.geometry) == 1 assert len(ss.graph.nodes_geometry) == 1 assert isinstance(s.graph.to_networkx(), g.nx.DiGraph)
def test_split(self): for fn in [ '2D/ChuteHolderPrint.DXF', '2D/tray-easy1.dxf', '2D/sliding-base.dxf', '2D/wrench.dxf', '2D/spline_1.dxf' ]: p = g.get_mesh(fn) # split by connected split = p.split() # make sure split parts have same area as source assert g.np.isclose(p.area, sum(i.area for i in split)) # make sure concatenation doesn't break that assert g.np.isclose(p.area, g.np.sum(split).area) # check that cache didn't screw things up for s in split: assert len(s.root) == 1 assert len(s.path_valid) == len(s.paths) assert len(s.paths) == len(s.discrete) assert s.path_valid.sum() == len(s.polygons_closed)
def test_vertex_attributes(self): """ Test writing vertex attributes to a ply, by reading them back and asserting the written attributes array matches """ m = g.get_mesh('box.STL') test_1d_attribute = g.np.copy(m.vertices[:, 0]) test_nd_attribute = g.np.copy(m.vertices) m.vertex_attributes['test_1d_attribute'] = test_1d_attribute m.vertex_attributes['test_nd_attribute'] = test_nd_attribute export = m.export(file_type='ply') reconstructed = g.wrapload(export, file_type='ply') vertex_attributes = reconstructed.metadata['ply_raw']['vertex']['data'] result_1d = vertex_attributes['test_1d_attribute'] result_nd = vertex_attributes['test_nd_attribute']['f1'] g.np.testing.assert_almost_equal(result_1d, test_1d_attribute) g.np.testing.assert_almost_equal(result_nd, test_nd_attribute)
def test_poly(self): p = g.get_mesh('2D/LM2.dxf') assert p.is_closed assert any( len(i.points) > 2 for i in p.entities if g.trimesh.util.is_instance_named(i, 'Line')) assert len(p.layers) == len(p.entities) assert len(g.np.unique(p.layers)) > 1 p.explode() assert all( len(i.points) == 2 for i in p.entities if g.trimesh.util.is_instance_named(i, 'Line')) assert p.is_closed p.entities = p.entities[:-1] assert not p.is_closed # fill gaps of any distance p.fill_gaps(g.np.inf) assert p.is_closed
def test_conversion(self): # test conversions on a multibody STL in a scene # a multibody STL with a unit hint in filename m = g.get_mesh('counter.unitsmm.STL') # nothing should be set assert m.units is None # split into watertight bodies s = g.trimesh.scene.split_scene(m) # save the extents extents_pre = s.extents # should extract units from file name without # raising a ValueError c = s.convert_units('in', guess=False) # should have converted mm -> in, 1/25.4 # extents should scale exactly with unit conversion assert g.np.allclose(extents_pre / c.extents, 25.4, atol=.01)
def test_empty(self): """ Test queries with no hits """ for use_embree in [True, False]: dimension = (100, 3) sphere = g.get_mesh('unit_sphere.STL', use_embree=use_embree) # should never hit the sphere ray_origins = g.np.random.random(dimension) ray_directions = g.np.tile([0, 1, 0], (dimension[0], 1)) ray_origins[:, 2] = -5 # make sure ray functions always return numpy arrays # these functions return multiple results all of which # should always be a numpy array assert all( len(i.shape) >= 0 for i in sphere.ray.intersects_id(ray_origins, ray_directions)) assert all( len(i.shape) >= 0 for i in sphere.ray.intersects_location( ray_origins, ray_directions))
def test_mtl(self): # get a mesh with texture m = g.get_mesh('fuze.obj') # export the mesh including data obj, data = g.trimesh.exchange.export.export_obj(m, include_texture=True) with g.trimesh.util.TemporaryDirectory() as path: # where is the OBJ file going to be saved obj_path = g.os.path.join(path, 'test.obj') with open(obj_path, 'w') as f: f.write(obj) # save the MTL and images for k, v in data.items(): with open(g.os.path.join(path, k), 'wb') as f: f.write(v) # reload the mesh from the export rec = g.trimesh.load(obj_path) # make sure loaded image is the same size as the original assert (rec.visual.material.image.size == m.visual.material.image.size) # make sure the faces are the same size assert rec.faces.shape == m.faces.shape
def test_dupe(self): m = g.get_mesh('tube.obj') assert m.body_count == 1 s = g.trimesh.scene.split_scene(m) assert len(s.graph.nodes) == 2 assert len(s.graph.nodes_geometry) == 1 assert len(s.duplicate_nodes) == 1 assert len(s.duplicate_nodes[0]) == 1 c = s.copy() assert len(c.graph.nodes) == 2 assert len(c.graph.nodes_geometry) == 1 assert len(c.duplicate_nodes) == 1 assert len(c.duplicate_nodes[0]) == 1 u = s.convert_units('in', guess=True) assert len(u.graph.nodes_geometry) == 1 assert len(u.duplicate_nodes) == 1 assert len(u.duplicate_nodes[0]) == 1
def test_rps(self): for use_embree in [True, False]: dimension = (10000, 3) sphere = g.get_mesh('unit_sphere.STL', use_embree=use_embree) ray_origins = g.np.random.random(dimension) ray_directions = g.np.tile([0, 0, 1], (dimension[0], 1)) ray_origins[:, 2] = -5 # force ray object to allocate tree before timing it #tree = sphere.ray.tree tic = [g.time.time()] sphere.ray.intersects_id(ray_origins, ray_directions) tic.append(g.time.time()) sphere.ray.intersects_location(ray_origins, ray_directions) tic.append(g.time.time()) rps = dimension[0] / g.np.diff(tic) g.log.info('Measured %s rays/second with embree %d', str(rps), use_embree)
def test_identifier(self): count = 25 meshes = g.np.append(g.get_meshes(10), g.get_mesh('fixed_top.ply')) for mesh in meshes: if not mesh.is_volume: g.log.warning('Mesh %s is not watertight!', mesh.metadata['file_name']) continue g.log.info('Trying hash at %d random transforms', count) md5 = g.deque() idf = g.deque() for i in range(count): permutated = mesh.permutate.transform() permutated = permutated.permutate.tesselation() md5.append(permutated.identifier_md5) idf.append(permutated.identifier) result = g.np.array(md5) ok = (result[0] == result[1:]).all() if not ok: debug = [] for a in idf: as_int, exp = g.trimesh.util.sigfig_int( a, g.trimesh.comparison.id_sigfig) debug.append(as_int * (10**exp)) g.log.error('Hashes on %s differ after transform! diffs:\n %s\n', mesh.metadata['file_name'], str(g.np.array(debug, dtype=g.np.int))) raise ValueError('values differ after transform!') if md5[-1] == permutated.permutate.noise( mesh.scale / 100.0).identifier_md5: raise ValueError('Hashes on %s didn\'t change after noise!', mesh.metadata['file_name'])
def test_poly(self): p = g.get_mesh('2D/LM2.dxf') assert p.is_closed # one of the lines should be a polyline assert any(len(e.points) > 2 for e in p.entities if isinstance(e, g.trimesh.path.entities.Line)) # layers should match entity count assert len(p.layers) == len(p.entities) assert len(set(p.layers)) > 1 count = len(p.entities) p.explode() # explode should have created new entities assert len(p.entities) > count # explode should have added some new layers assert len(p.entities) == len(p.layers) # all line segments should have two points now assert all(len(i.points) == 2 for i in p.entities if isinstance(i, g.trimesh.path.entities.Line)) # should still be closed assert p.is_closed # chop off the last entity p.entities = p.entities[:-1] # should no longer be closed assert not p.is_closed # fill gaps of any distance p.fill_gaps(g.np.inf) # should have fixed this puppy assert p.is_closed # remove 2 short edges using remove_entity() count = len(p.entities) p.remove_entities([count - 1, count - 6]) assert not p.is_closed p.fill_gaps(2) assert p.is_closed
def test_scene(self): for mesh in g.get_mesh('cycloidal.ply', 'kinematic.tar.gz', 'sphere.ply'): scene_split = g.trimesh.scene.split_scene(mesh) scene_base = g.trimesh.Scene(mesh) for s in [scene_split, scene_base]: self.assertTrue(len(s.geometry) > 0) flattened = s.graph.to_flattened() g.json.dumps(flattened) edgelist = s.graph.to_edgelist() g.json.dumps(edgelist) assert s.bounds.shape == (2, 3) assert s.centroid.shape == (3,) assert s.extents.shape == (3,) assert isinstance(s.scale, float) assert g.trimesh.util.is_shape(s.triangles, (-1, 3, 3)) assert len(s.triangles) == len(s.triangles_node) assert s.md5() is not None assert len(s.duplicate_nodes) > 0 r = s.dump() for export_format in ['dict', 'dict64']: # try exporting the scene as a dict # then make sure json can serialize it e = g.json.dumps(s.export(export_format)) # reconstitute the dict into a scene r = g.trimesh.load(g.json.loads(e)) # make sure the extents are similar before and after assert g.np.allclose(g.np.product(s.extents), g.np.product(r.extents))
def test_rasterize(self): p = g.get_mesh('2D/wrench.dxf') origin = p.bounds[0] pitch = p.extents.max() / 600 resolution = g.np.ceil(p.extents / pitch).astype(int) # rasterize with filled filled = p.rasterize(origin=origin, pitch=pitch, resolution=resolution, fill=True, width=None) # rasterize just the outline outline = p.rasterize(origin=origin, pitch=pitch, resolution=resolution, fill=False, width=2.0) # rasterize both both = p.rasterize(origin=origin, pitch=pitch, resolution=resolution, fill=True, width=2.0) # count the number of filled pixels fill_cnt = g.np.array(filled).sum() both_cnt = g.np.array(both).sum() outl_cnt = g.np.array(outline).sum() # filled should have more than an outline assert fill_cnt > outl_cnt # filled+outline should have more than outline assert both_cnt > outl_cnt # filled+outline should have more than filled assert both_cnt > fill_cnt
def test_image(self): try: import xatlas # noqa except BaseException: g.log.info('not testing unwrap as no `xatlas`') return a = g.get_mesh('bunny.ply', force="mesh") u = a.unwrap() assert u.visual.uv.shape == (len(u.vertices), 2) checkerboard = g.np.kron([[1, 0] * 4, [0, 1] * 4] * 4, g.np.ones((10, 10))) try: from PIL import Image except BaseException: return image = Image.fromarray((checkerboard * 255).astype(g.np.uint8)) u = a.unwrap(image=image) # make sure image was attached correctly assert u.visual.material.image.size == image.size
def test_text(self): """ Do some checks on Text entities """ p = g.get_mesh('2D/LM2.dxf') p.explode() # get some text entities text = [ e for e in p.entities if isinstance(e, g.trimesh.path.entities.Text) ] assert len(text) > 1 # loop through each of them for t in text: # a spurious error we were seeing in CI if g.trimesh.util.is_instance_named(t, 'Line'): raise ValueError( 'type bases:', [i.__name__ for i in g.trimesh.util.type_bases(t)]) # make sure this doesn't crash with text entities g.trimesh.rendering.convert_to_vertexlist(p)
def test_integrate(self): from trimesh.integrate import symbolic_barycentric import sympy as sp m = g.get_mesh('featuretype.STL') integrator, expr = symbolic_barycentric('1') self.assertTrue(g.np.allclose(integrator(m).sum(), m.area)) x, y, z = sp.symbols('x y z') functions = [x**2 + y**2, x + y + z] for f in functions: integrator, expr = symbolic_barycentric(f) integrator_p, expr_p = symbolic_barycentric(str(f)) g.log.debug('expression %s was integrated to %s', str(f), str(expr)) summed = integrator(m).sum() summed_p = integrator_p(m).sum() self.assertTrue(g.np.allclose(summed, summed_p)) self.assertFalse(g.np.allclose(summed, 0.0))
def test_face_attributes(self): # Test writing face attributes to a ply, by reading # them back and asserting the written attributes array matches m = g.get_mesh('box.STL') test_1d_attribute = g.np.copy(m.face_angles[:, 0]) test_nd_attribute = g.np.copy(m.face_angles) m.face_attributes['test_1d_attribute'] = test_1d_attribute m.face_attributes['test_nd_attribute'] = test_nd_attribute export = m.export(file_type='ply') reconstructed = g.wrapload(export, file_type='ply') face_attributes = reconstructed.metadata['ply_raw']['face']['data'] result_1d = face_attributes['test_1d_attribute'] result_nd = face_attributes['test_nd_attribute']['f1'] g.np.testing.assert_almost_equal(result_1d, test_1d_attribute) g.np.testing.assert_almost_equal(result_nd, test_nd_attribute) no_attr = m.export(file_type='ply', include_attributes=False) assert len(no_attr) < len(export)
def test_poly(self): p = g.get_mesh('2D/LM2.dxf') self.assertTrue(p.is_closed) self.assertTrue( any( len(i.points) > 2 for i in p.entities if g.trimesh.util.is_instance_named(i, 'Line'))) assert len(p.layers) == len(p.entities) assert len(g.np.unique(p.layers)) > 1 p.explode() self.assertTrue( all( len(i.points) == 2 for i in p.entities if g.trimesh.util.is_instance_named(i, 'Line'))) self.assertTrue(p.is_closed) p.entities = p.entities[:-1] self.assertFalse(p.is_closed) p.fill_gaps() self.assertTrue(p.is_closed)
def test_section(self): mesh = g.get_mesh('tube.obj') # check the CCW correctness with a normal in both directions for sign in [1.0, -1.0]: # get a cross section of the tube section = mesh.section(plane_origin=mesh.center_mass, plane_normal=[0.0, sign, 0.0]) # Path3D -> Path2D planar, T = section.to_planar() # tube should have one closed polygon assert len(planar.polygons_full) == 1 polygon = planar.polygons_full[0] # closed polygon should have one interior assert len(polygon.interiors) == 1 # the exterior SHOULD be counterclockwise assert g.trimesh.path.util.is_ccw(polygon.exterior.coords) # the interior should NOT be counterclockwise assert not g.trimesh.path.util.is_ccw(polygon.interiors[0].coords)
def test_simplify(self): for file_name in ['2D/cycloidal.dxf', '2D/125_cycloidal.DXF', '2D/spline_1.dxf']: original = g.get_mesh(file_name) split = original.split() assert g.np.allclose(original.area, sum(i.area for i in split)) for drawing in split: # we split so there should be only one polygon per drawing now assert len(drawing.polygons_full) == 1 polygon = drawing.polygons_full[0] arc_count = sum(int(type(i).__name__ == 'Arc') for i in drawing.entities) self.polygon_simplify(polygon=polygon, arc_count=arc_count)
def test_vhacd(self): if not g.trimesh.interfaces.vhacd.exists: g.log.warning('not testing convex decomposition (no vhacd)!') return g.log.info('testing convex decomposition using vhacd') mesh = g.get_mesh('bunny.ply') # run a convex decomposition using vhacd decomposed = mesh.convex_decomposition(maxhulls=10) # it should return the correct number of meshes assert len(decomposed) == 10 # make sure everything is convex # also this will fail if the type is returned incorrectly assert all(i.is_convex for i in decomposed) # make sure every result is actually a volume # ie watertight, consistent winding, positive nonzero volume assert all(i.is_volume for i in decomposed)
def test_duck(self): scene = g.get_mesh('Duck.glb') # should have one mesh assert len(scene.geometry) == 1 # get the mesh geom = next(iter(scene.geometry.values())) # should not be watertight assert not geom.is_volume # make sure export doesn't crash export = scene.export(file_type='glb') assert len(export) > 0 # check a roundtrip reloaded = g.trimesh.load(g.trimesh.util.wrap_as_stream(export), file_type='glb') # make basic assertions g.scene_equal(scene, reloaded) # if we merge ugly it should now be watertight geom.merge_vertices(textured=False) assert geom.is_volume
def test_fill_holes(self): for mesh_name in ['unit_cube.STL', 'machinist.XAML', 'round.stl', 'quadknot.obj']: mesh = g.get_mesh(mesh_name) if not mesh.is_watertight: continue mesh.faces = mesh.faces[1:-1] assert not mesh.is_watertight assert not mesh.is_volume # color some faces g.trimesh.repair.broken_faces(mesh, color=[255, 0, 0, 255]) # run the fill holes operation mesh.fill_holes() # should be a superset of the last two assert mesh.is_volume assert mesh.is_watertight assert mesh.is_winding_consistent
def test_face_attributes(self): """ Test writing face attributes to a ply, by reading them back and asserting the written attributes array matches """ m = g.get_mesh('box.STL') test_1d_attribute = g.np.copy(m.face_angles[:, 0]) test_nd_attribute = g.np.copy(m.face_angles) m.face_attributes['test_1d_attribute'] = test_1d_attribute m.face_attributes['test_nd_attribute'] = test_nd_attribute export = m.export(file_type='ply') reconstructed = g.trimesh.load(g.trimesh.util.wrap_as_stream(export), file_type='ply') face_attributes = reconstructed.metadata['ply_raw']['face']['data'] result_1d = face_attributes['test_1d_attribute'] result_nd = face_attributes['test_nd_attribute']['f1'] g.np.testing.assert_almost_equal(result_1d, test_1d_attribute) g.np.testing.assert_almost_equal(result_nd, test_nd_attribute)
def test_hash(self): setup = 'import numpy, trimesh;' setup += 'd = numpy.random.random((10000,3));' setup += 't = trimesh.caching.tracked_array(d)' count = 10000 mt = g.timeit.timeit(setup=setup, stmt='t._modified_m=True;t.md5()', number=count) ct = g.timeit.timeit(setup=setup, stmt='t._modified_c=True;t.crc()', number=count) xt = g.timeit.timeit(setup=setup, stmt='t._modified_x=True;t.fast_hash()', number=count) m = g.get_mesh('featuretype.STL') # log result values g.log.info('\nResult\nMD5:\n{}\nCRC:\n{}\nXX:\n{}'.format( m.vertices.md5(), m.vertices.crc(), m.vertices.fast_hash())) # crc should always be faster than MD5 g.log.info('\nTime\nMD5:\n{}\nCRC:\n{}\nXX:\n{}'.format( mt, ct, xt)) # CRC should be faster than MD5 # this is NOT true if you blindly call adler32 # but our speed check on import should assure this assert ct < mt # xxhash should be faster than CRC and MD5 # it is sometimes slower on Windows/Appveyor TODO: figure out why if g.trimesh.caching.xxhash is not None and g.platform.system() == 'Linux': assert xt < mt assert xt < ct
def test_cap_coplanar(self): # check to see if we handle capping with # existing coplanar faces correctly try: from triangle import triangulate # NOQA except BaseException as E: if g.all_dep: raise E else: return s = g.get_mesh('cap.zip') mesh = next(iter(s.geometry.values())) plane_origin = [0, 0, 5000] plane_normal = [0, 0, -1] assert mesh.is_watertight newmesh = mesh.slice_plane(plane_origin=plane_origin, plane_normal=plane_normal, cap=True) assert newmesh.is_watertight