__author__ = "dcowden" """ Tests for CadQuery Selectors These tests do not construct any solids, they test only selectors that query an existing solid """ import math import unittest import sys import os.path # my modules from tests import BaseTest, makeUnitCube, makeUnitSquareWire from cadquery import * from cadquery import selectors class TestCQSelectors(BaseTest): def testWorkplaneCenter(self): "Test Moving workplane center" s = Workplane(Plane.XY()) # current point and world point should be equal self.assertTupleAlmostEquals((0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3) # move origin and confirm center moves s = s.center(-2.0, -2.0) # current point should be 0,0, but self.assertTupleAlmostEquals((-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3) def testVertices(self): t = makeUnitSquareWire() # square box c = CQ(t) self.assertEqual(4, c.vertices().size()) self.assertEqual(4, c.edges().size()) self.assertEqual(0, c.vertices().edges().size()) # no edges on any vertices # but selecting all edges still yields all vertices self.assertEqual(4, c.edges().vertices().size()) self.assertEqual(1, c.wires().size()) # just one wire self.assertEqual(0, c.faces().size()) # odd combinations all work but yield no results self.assertEqual(0, c.vertices().faces().size()) self.assertEqual(0, c.edges().faces().size()) self.assertEqual(0, c.edges().vertices().faces().size()) def testEnd(self): c = CQ(makeUnitSquareWire()) # 4 because there are 4 vertices self.assertEqual(4, c.vertices().size()) # 1 because we started with 1 wire self.assertEqual(1, c.vertices().end().size()) def testAll(self): "all returns a list of CQ objects, so that you can iterate over them individually" c = CQ(makeUnitCube()) self.assertEqual(6, c.faces().size()) self.assertEqual(6, len(c.faces().all())) self.assertEqual(4, c.faces().all()[0].vertices().size()) def testFirst(self): c = CQ(makeUnitCube()) self.assertEqual(type(c.vertices().first().val()), Vertex) self.assertEqual(type(c.vertices().first().first().first().val()), Vertex) def testCompounds(self): c = CQ(makeUnitSquareWire()) self.assertEqual(0, c.compounds().size()) self.assertEqual(0, c.shells().size()) self.assertEqual(0, c.solids().size()) def testSolid(self): c = CQ(makeUnitCube(False)) # make sure all the counts are right for a cube self.assertEqual(1, c.solids().size()) self.assertEqual(6, c.faces().size()) self.assertEqual(12, c.edges().size()) self.assertEqual(8, c.vertices().size()) self.assertEqual(0, c.compounds().size()) # now any particular face should result in 4 edges and four vertices self.assertEqual(4, c.faces().first().edges().size()) self.assertEqual(1, c.faces().first().size()) self.assertEqual(4, c.faces().first().vertices().size()) self.assertEqual(4, c.faces().last().edges().size()) def testFaceTypesFilter(self): "Filters by face type" c = CQ(makeUnitCube()) self.assertEqual(c.faces().size(), c.faces("%PLANE").size()) self.assertEqual(c.faces().size(), c.faces("%plane").size()) self.assertEqual(0, c.faces("%sphere").size()) self.assertEqual(0, c.faces("%cone").size()) self.assertEqual(0, c.faces("%SPHERE").size()) def testPerpendicularDirFilter(self): c = CQ(makeUnitCube()) perp_edges = c.edges("#Z") self.assertEqual(8, perp_edges.size()) # 8 edges are perp. to z # dot product of perpendicular vectors is zero for e in perp_edges.vals(): self.assertAlmostEqual(e.tangentAt(0).dot(Vector(0, 0, 1)), 0.0) perp_faces = c.faces("#Z") self.assertEqual(4, perp_faces.size()) # 4 faces are perp to z too! for f in perp_faces.vals(): self.assertAlmostEqual(f.normalAt(None).dot(Vector(0, 0, 1)), 0.0) def testFaceDirFilter(self): c = CQ(makeUnitCube()) # a cube has one face in each direction self.assertEqual(1, c.faces("+Z").size()) self.assertTupleAlmostEquals( (0, 0, 1), c.faces("+Z").val().Center().toTuple(), 3 ) self.assertEqual(1, c.faces("-Z").size()) self.assertTupleAlmostEquals( (0, 0, 0), c.faces("-Z").val().Center().toTuple(), 3 ) self.assertEqual(1, c.faces("+X").size()) self.assertTupleAlmostEquals( (0.5, 0, 0.5), c.faces("+X").val().Center().toTuple(), 3 ) self.assertEqual(1, c.faces("-X").size()) self.assertTupleAlmostEquals( (-0.5, 0, 0.5), c.faces("-X").val().Center().toTuple(), 3 ) self.assertEqual(1, c.faces("+Y").size()) self.assertTupleAlmostEquals( (0, 0.5, 0.5), c.faces("+Y").val().Center().toTuple(), 3 ) self.assertEqual(1, c.faces("-Y").size()) self.assertTupleAlmostEquals( (0, -0.5, 0.5), c.faces("-Y").val().Center().toTuple(), 3 ) self.assertEqual(0, c.faces("XY").size()) self.assertEqual(1, c.faces("X").size()) # should be same as +X self.assertEqual(c.faces("+X").val().Center(), c.faces("X").val().Center()) self.assertNotEqual(c.faces("+X").val().Center(), c.faces("-X").val().Center()) def testParallelPlaneFaceFilter(self): c = CQ(makeUnitCube(centered=False)) # faces parallel to Z axis # these two should produce the same behaviour: for s in ["|Z", selectors.ParallelDirSelector(Vector(0, 0, 1))]: parallel_faces = c.faces(s) self.assertEqual(2, parallel_faces.size()) for f in parallel_faces.vals(): self.assertAlmostEqual(abs(f.normalAt(None).dot(Vector(0, 0, 1))), 1) self.assertEqual( 2, c.faces(selectors.ParallelDirSelector(Vector((0, 0, -1)))).size() ) # same thing as above # just for fun, vertices on faces parallel to z self.assertEqual(8, c.faces("|Z").vertices().size()) # check that the X & Y center of these faces is the same as the box (ie. we haven't selected the wrong face) faces = c.faces(selectors.ParallelDirSelector(Vector((0, 0, 1)))).vals() for f in faces: c = f.Center() self.assertAlmostEqual(c.x, 0.5) self.assertAlmostEqual(c.y, 0.5) def testParallelEdgeFilter(self): c = CQ(makeUnitCube()) for sel, vec in zip( ["|X", "|Y", "|Z"], [Vector(1, 0, 0), Vector(0, 1, 0), Vector(0, 0, 1)] ): edges = c.edges(sel) # each direction should have 4 edges self.assertEqual(4, edges.size()) # each edge should be parallel with vec and have a cross product with a length of 0 for e in edges.vals(): self.assertAlmostEqual(e.tangentAt(0).cross(vec).Length, 0.0) def testCenterNthSelector(self): sel = selectors.CenterNthSelector c = CQ(makeUnitCube(centered=True)) bottom_face = c.faces(sel(0, Vector(0, 0, 1))) self.assertEqual(bottom_face.size(), 1) self.assertTupleAlmostEquals((0, 0, 0), bottom_face.val().Center().toTuple(), 3) side_faces = c.faces(sel(1, Vector(0, 0, 1))) self.assertEqual(side_faces.size(), 4) for f in side_faces.vals(): self.assertAlmostEqual(0.5, f.Center().z) top_face = c.faces(sel(2, Vector(0, 0, 1))) self.assertEqual(top_face.size(), 1) self.assertTupleAlmostEquals((0, 0, 1), top_face.val().Center().toTuple(), 3) with self.assertRaises(IndexError): c.faces(sel(3, Vector(0, 0, 1))) left_face = c.faces(sel(0, Vector(1, 0, 0))) self.assertEqual(left_face.size(), 1) self.assertTupleAlmostEquals( (-0.5, 0, 0.5), left_face.val().Center().toTuple(), 3 ) middle_faces = c.faces(sel(1, Vector(1, 0, 0))) self.assertEqual(middle_faces.size(), 4) for f in middle_faces.vals(): self.assertAlmostEqual(0, f.Center().x) right_face = c.faces(sel(2, Vector(1, 0, 0))) self.assertEqual(right_face.size(), 1) self.assertTupleAlmostEquals( (0.5, 0, 0.5), right_face.val().Center().toTuple(), 3 ) with self.assertRaises(IndexError): c.faces(sel(3, Vector(1, 0, 0))) # lower corner faces self.assertEqual(c.faces(sel(0, Vector(1, 1, 1))).size(), 3) # upper corner faces self.assertEqual(c.faces(sel(1, Vector(1, 1, 1))).size(), 3) with self.assertRaises(IndexError): c.faces(sel(2, Vector(1, 1, 1))) for idx, z_val in zip([0, 1, 2], [0, 0.5, 1]): edges = c.edges(sel(idx, Vector(0, 0, 1))) self.assertEqual(edges.size(), 4) for e in edges.vals(): self.assertAlmostEqual(z_val, e.Center().z) with self.assertRaises(IndexError): c.edges(sel(3, Vector(0, 0, 1))) for idx, z_val in zip([0, 1], [0, 1]): vertices = c.vertices(sel(idx, Vector(0, 0, 1))) self.assertEqual(vertices.size(), 4) for e in vertices.vals(): self.assertAlmostEqual(z_val, e.Z) with self.assertRaises(IndexError): c.vertices(sel(3, Vector(0, 0, 1))) # select a non-linear edge part = ( Workplane() .rect(10, 10, centered=False) .extrude(1) .faces(">Z") .workplane(centerOption="CenterOfMass") .move(-3, 0) .hole(2) ) hole = part.faces(">Z").edges(sel(1, Vector(1, 0, 0))) # have we selected a single hole? self.assertEqual(1, hole.size()) self.assertAlmostEqual(1, hole.val().radius()) # select solids box0 = Workplane().box(1, 1, 1, centered=(True, True, True)) box1 = Workplane("XY", origin=(10, 10, 10)).box( 1, 1, 1, centered=(True, True, True) ) part = box0.add(box1) self.assertEqual(part.solids().size(), 2) for direction in [(0, 0, 1), (0, 1, 0), (1, 0, 0)]: box0_selected = part.solids(sel(0, Vector(direction))) self.assertEqual(1, box0_selected.size()) self.assertTupleAlmostEquals( (0, 0, 0), box0_selected.val().Center().toTuple(), 3 ) box1_selected = part.solids(sel(1, Vector(direction))) self.assertEqual(1, box0_selected.size()) self.assertTupleAlmostEquals( (10, 10, 10), box1_selected.val().Center().toTuple(), 3 ) def testMaxDistance(self): c = CQ(makeUnitCube()) # should select the topmost face self.assertEqual(1, c.faces(">Z").size()) self.assertEqual(4, c.faces(">Z").vertices().size()) # vertices should all be at z=1, if this is the top face self.assertEqual(4, len(c.faces(">Z").vertices().vals())) for v in c.faces(">Z").vertices().vals(): self.assertAlmostEqual(1.0, v.Z, 3) # test the case of multiple objects at the same distance el = c.edges(">Z").vals() self.assertEqual(4, len(el)) for e in el: self.assertAlmostEqual(e.Center().z, 1) def testMinDistance(self): c = CQ(makeUnitCube()) # should select the bottom face self.assertEqual(1, c.faces("(1,0,0)[1]").val() self.assertAlmostEqual(val.Center().x, -1.5) val = c.faces(">X[1]").val() self.assertAlmostEqual(val.Center().x, -1.5) # 2nd face with inversed selection vector val = c.faces(">(-1,0,0)[1]").val() self.assertAlmostEqual(val.Center().x, 1.5) val = c.faces("X[-2]").val() self.assertAlmostEqual(val.Center().x, 1.5) # Last face val = c.faces(">X[-1]").val() self.assertAlmostEqual(val.Center().x, 2.5) # check if the selected face if normal to the specified Vector self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0) # test selection of multiple faces with the same distance c = ( Workplane("XY") .box(1, 4, 1, centered=(False, True, False)) .faces("Z") .box(1, 1, 1, centered=(True, True, False)) ) # select 2nd from the bottom (NB python indexing is 0-based) vals = c.faces(">Z[1]").vals() self.assertEqual(len(vals), 2) val = c.faces(">Z[1]").val() self.assertAlmostEqual(val.Center().z, 1) # do the same but by selecting 3rd from the top vals = c.faces("Z[-1] is equivalent to >Z val1 = c.faces(">Z[-1]").vals()[0] val2 = c.faces(">Z").vals()[0] self.assertTupleAlmostEquals( val1.Center().toTuple(), val2.Center().toTuple(), 3 ) # DirectionNthSelector should not select faces that are not perpendicular twisted_boxes = ( Workplane() .box(1, 1, 1, centered=(True, True, False)) .transformed(rotate=(45, 0, 0), offset=(0, 0, 3)) .box(1, 1, 1) ) self.assertTupleAlmostEquals( twisted_boxes.faces(">Z[-1]").val().Center().toTuple(), (0, 0, 1), 3 ) # this should select a face on the upper/rotated cube, not the lower/unrotated cube self.assertGreater(twisted_boxes.faces("<(0, 1, 1)[-1]").val().Center().z, 1) # verify that >Z[-1] is equivalent to >Z self.assertTupleAlmostEquals( twisted_boxes.faces(">(0, 1, 1)[0]").vals()[0].Center().toTuple(), twisted_boxes.faces("<(0, 1, 1)[-1]").vals()[0].Center().toTuple(), 3, ) def testNearestTo(self): c = CQ(makeUnitCube(centered=False)) # nearest vertex to origin is (0,0,0) t = (0.1, 0.1, 0.1) v = c.vertices(selectors.NearestToPointSelector(t)).vals()[0] self.assertTupleAlmostEquals((0.0, 0.0, 0.0), (v.X, v.Y, v.Z), 3) t = (0.1, 0.1, 0.2) # nearest edge is the vertical side edge, 0,0,0 -> 0,0,1 e = c.edges(selectors.NearestToPointSelector(t)).vals()[0] v = c.edges(selectors.NearestToPointSelector(t)).vertices().vals() self.assertEqual(2, len(v)) # nearest solid is myself s = c.solids(selectors.NearestToPointSelector(t)).vals() self.assertEqual(1, len(s)) def testBox(self): c = CQ(makeUnitCube(centered=False)) # test vertice selection test_data_vertices = [ # box point0, box point1, selected vertice ((0.9, 0.9, 0.9), (1.1, 1.1, 1.1), (1.0, 1.0, 1.0)), ((-0.1, 0.9, 0.9), (0.9, 1.1, 1.1), (0.0, 1.0, 1.0)), ((-0.1, -0.1, 0.9), (0.1, 0.1, 1.1), (0.0, 0.0, 1.0)), ((-0.1, -0.1, -0.1), (0.1, 0.1, 0.1), (0.0, 0.0, 0.0)), ((0.9, -0.1, -0.1), (1.1, 0.1, 0.1), (1.0, 0.0, 0.0)), ((0.9, 0.9, -0.1), (1.1, 1.1, 0.1), (1.0, 1.0, 0.0)), ((-0.1, 0.9, -0.1), (0.1, 1.1, 0.1), (0.0, 1.0, 0.0)), ((0.9, -0.1, 0.9), (1.1, 0.1, 1.1), (1.0, 0.0, 1.0)), ] for d in test_data_vertices: vl = c.vertices(selectors.BoxSelector(d[0], d[1])).vals() self.assertEqual(1, len(vl)) v = vl[0] self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3) # this time box points are swapped vl = c.vertices(selectors.BoxSelector(d[1], d[0])).vals() self.assertEqual(1, len(vl)) v = vl[0] self.assertTupleAlmostEquals(d[2], (v.X, v.Y, v.Z), 3) # test multiple vertices selection vl = c.vertices( selectors.BoxSelector((-0.1, -0.1, 0.9), (0.1, 1.1, 1.1)) ).vals() self.assertEqual(2, len(vl)) vl = c.vertices( selectors.BoxSelector((-0.1, -0.1, -0.1), (0.1, 1.1, 1.1)) ).vals() self.assertEqual(4, len(vl)) # test edge selection test_data_edges = [ # box point0, box point1, edge center ((0.4, -0.1, -0.1), (0.6, 0.1, 0.1), (0.5, 0.0, 0.0)), ((-0.1, -0.1, 0.4), (0.1, 0.1, 0.6), (0.0, 0.0, 0.5)), ((0.9, 0.9, 0.4), (1.1, 1.1, 0.6), (1.0, 1.0, 0.5)), ((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0)), ] for d in test_data_edges: el = c.edges(selectors.BoxSelector(d[0], d[1])).vals() self.assertEqual(1, len(el)) ec = el[0].Center() self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3) # test again by swapping box points el = c.edges(selectors.BoxSelector(d[1], d[0])).vals() self.assertEqual(1, len(el)) ec = el[0].Center() self.assertTupleAlmostEquals(d[2], (ec.x, ec.y, ec.z), 3) # test multiple edge selection el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (0.6, 0.1, 0.6))).vals() self.assertEqual(2, len(el)) el = c.edges(selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6))).vals() self.assertEqual(3, len(el)) # test face selection test_data_faces = [ # box point0, box point1, face center ((0.4, -0.1, 0.4), (0.6, 0.1, 0.6), (0.5, 0.0, 0.5)), ((0.9, 0.4, 0.4), (1.1, 0.6, 0.6), (1.0, 0.5, 0.5)), ((0.4, 0.4, 0.9), (0.6, 0.6, 1.1), (0.5, 0.5, 1.0)), ((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0)), ] for d in test_data_faces: fl = c.faces(selectors.BoxSelector(d[0], d[1])).vals() self.assertEqual(1, len(fl)) fc = fl[0].Center() self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3) # test again by swapping box points fl = c.faces(selectors.BoxSelector(d[1], d[0])).vals() self.assertEqual(1, len(fl)) fc = fl[0].Center() self.assertTupleAlmostEquals(d[2], (fc.x, fc.y, fc.z), 3) # test multiple face selection fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (0.6, 1.1, 1.1))).vals() self.assertEqual(2, len(fl)) fl = c.faces(selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1))).vals() self.assertEqual(3, len(fl)) # test boundingbox option el = c.edges( selectors.BoxSelector((-0.1, -0.1, -0.1), (1.1, 0.1, 0.6), True) ).vals() self.assertEqual(1, len(el)) fl = c.faces( selectors.BoxSelector((0.4, 0.4, 0.4), (1.1, 1.1, 1.1), True) ).vals() self.assertEqual(0, len(fl)) fl = c.faces( selectors.BoxSelector((-0.1, 0.4, -0.1), (1.1, 1.1, 1.1), True) ).vals() self.assertEqual(1, len(fl)) def testRadiusNthSelector(self): part = ( Workplane() .box(10, 10, 1) .edges(">(1, 1, 0) and |Z") .fillet(1) .edges(">(-1, 1, 0) and |Z") .fillet(1) .edges(">(-1, -1, 0) and |Z") .fillet(2) .edges(">(1, -1, 0) and |Z") .fillet(3) .faces(">Z") ) # smallest radius is 1.0 self.assertAlmostEqual( part.edges(selectors.RadiusNthSelector(0)).val().radius(), 1.0 ) # there are two edges with the smallest radius self.assertEqual(len(part.edges(selectors.RadiusNthSelector(0)).vals()), 2) # next radius is 2.0 self.assertAlmostEqual( part.edges(selectors.RadiusNthSelector(1)).val().radius(), 2.0 ) # largest radius is 3.0 self.assertAlmostEqual( part.edges(selectors.RadiusNthSelector(-1)).val().radius(), 3.0 ) # accessing index 3 should be an IndexError with self.assertRaises(IndexError): part.edges(selectors.RadiusNthSelector(3)) # reversed self.assertAlmostEqual( part.edges(selectors.RadiusNthSelector(0, directionMax=False)) .val() .radius(), 3.0, ) # test the selector on wires wire_circles = ( Workplane() .circle(2) .moveTo(10, 0) .circle(2) .moveTo(20, 0) .circle(4) .consolidateWires() ) self.assertEqual( len(wire_circles.wires(selectors.RadiusNthSelector(0)).vals()), 2 ) self.assertEqual( len(wire_circles.wires(selectors.RadiusNthSelector(1)).vals()), 1 ) self.assertAlmostEqual( wire_circles.wires(selectors.RadiusNthSelector(0)).val().radius(), 2 ) self.assertAlmostEqual( wire_circles.wires(selectors.RadiusNthSelector(1)).val().radius(), 4 ) def testAndSelector(self): c = CQ(makeUnitCube()) S = selectors.StringSyntaxSelector BS = selectors.BoxSelector el = c.edges( selectors.AndSelector(S("|X"), BS((-2, -2, 0.1), (2, 2, 2))) ).vals() self.assertEqual(2, len(el)) # test 'and' (intersection) operator el = c.edges(S("|X") & BS((-2, -2, 0.1), (2, 2, 2))).vals() self.assertEqual(2, len(el)) # test using extended string syntax v = c.vertices(">X and >Y").vals() self.assertEqual(2, len(v)) def testSumSelector(self): c = CQ(makeUnitCube()) S = selectors.StringSyntaxSelector fl = c.faces(selectors.SumSelector(S(">Z"), S("Z") + S("Z or X"))).vals() self.assertEqual(3, len(fl)) # test the subtract operator fl = c.faces(S("#Z") - S(">X")).vals() self.assertEqual(3, len(fl)) # test using extended string syntax fl = c.faces("#Z exc >X").vals() self.assertEqual(3, len(fl)) def testInverseSelector(self): c = CQ(makeUnitCube()) S = selectors.StringSyntaxSelector fl = c.faces(selectors.InverseSelector(S(">Z"))).vals() self.assertEqual(5, len(fl)) el = c.faces(">Z").edges(selectors.InverseSelector(S(">X"))).vals() self.assertEqual(3, len(el)) # test invert operator fl = c.faces(-S(">Z")).vals() self.assertEqual(5, len(fl)) el = c.faces(">Z").edges(-S(">X")).vals() self.assertEqual(3, len(el)) # test using extended string syntax fl = c.faces("not >Z").vals() self.assertEqual(5, len(fl)) el = c.faces(">Z").edges("not >X").vals() self.assertEqual(3, len(el)) def testComplexStringSelector(self): c = CQ(makeUnitCube()) v = c.vertices("(>X and >Y) or (XZ", "(1,4,55.)[20]", "|XY", "(0,0,1) or XY except >(1,1,1)[-1]", "(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)", "not ( X or Y )", ] for e in expressions: gram.parseString(e, parseAll=True)