import pytest import os from itertools import product import nlopt import cadquery as cq from cadquery.occ_impl.exporters.assembly import ( exportAssembly, exportCAF, exportVTKJS, exportVRML, ) from cadquery.occ_impl.assembly import toJSON from OCP.gp import gp_XYZ @pytest.fixture def simple_assy(): b1 = cq.Solid.makeBox(1, 1, 1) b2 = cq.Workplane().box(1, 1, 2) b3 = cq.Workplane().pushPoints([(0, 0), (-2, -5)]).box(1, 1, 3) assy = cq.Assembly(b1, loc=cq.Location(cq.Vector(2, -5, 0))) assy.add(b2, loc=cq.Location(cq.Vector(1, 1, 0))) assy.add(b3, loc=cq.Location(cq.Vector(2, 3, 0))) return assy @pytest.fixture def nested_assy(): b1 = cq.Workplane().box(1, 1, 1).faces(" bool: checks = [ solve_result["status"] == nlopt.XTOL_REACHED, solve_result["cost"] < 1e-9, solve_result["iters"] > 0, ] return all(checks) def test_color(): c1 = cq.Color("red") assert c1.wrapped.GetRGB().Red() == 1 assert c1.wrapped.Alpha() == 1 c2 = cq.Color(1, 0, 0) assert c2.wrapped.GetRGB().Red() == 1 assert c2.wrapped.Alpha() == 1 c3 = cq.Color(1, 0, 0, 0.5) assert c3.wrapped.GetRGB().Red() == 1 assert c3.wrapped.Alpha() == 0.5 with pytest.raises(ValueError): cq.Color("?????") with pytest.raises(ValueError): cq.Color(1, 2, 3, 4, 5) def test_assembly(simple_assy, nested_assy): # basic checks assert len(simple_assy.objects) == 3 assert len(simple_assy.children) == 2 assert len(simple_assy.shapes) == 1 assert len(nested_assy.objects) == 3 assert len(nested_assy.children) == 1 assert nested_assy.objects["SECOND"].parent is nested_assy # bottom-up traversal kvs = list(nested_assy.traverse()) assert kvs[0][0] == "BOTTOM" assert len(kvs[0][1].shapes[0].Solids()) == 2 assert kvs[-1][0] == "TOP" def test_step_export(nested_assy): exportAssembly(nested_assy, "nested.step") w = cq.importers.importStep("nested.step") assert w.solids().size() == 4 # check that locations were applied correctly c = cq.Compound.makeCompound(w.solids().vals()).Center() c.toTuple() assert pytest.approx(c.toTuple()) == (0, 4, 0) def test_native_export(simple_assy): exportCAF(simple_assy, "assy.xml") # only sanity check for now assert os.path.exists("assy.xml") def test_vtkjs_export(nested_assy): exportVTKJS(nested_assy, "assy") # only sanity check for now assert os.path.exists("assy.zip") def test_vrml_export(simple_assy): exportVRML(simple_assy, "assy.wrl") # only sanity check for now assert os.path.exists("assy.wrl") def test_toJSON(simple_assy, nested_assy, empty_top_assy): r1 = toJSON(simple_assy) r2 = toJSON(simple_assy) r3 = toJSON(empty_top_assy) assert len(r1) == 3 assert len(r2) == 3 assert len(r3) == 1 def test_save(simple_assy, nested_assy): simple_assy.save("simple.step") assert os.path.exists("simple.step") simple_assy.save("simple.xml") assert os.path.exists("simple.xml") simple_assy.save("simple.step") assert os.path.exists("simple.step") simple_assy.save("simple.stp", "STEP") assert os.path.exists("simple.stp") simple_assy.save("simple.caf", "XML") assert os.path.exists("simple.caf") with pytest.raises(ValueError): simple_assy.save("simple.dxf") with pytest.raises(ValueError): simple_assy.save("simple.step", "DXF") def test_constrain(simple_assy, nested_assy): subassy1 = simple_assy.children[0] subassy2 = simple_assy.children[1] b1 = simple_assy.obj b2 = subassy1.obj b3 = subassy2.obj simple_assy.constrain( simple_assy.name, b1.Faces()[0], subassy1.name, b2.faces("Z").val(), subassy2.name, b3.faces("Z", "SECOND/BOTTOM@faces@X", "SECOND/BOTTOM@faces@Z", "SECOND/BOTTOM@vertices@>X and >Y and >Z", "Point" ) def test_PointInPlane_constraint(box_and_vertex): # add first constraint box_and_vertex.constrain( "vertex", box_and_vertex.children[0].obj.val(), "box", box_and_vertex.obj.faces(">X").val(), "PointInPlane", param=0, ) box_and_vertex.solve() solve_result_check(box_and_vertex._solve_result) x_pos = ( box_and_vertex.children[0].loc.wrapped.Transformation().TranslationPart().X() ) assert x_pos == pytest.approx(0.5) # add a second PointInPlane constraint box_and_vertex.constrain("vertex", "box@faces@>Y", "PointInPlane", param=0) box_and_vertex.solve() solve_result_check(box_and_vertex._solve_result) vertex_translation_part = ( box_and_vertex.children[0].loc.wrapped.Transformation().TranslationPart() ) # should still be on the >X face from the first constraint assert vertex_translation_part.X() == pytest.approx(0.5) # now should additionally be on the >Y face assert vertex_translation_part.Y() == pytest.approx(1) # add a third PointInPlane constraint box_and_vertex.constrain("vertex", "box@faces@>Z", "PointInPlane", param=0) box_and_vertex.solve() solve_result_check(box_and_vertex._solve_result) # should now be on the >X and >Y and >Z corner assert ( box_and_vertex.children[0] .loc.wrapped.Transformation() .TranslationPart() .IsEqual(gp_XYZ(0.5, 1, 1.5), 1e-6) ) def test_PointInPlane_3_parts(box_and_vertex): cylinder_height = 2 cylinder = cq.Workplane().circle(0.1).extrude(cylinder_height) box_and_vertex.add(cylinder, name="cylinder") box_and_vertex.constrain("box@faces@>Z", "cylinder@faces@Z", "PointInPlane") box_and_vertex.constrain("vertex", "box@faces@>X", "PointInPlane") box_and_vertex.solve() solve_result_check(box_and_vertex._solve_result) vertex_translation_part = ( box_and_vertex.children[0].loc.wrapped.Transformation().TranslationPart() ) assert vertex_translation_part.Z() == pytest.approx(1.5 + cylinder_height) assert vertex_translation_part.X() == pytest.approx(0.5) @pytest.mark.parametrize("param1", [-1, 0, 2]) @pytest.mark.parametrize("param0", [-2, 0, 0.01]) def test_PointInPlane_param(box_and_vertex, param0, param1): box_and_vertex.constrain("vertex", "box@faces@>Z", "PointInPlane", param=param0) box_and_vertex.constrain("vertex", "box@faces@>X", "PointInPlane", param=param1) box_and_vertex.solve() solve_result_check(box_and_vertex._solve_result) vertex_translation_part = ( box_and_vertex.children[0].loc.wrapped.Transformation().TranslationPart() ) assert vertex_translation_part.Z() - 1.5 == pytest.approx(param0, abs=1e-6) assert vertex_translation_part.X() - 0.5 == pytest.approx(param1, abs=1e-6) def test_constraint_getPln(): """ Test that _getPln does the right thing with different arguments """ ids = (0, 1) sublocs = (cq.Location(), cq.Location()) def make_constraint(shape0): return cq.Constraint(ids, (shape0, shape0), sublocs, "PointInPlane", 0) def fail_this(shape0): c0 = make_constraint(shape0) with pytest.raises(ValueError): c0._getPln(c0.args[0]) def resulting_pln(shape0): c0 = make_constraint(shape0) return c0._getPln(c0.args[0]) def resulting_plane(shape0): p0 = resulting_pln(shape0) return cq.Plane( cq.Vector(p0.Location()), cq.Vector(p0.XAxis().Direction()), cq.Vector(p0.Axis().Direction()), ) # point should fail fail_this(cq.Vertex.makeVertex(0, 0, 0)) # line should fail fail_this(cq.Edge.makeLine(cq.Vector(1, 0, 0), cq.Vector(0, 0, 0))) # planar edge (circle) should succeed origin = cq.Vector(1, 2, 3) direction = cq.Vector(4, 5, 6).normalized() p1 = resulting_plane(cq.Edge.makeCircle(1, pnt=origin, dir=direction)) assert p1.zDir == direction assert p1.origin == origin # planar edge (spline) should succeed # it's a touch risky calling a spline a planar edge, but lets see if it's within tolerance points0 = [cq.Vector(x) for x in [(-1, 0, 1), (0, 1, 1), (1, 0, 1), (0, -1, 1)]] planar_spline = cq.Edge.makeSpline(points0, periodic=True) p2 = resulting_plane(planar_spline) assert p2.origin == planar_spline.Center() assert p2.zDir == cq.Vector(0, 0, 1) # non-planar edge should fail points1 = [cq.Vector(x) for x in [(-1, 0, -1), (0, 1, 1), (1, 0, -1), (0, -1, 1)]] nonplanar_spline = cq.Edge.makeSpline(points1, periodic=True) fail_this(nonplanar_spline) # planar wire should succeed # make a triangle in the XZ plane points2 = [cq.Vector(x) for x in [(-1, 0, -1), (0, 0, 1), (1, 0, -1)]] points2.append(points2[0]) triangle = cq.Wire.makePolygon(points2) p3 = resulting_plane(triangle) assert p3.origin == triangle.Center() assert p3.zDir == cq.Vector(0, 1, 0) # non-planar wire should fail points3 = [cq.Vector(x) for x in [(-1, 0, -1), (0, 1, 1), (1, 0, 0), (0, -1, 1)]] wonky_shape = cq.Wire.makePolygon(points3) fail_this(wonky_shape) # all makePlane faces should succeed for length, width in product([None, 10], [None, 11]): f0 = cq.Face.makePlane( length=length, width=width, basePnt=(1, 2, 3), dir=(1, 0, 0) ) p4 = resulting_plane(f0) if length and width: assert p4.origin == cq.Vector(1, 2, 3) assert p4.zDir == cq.Vector(1, 0, 0) f1 = cq.Face.makeFromWires(triangle, []) p5 = resulting_plane(f1) # not sure why, but the origins only roughly line up assert (p5.origin - triangle.Center()).Length < 0.1 assert p5.zDir == cq.Vector(0, 1, 0) # shell... not sure? # solid should fail fail_this(cq.Solid.makeBox(1, 1, 1)) def test_toCompound(simple_assy, nested_assy): c0 = simple_assy.toCompound() assert isinstance(c0, cq.Compound) assert len(c0.Solids()) == 4 c1 = nested_assy.toCompound() assert isinstance(c1, cq.Compound) assert len(c1.Solids()) == 4 # check nested assy location appears in compound # create four boxes, stack them on top of each other, check highest face is in final compound box0 = cq.Workplane().box(1, 1, 3, centered=(True, True, False)) box1 = cq.Workplane().box(1, 1, 4) box2 = cq.Workplane().box(1, 1, 5) box3 = cq.Workplane().box(1, 1, 6) # top level assy assy0 = cq.Assembly(box0, name="box0") assy0.add(box1, name="box1") assy0.constrain("box0@faces@>Z", "box1@faces@Z", "box3@faces@Z", "assy1/box2@faces@