From 653549dd9041ae7a2649916a001fd1dae4bf19c5 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sat, 30 Nov 2019 14:57:29 -0500 Subject: [PATCH 01/70] Fixed line continuations in examples per #221 --- doc/examples.rst | 94 +++++++++---------- .../Ex002_Block_With_Bored_Center_Hole.py | 4 +- ...03_Pillow_Block_With_Counterbored_Holes.py | 12 +-- examples/Ex004_Extruded_Cylindrical_Plate.py | 6 +- examples/Ex005_Extruded_Lines_and_Arcs.py | 12 +-- examples/Ex008_Polygon_Creation.py | 8 +- examples/Ex009_Polylines.py | 8 +- examples/Ex015_Rotated_Workplanes.py | 8 +- examples/Ex016_Using_Construction_Geometry.py | 8 +- examples/Ex018_Making_Lofts.py | 8 +- examples/Ex019_Counter_Sunk_Holes.py | 6 +- examples/Ex021_Splitting_an_Object.py | 4 +- .../Ex024_Sweep_With_Multiple_Sections.py | 36 +++---- examples/Ex100_Lego_Brick.py | 18 ++-- 14 files changed, 116 insertions(+), 116 deletions(-) diff --git a/doc/examples.rst b/doc/examples.rst index 391af5a0..0f3662e0 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -71,8 +71,8 @@ of a working plane is at the center of the face. The default hole depth is thro center_hole_dia = 22.0 # Create a box based on the dimensions above and add a 22mm center hole - result = cq.Workplane("XY").box(length, height, thickness) \ - .faces(">Z").workplane().hole(center_hole_dia) + result = (cq.Workplane("XY").box(length, height, thickness) + .faces(">Z").workplane().hole(center_hole_dia)) show_object(result) @@ -121,8 +121,8 @@ closed curve. .. cq_plot:: - result = cq.Workplane("front").lineTo(2.0, 0).lineTo(2.0, 1.0).threePointArc((1.0, 1.5),(0.0, 1.0))\ - .close().extrude(0.25) + result = (cq.Workplane("front").lineTo(2.0, 0).lineTo(2.0, 1.0).threePointArc((1.0, 1.5),(0.0, 1.0)) + .close().extrude(0.25)) show_object(result) @@ -152,7 +152,7 @@ A new work plane center can be established at any point. result = result.center(1.5, 0.0).rect(0.5, 0.5) # new work center is (1.5, 0.0) result = result.center(-1.5, 1.5).circle(0.25) # new work center is ( 0.0, 1.5). - #the new center is specified relative to the previous center, not global coordinates! + # The new center is specified relative to the previous center, not global coordinates! result = result.extrude(0.25) show_object(result) @@ -204,8 +204,8 @@ correct for small hole sizes. .. cq_plot:: - result = cq.Workplane("front").box(3.0, 4.0, 0.25).pushPoints ( [ ( 0,0.75 ),(0, -0.75) ]) \ - .polygon(6, 1.0).cutThruAll() + result = (cq.Workplane("front").box(3.0, 4.0, 0.25).pushPoints ( [ ( 0,0.75 ),(0, -0.75) ]) + .polygon(6, 1.0).cutThruAll()) show_object(result) .. topic:: Api References @@ -467,9 +467,9 @@ You can create a rotated work plane by specifying angles of rotation relative to .. cq_plot:: - result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").workplane() \ - .transformed(offset=cq.Vector(0, -1.5, 1.0),rotate=cq.Vector(60, 0, 0)) \ - .rect(1.5,1.5,forConstruction=True).vertices().hole(0.25) + result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").workplane() + .transformed(offset=cq.Vector(0, -1.5, 1.0),rotate=cq.Vector(60, 0, 0)) + .rect(1.5,1.5,forConstruction=True).vertices().hole(0.25)) show_object(result) .. topic:: Api References @@ -492,8 +492,8 @@ In the example below, a rectangle is drawn, and its vertices are used to locate .. cq_plot:: - result = cq.Workplane("front").box(2, 2, 0.5).faces(">Z").workplane() \ - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.125 ) + result = (cq.Workplane("front").box(2, 2, 0.5).faces(">Z").workplane() + .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.125 )) show_object(result) .. topic:: Api References @@ -538,8 +538,8 @@ and a circular section. .. cq_plot:: - result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").circle(1.5) \ - .workplane(offset=3.0).rect(0.75, 0.5).loft(combine=True) + result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").circle(1.5) + .workplane(offset=3.0).rect(0.75, 0.5).loft(combine=True)) show_object(result) @@ -563,8 +563,8 @@ Similar to :py:meth:`Workplane.hole` , these functions operate on a list of poin .. cq_plot:: - result = cq.Workplane(cq.Plane.XY()).box(4,2, 0.5).faces(">Z").workplane().rect(3.5, 1.5, forConstruction=True)\ - .vertices().cboreHole(0.125, 0.25, 0.125, depth=None) + result = (cq.Workplane(cq.Plane.XY()).box(4,2, 0.5).faces(">Z").workplane().rect(3.5, 1.5, forConstruction=True) + .vertices().cboreHole(0.125, 0.25, 0.125, depth=None)) show_object(result) @@ -614,10 +614,10 @@ with just a few lines of code. (length,height,bearing_diam, thickness,padding) = ( 30.0, 40.0, 22.0, 10.0, 8.0) - result = cq.Workplane("XY").box(length,height,thickness).faces(">Z").workplane().hole(bearing_diam) \ - .faces(">Z").workplane() \ - .rect(length-padding,height-padding,forConstruction=True) \ - .vertices().cboreHole(2.4, 4.4, 2.1) + result = (cq.Workplane("XY").box(length,height,thickness).faces(">Z").workplane().hole(bearing_diam) + .faces(">Z").workplane() + .rect(length-padding,height-padding,forConstruction=True) + .vertices().cboreHole(2.4, 4.4, 2.1)) show_object(result) @@ -665,10 +665,10 @@ ones at 13 lines, but that's very short compared to the pythonOCC version, which (L,w,t) = (20.0, 6.0, 3.0) s = cq.Workplane("XY") - #draw half the profile of the bottle and extrude it - p = s.center(-L/2.0, 0).vLine(w/2.0) \ - .threePointArc((L/2.0, w/2.0 + t),(L, w/2.0)).vLine(-w/2.0) \ - .mirrorX().extrude(30.0,True) + # Draw half the profile of the bottle and extrude it + p = (s.center(-L/2.0, 0).vLine(w/2.0) + .threePointArc((L/2.0, w/2.0 + t),(L, w/2.0)).vLine(-w/2.0) + .mirrorX().extrude(30.0,True)) #make the neck p = p.faces(">Z").workplane().circle(3.0).extrude(2.0,True) @@ -729,9 +729,9 @@ A Parametric Enclosure oshell = oshell.edges("|Z").fillet(p_sideRadius) #inner shell - ishell = oshell.faces("Z").workplane(-p_thickness)\ - .rect(POSTWIDTH,POSTLENGTH,forConstruction=True)\ - .vertices().circle(p_screwpostOD/2.0).circle(p_screwpostID/2.0)\ - .extrude((-1.0)*(p_outerHeight + p_lipHeight -p_thickness ),True) + box = (box.faces(">Z").workplane(-p_thickness) + .rect(POSTWIDTH,POSTLENGTH,forConstruction=True) + .vertices().circle(p_screwpostOD/2.0).circle(p_screwpostID/2.0) + .extrude((-1.0)*(p_outerHeight + p_lipHeight -p_thickness ),True)) #split lid into top and bottom parts (lid,bottom) = box.faces(">Z").workplane(-p_thickness -p_lipHeight ).split(keepTop=True,keepBottom=True).all() #splits into two solids @@ -835,23 +835,23 @@ regarding the underside of the brick. s = s.faces("Z").workplane(). \ - rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) \ - .extrude(bumpHeight) + s = (s.faces(">Z").workplane(). + rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) + .extrude(bumpHeight)) # add posts on the bottom. posts are different diameter depending on geometry # solid studs for 1 bump, tubes for multiple, none for 1x1 tmp = s.faces(" 1 and wbumps > 1: - tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). \ - circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). + circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t)) elif lbumps > 1: - tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). \ - circle(t).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). + circle(t).extrude(height - t)) elif wbumps > 1: - tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). \ - circle(t).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). + circle(t).extrude(height - t)) else: tmp = s @@ -1018,12 +1018,12 @@ Braille Example line_start_pos += Point(0, -cell_geometry.interline) r = get_cylinder_radius(cell_geometry) - base = base.faces('>Z').vertices('Z').vertices('Z').edges() \ - .fillet(r - 0.001) + base = (base.faces('>Z').edges() + .fillet(r - 0.001)) hidding_box = cq.Workplane('XY').box( base_width, base_height, base_thickness, centered=(False, False, False)) result = hidding_box.union(base) @@ -1119,7 +1119,7 @@ This specific examples generates a helical cycloidal gear. return hypocycloid(t,r1,r2) # create the gear profile and extrude it - result = cq.Workplane('XY').parametricCurve(lambda t: gear(t*2*pi,6,1))\ - .twistExtrude(15,90).faces('>Z').workplane().circle(2).cutThruAll() + result = (cq.Workplane('XY').parametricCurve(lambda t: gear(t*2*pi,6,1)) + .twistExtrude(15,90).faces('>Z').workplane().circle(2).cutThruAll()) show_object(result) diff --git a/examples/Ex002_Block_With_Bored_Center_Hole.py b/examples/Ex002_Block_With_Bored_Center_Hole.py index a825f98e..6b00ffcd 100644 --- a/examples/Ex002_Block_With_Bored_Center_Hole.py +++ b/examples/Ex002_Block_With_Bored_Center_Hole.py @@ -13,8 +13,8 @@ center_hole_dia = 22.0 # Diameter of center hole in block # 2. The highest (max) Z face is selected and a new workplane is created on it. # 3. The new workplane is used to drill a hole through the block. # 3a. The hole is automatically centered in the workplane. -result = cq.Workplane("XY").box(length, height, thickness) \ - .faces(">Z").workplane().hole(center_hole_dia) +result = (cq.Workplane("XY").box(length, height, thickness) + .faces(">Z").workplane().hole(center_hole_dia)) # Displays the result of this script show_object(result) diff --git a/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py b/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py index 1b2be255..071dfd9a 100644 --- a/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py +++ b/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py @@ -26,12 +26,12 @@ cbore_depth = 2.1 # Bolt head pocket hole depth # do not show up in the final displayed geometry. # 6. The vertices of the rectangle (corners) are selected, and a counter-bored # hole is placed at each of the vertices (all 4 of them at once). -result = cq.Workplane("XY").box(length, height, thickness) \ - .faces(">Z").workplane().hole(center_hole_dia) \ - .faces(">Z").workplane() \ - .rect(length - cbore_inset, height - cbore_inset, forConstruction=True) \ - .vertices().cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) \ - .edges("|Z").fillet(2.0) +result = (cq.Workplane("XY").box(length, height, thickness) + .faces(">Z").workplane().hole(center_hole_dia) + .faces(">Z").workplane() + .rect(length - cbore_inset, height - cbore_inset, forConstruction=True) + .vertices().cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) + .edges("|Z").fillet(2.0)) # Displays the result of this script show_object(result) diff --git a/examples/Ex004_Extruded_Cylindrical_Plate.py b/examples/Ex004_Extruded_Cylindrical_Plate.py index 6ccb2eca..c9f308ff 100644 --- a/examples/Ex004_Extruded_Cylindrical_Plate.py +++ b/examples/Ex004_Extruded_Cylindrical_Plate.py @@ -21,9 +21,9 @@ rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate # plate with a rectangular hole in the center. # 3a. circle() and rect() could be changed to any other shape to completely # change the resulting plate and/or the hole in it. -result = cq.Workplane("front").circle(circle_radius) \ - .rect(rectangle_width, rectangle_length) \ - .extrude(thickness) +result = (cq.Workplane("front").circle(circle_radius) + .rect(rectangle_width, rectangle_length) + .extrude(thickness)) # Displays the result of this script show_object(result) diff --git a/examples/Ex005_Extruded_Lines_and_Arcs.py b/examples/Ex005_Extruded_Lines_and_Arcs.py index fa93a893..e213f31e 100644 --- a/examples/Ex005_Extruded_Lines_and_Arcs.py +++ b/examples/Ex005_Extruded_Lines_and_Arcs.py @@ -34,12 +34,12 @@ thickness = 0.25 # Thickness of the plate # 7a. Without the close(), the 2D sketch will be left open and the extrude # operation will provide unpredictable results. # 8. The 2D sketch is extruded into a solid object of the specified thickness. -result = cq.Workplane("front").lineTo(width, 0) \ - .lineTo(width, 1.0) \ - .threePointArc((1.0, 1.5), (0.0, 1.0)) \ - .sagittaArc((-0.5, 1.0), 0.2) \ - .radiusArc((-0.7, -0.2), -1.5) \ - .close().extrude(thickness) +result = (cq.Workplane("front").lineTo(width, 0) + .lineTo(width, 1.0) + .threePointArc((1.0, 1.5), (0.0, 1.0)) + .sagittaArc((-0.5, 1.0), 0.2) + .radiusArc((-0.7, -0.2), -1.5) + .close().extrude(thickness)) # Displays the result of this script show_object(result) diff --git a/examples/Ex008_Polygon_Creation.py b/examples/Ex008_Polygon_Creation.py index 2fdecfc5..2853c1e0 100644 --- a/examples/Ex008_Polygon_Creation.py +++ b/examples/Ex008_Polygon_Creation.py @@ -30,10 +30,10 @@ polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points # like cutBlind() assume a positive cut direction, but cutThruAll() assumes # instead that the cut is made from a max direction and cuts downward from # that max through all objects. -result = cq.Workplane("front").box(width, height, thickness) \ - .pushPoints([(0, 0.75), (0, -0.75)]) \ - .polygon(polygon_sides, polygon_dia) \ - .cutThruAll() +result = (cq.Workplane("front").box(width, height, thickness) + .pushPoints([(0, 0.75), (0, -0.75)]) + .polygon(polygon_sides, polygon_dia) + .cutThruAll()) # Displays the result of this script show_object(result) diff --git a/examples/Ex009_Polylines.py b/examples/Ex009_Polylines.py index 85a7d6ae..8712bf99 100644 --- a/examples/Ex009_Polylines.py +++ b/examples/Ex009_Polylines.py @@ -30,10 +30,10 @@ pts = [ # 3. Only half of the I-beam profile has been drawn so far. That half is # mirrored around the Y-axis to create the complete I-beam profile. # 4. The I-beam profile is extruded to the final length of the beam. -result = cq.Workplane("front").moveTo(0, H/2.0) \ - .polyline(pts) \ - .mirrorY() \ - .extrude(L) +result = (cq.Workplane("front").moveTo(0, H/2.0) + .polyline(pts) + .mirrorY() + .extrude(L)) # Displays the result of this script show_object(result) diff --git a/examples/Ex015_Rotated_Workplanes.py b/examples/Ex015_Rotated_Workplanes.py index a964e015..13a9a8e6 100644 --- a/examples/Ex015_Rotated_Workplanes.py +++ b/examples/Ex015_Rotated_Workplanes.py @@ -13,10 +13,10 @@ import cadquery as cq # 6. Selects the vertices of the for-construction rectangle. # 7. Places holes at the center of each selected vertex. # 7a. Since the workplane is rotated, this results in angled holes in the face. -result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") \ - .workplane() \ - .transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0)) \ - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25) +result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") + .workplane() + .transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0)) + .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25)) # Displays the result of this script show_object(result) diff --git a/examples/Ex016_Using_Construction_Geometry.py b/examples/Ex016_Using_Construction_Geometry.py index 48a4f870..aa92a0c4 100644 --- a/examples/Ex016_Using_Construction_Geometry.py +++ b/examples/Ex016_Using_Construction_Geometry.py @@ -12,10 +12,10 @@ import cadquery as cq # other geometry. # 6. Selects the vertices of the for-construction rectangle. # 7. Places holes at the center of each selected vertex. -result = cq.Workplane("front").box(2, 2, 0.5)\ - .faces(">Z").workplane() \ - .rect(1.5, 1.5, forConstruction=True).vertices() \ - .hole(0.125) +result = (cq.Workplane("front").box(2, 2, 0.5) + .faces(">Z").workplane() + .rect(1.5, 1.5, forConstruction=True).vertices() + .hole(0.125)) # Displays the result of this script show_object(result) diff --git a/examples/Ex018_Making_Lofts.py b/examples/Ex018_Making_Lofts.py index 6e9ad1e2..f5ae39b8 100644 --- a/examples/Ex018_Making_Lofts.py +++ b/examples/Ex018_Making_Lofts.py @@ -11,10 +11,10 @@ import cadquery as cq # 5. Creates a workplane 3 mm above the face the circle was drawn on. # 6. Draws a 2D circle on the new, offset workplane. # 7. Creates a loft between the circle and the rectangle. -result = cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") \ - .circle(1.5).workplane(offset=3.0) \ - .rect(0.75, 0.5) \ - .loft(combine=True) +result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") + .circle(1.5).workplane(offset=3.0) + .rect(0.75, 0.5) + .loft(combine=True)) # Displays the result of this script show_object(result) diff --git a/examples/Ex019_Counter_Sunk_Holes.py b/examples/Ex019_Counter_Sunk_Holes.py index e75039a4..5ca39c62 100644 --- a/examples/Ex019_Counter_Sunk_Holes.py +++ b/examples/Ex019_Counter_Sunk_Holes.py @@ -11,9 +11,9 @@ import cadquery as cq # function. # 5a. When the depth of the counter-sink hole is set to None, the hole will be # cut through. -result = cq.Workplane(cq.Plane.XY()).box(4, 2, 0.5).faces(">Z") \ - .workplane().rect(3.5, 1.5, forConstruction=True) \ - .vertices().cskHole(0.125, 0.25, 82.0, depth=None) +result = (cq.Workplane(cq.Plane.XY()).box(4, 2, 0.5).faces(">Z") + .workplane().rect(3.5, 1.5, forConstruction=True) + .vertices().cskHole(0.125, 0.25, 82.0, depth=None)) # Displays the result of this script show_object(result) diff --git a/examples/Ex021_Splitting_an_Object.py b/examples/Ex021_Splitting_an_Object.py index e903a13d..5f878bf6 100644 --- a/examples/Ex021_Splitting_an_Object.py +++ b/examples/Ex021_Splitting_an_Object.py @@ -9,8 +9,8 @@ import cadquery as cq # that new geometry can be built on. # 4. Draws a 2D circle on the new workplane and then uses it to cut a hole # all the way through the box. -c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane() \ - .circle(0.25).cutThruAll() +c = (cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane() + .circle(0.25).cutThruAll()) # 5. Selects the face furthest away from the origin in the +Y axis direction. # 6. Creates an offset workplane that is set in the center of the object. diff --git a/examples/Ex024_Sweep_With_Multiple_Sections.py b/examples/Ex024_Sweep_With_Multiple_Sections.py index 6659e61e..0c76f93d 100644 --- a/examples/Ex024_Sweep_With_Multiple_Sections.py +++ b/examples/Ex024_Sweep_With_Multiple_Sections.py @@ -4,37 +4,37 @@ import cadquery as cq path = cq.Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) # Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 -defaultSweep = cq.Workplane("YZ").workplane(offset=-10.0).circle(2.0). \ - workplane(offset=10.0).circle(1.0). \ - workplane(offset=10.0).circle(2.0).sweep(path, multisection=True) +defaultSweep = (cq.Workplane("YZ").workplane(offset=-10.0).circle(2.0). + workplane(offset=10.0).circle(1.0). + workplane(offset=10.0).circle(2.0).sweep(path, multisection=True)) # We can sweep thrue different shapes -recttocircleSweep = cq.Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). \ - workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). \ - workplane(offset=8.0).rect(2.0, 2.0).sweep(path, multisection=True) +recttocircleSweep = (cq.Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). + workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). + workplane(offset=8.0).rect(2.0, 2.0).sweep(path, multisection=True)) -circletorectSweep = cq.Workplane("YZ").workplane(offset=-10.0).circle(1.0). \ - workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). \ - workplane(offset=7.0).circle(1.0).sweep(path, multisection=True) +circletorectSweep = (cq.Workplane("YZ").workplane(offset=-10.0).circle(1.0). + workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). + workplane(offset=7.0).circle(1.0).sweep(path, multisection=True)) # Placement of the Shape is important otherwise could produce unexpected shape -specialSweep = cq.Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0). \ - sweep(path, multisection=True) +specialSweep = (cq.Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0). + sweep(path, multisection=True)) # Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 -path = cq.Workplane("XZ").moveTo(-5, 4).lineTo(0, 4). \ - threePointArc((4, 0), (0, -4)).lineTo(-5, -4) +path = (cq.Workplane("XZ").moveTo(-5, 4).lineTo(0, 4). + threePointArc((4, 0), (0, -4)).lineTo(-5, -4)) # Placement of different shapes should follow the path # cylinder r=1.5 along first line # then sweep allong arc from r=1.5 to r=1.0 # then cylinder r=1.0 along last line -arcSweep = cq.Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). \ - workplane(offset=5).circle(1.5). \ - moveTo(0, -8).circle(1.0). \ - workplane(offset=-5).circle(1.0). \ - sweep(path, multisection=True) +arcSweep = (cq.Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). + workplane(offset=5).circle(1.5). + moveTo(0, -8).circle(1.0). + workplane(offset=-5).circle(1.0). + sweep(path, multisection=True)) # Translate the resulting solids so that they do not overlap and display them left to right diff --git a/examples/Ex100_Lego_Brick.py b/examples/Ex100_Lego_Brick.py index 6a6381f7..e0981255 100644 --- a/examples/Ex100_Lego_Brick.py +++ b/examples/Ex100_Lego_Brick.py @@ -32,23 +32,23 @@ s = cq.Workplane("XY").box(total_length, total_width, height) s = s.faces("Z").workplane(). \ - rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) \ - .extrude(bumpHeight) +s = (s.faces(">Z").workplane(). + rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) + .extrude(bumpHeight)) # add posts on the bottom. posts are different diameter depending on geometry # solid studs for 1 bump, tubes for multiple, none for 1x1 tmp = s.faces(" 1 and wbumps > 1: - tmp = tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). \ - circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). + circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t)) elif lbumps > 1: - tmp = tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). \ - circle(t).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). + circle(t).extrude(height - t)) elif wbumps > 1: - tmp = tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). \ - circle(t).extrude(height - t) + tmp = (tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). + circle(t).extrude(height - t)) else: tmp = s From 6beb5d591413b5ddbca73e059e37b5abcad3c3a0 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Sun, 1 Dec 2019 15:26:17 -0500 Subject: [PATCH 02/70] Fixed makeWedge function and made wedge primitive handling consistent. --- cadquery/cq.py | 58 +++++++++++++++++++++++++++++++++++++ cadquery/occ_impl/shapes.py | 25 ++++++++-------- tests/TestCadQuery.py | 32 +++++++++++++++++++- 3 files changed, 101 insertions(+), 14 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index bc289659..02916153 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -2862,6 +2862,64 @@ class Workplane(CQ): else: return self.union(spheres, clean=clean) + def wedge(self, dx, dy, dz, xmin, zmin, xmax, zmax, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), + centered=(True, True, True), combine=True, clean=True): + """ + :param dx: Distance along the X axis + :param dy: Distance along the Y axis + :param dz: Distance along the Z axis + :param xmin: The minimum X location + :param zmin:The minimum Z location + :param xmax:The maximum X location + :param zmax: The maximum Z location + :param pnt: A vector for the origin of the direction for the wedge + :param dir: The direction vector for the major axis of the wedge + :param combine: Whether the results should be combined with other solids on the stack + (and each other) + :param clean: true to attempt to have the kernel clean up the geometry, false otherwise + :return: A wedge object for each point on the stack + + One wedge is created for each item on the current stack. If no items are on the stack, one + wedge using the current workplane center is created. + + If combine is true, the result will be a single object on the stack: + If a solid was found in the chain, the result is that solid with all wedges produced + fused onto it otherwise, the result is the combination of all the produced wedges + + If combine is false, the result will be a list of the wedges produced + """ + + # Convert the point tuple to a vector, if needed + if isinstance(pnt, tuple): + pnt = Vector(pnt) + + # Convert the direction tuple to a vector, if needed + if isinstance(dir, tuple): + dir = Vector(dir) + + def _makewedge(pnt): + (xp, yp, zp) = pnt.toTuple() + + if not centered[0]: + xp += dx / 2. + + if not centered[1]: + yp += dy / 2. + + if not centered[2]: + zp += dx / 2. + + return Solid.makeWedge(dx, dy, dz, xmin, zmin, xmax, zmax, Vector(xp, yp, zp), dir) + + # We want a wedge for each point on the workplane + wedges = self.eachpoint(_makewedge) + + # If we don't need to combine everything, just return the created wedges + if not combine: + return wedges + else: + return self.union(wedges, clean=clean) + def clean(self): """ Cleans the current solid by removing unwanted edges from the diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 5cf7e7cc..4adcacf4 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -1226,23 +1226,22 @@ class Solid(Shape, Mixin3D): return cls(loft_builder.Shape()) @classmethod - def makeWedge(cls, xmin, ymin, zmin, z2min, x2min, xmax, ymax, zmax, z2max, x2max, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)): + def makeWedge(cls, dx, dy, dz, xmin, zmin, xmax, zmax, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)): """ Make a wedge located in pnt By default pnt=Vector(0,0,0) and dir=Vector(0,0,1) """ - return cls(BRepPrimAPI_MakeWedge(gp_Ax2(pnt.toPnt(), - dir.toDir()), - xmin, - ymin, - zmin, - z2min, - x2min, - xmax, - ymax, - zmax, - z2max, - x2max).Solid()) + + return cls(BRepPrimAPI_MakeWedge( + gp_Ax2(pnt.toPnt(), + dir.toDir()), + dx, + dy, + dz, + xmin, + zmin, + xmax, + zmax).Solid()) @classmethod def makeSphere(cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees1=0, angleDegrees2=90, angleDegrees3=360): diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index 205c1d8e..ddd7cc45 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -1365,7 +1365,7 @@ class TestCadQuery(BaseTest): def testSphereDefaults(self): s = Workplane("XY").sphere(10) - # self.saveModel(s) # Until FreeCAD fixes their sphere operation + self.saveModel(s) # Until FreeCAD fixes their sphere operation self.assertEqual(1, s.solids().size()) self.assertEqual(1, s.faces().size()) @@ -1390,6 +1390,36 @@ class TestCadQuery(BaseTest): self.assertEqual(1, s.solids().size()) self.assertEqual(4, s.faces().size()) + def testWedgeDefaults(self): + s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5) + self.saveModel(s) + self.assertEqual(1, s.solids().size()) + self.assertEqual(5, s.faces().size()) + self.assertEqual(5, s.vertices().size()) + + def testWedgeCentering(self): + s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5, centered=(False, False, False)) + # self.saveModel(s) + self.assertEqual(1, s.solids().size()) + self.assertEqual(5, s.faces().size()) + self.assertEqual(5, s.vertices().size()) + + def testWedgePointList(self): + s = Workplane("XY").rect( + 4.0, 4.0, forConstruction=True).vertices().wedge(10, 10, 10, 5, 5, 5, 5, combine=False) + #self.saveModel(s) + self.assertEqual(4, s.solids().size()) + self.assertEqual(20, s.faces().size()) + self.assertEqual(20, s.vertices().size()) + + def testWedgeCombined(self): + s = Workplane("XY").rect( + 4.0, 4.0, forConstruction=True).vertices().wedge(10, 10, 10, 5, 5, 5, 5, combine=True) + # self.saveModel(s) + self.assertEqual(1, s.solids().size()) + self.assertEqual(12, s.faces().size()) + self.assertEqual(16, s.vertices().size()) + def testQuickStartXY(self): s = Workplane(Plane.XY()).box(2, 4, 0.5).faces(">Z").workplane().rect(1.5, 3.5, forConstruction=True)\ .vertices().cskHole(0.125, 0.25, 82, depth=None) From 03c45c72b28c279a85140358d7f01fcbdbb980b6 Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Mon, 2 Dec 2019 05:55:37 -0500 Subject: [PATCH 03/70] Simple docstring addition to note acceptance of vector or tuple. --- cadquery/cq.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 02916153..b666c8db 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -2872,8 +2872,8 @@ class Workplane(CQ): :param zmin:The minimum Z location :param xmax:The maximum X location :param zmax: The maximum Z location - :param pnt: A vector for the origin of the direction for the wedge - :param dir: The direction vector for the major axis of the wedge + :param pnt: A vector (or tuple) for the origin of the direction for the wedge + :param dir: The direction vector (or tuple) for the major axis of the wedge :param combine: Whether the results should be combined with other solids on the stack (and each other) :param clean: true to attempt to have the kernel clean up the geometry, false otherwise From 2732acf6b82f959aa74d4cb98f9310708c87c0ca Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Wed, 11 Dec 2019 13:49:16 -0500 Subject: [PATCH 04/70] Corrected an error in the Workplane.center docstring example. --- cadquery/cq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index b666c8db..23bb30e4 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1187,7 +1187,7 @@ class Workplane(CQ): #this workplane is centered at x=0.5,y=0.5, the center of the upper face s = Workplane().box(1,1,1).faces(">Z").workplane() - s.center(-0.5,-0.5) # move the center to the corner + s = s.center(-0.5,-0.5) # move the center to the corner t = s.circle(0.25).extrude(0.2) assert ( t.faces().size() == 9 ) # a cube with a cylindrical nub at the top right corner From aadbf26eb175034cbd20e621d0cdccb7f71cd170 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20S=C3=A1nchez=20de=20Le=C3=B3n=20Peque?= Date: Thu, 12 Dec 2019 20:04:38 +0100 Subject: [PATCH 05/70] Remove unused requirements files --- requirements-dev.txt | 3 --- requirements.txt | 0 2 files changed, 3 deletions(-) delete mode 100644 requirements-dev.txt delete mode 100644 requirements.txt diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 872a66d2..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,3 +0,0 @@ -sphinx-rtd-theme==0.1.9 -travis-sphinx==1.1.0 -Sphinx==1.3.1 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29b..00000000 From 5069483aefea518ce8e467eb2c88e1d1ed8a4edb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20S=C3=A1nchez=20de=20Le=C3=B3n=20Peque?= Date: Thu, 12 Dec 2019 19:51:08 +0100 Subject: [PATCH 06/70] Use pytest to run the test suite --- .travis.yml | 9 ++++---- appveyor.yml | 6 ++--- environment.yml | 7 ++---- runtests.py | 22 ------------------- setup.py | 11 ++++++++++ ...{TestCadObjects.py => test_cad_objects.py} | 0 tests/{TestCadQuery.py => test_cadquery.py} | 0 tests/{TestCQGI.py => test_cqgi.py} | 0 tests/{TestExporters.py => test_exporters.py} | 0 tests/{TestImporters.py => test_importers.py} | 0 tests/{TestJupyter.py => test_jupyter.py} | 0 .../{TestCQSelectors.py => test_selectors.py} | 0 .../{TestWorkplanes.py => test_workplanes.py} | 0 13 files changed, 20 insertions(+), 35 deletions(-) delete mode 100644 runtests.py rename tests/{TestCadObjects.py => test_cad_objects.py} (100%) rename tests/{TestCadQuery.py => test_cadquery.py} (100%) rename tests/{TestCQGI.py => test_cqgi.py} (100%) rename tests/{TestExporters.py => test_exporters.py} (100%) rename tests/{TestImporters.py => test_importers.py} (100%) rename tests/{TestJupyter.py => test_jupyter.py} (100%) rename tests/{TestCQSelectors.py => test_selectors.py} (100%) rename tests/{TestWorkplanes.py => test_workplanes.py} (100%) diff --git a/.travis.yml b/.travis.yml index c6a7ba09..2f963664 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,26 +51,25 @@ before_install: - hash -r; - conda config --set always_yes yes --set changeps1 no; - conda create -y -q -n test_cq -c cadquery -c conda-forge pythonocc-core=0.18.2 oce=0.18.2 python=$TRAVIS_PYTHON_VERSION - pyparsing mock; - source ~/miniconda/bin/activate test_cq; +- pip install .[dev] - python -c 'import OCC.gp as gp; print(gp.gp_Vec())' -- pip install codecov install: - python setup.py install before_script: -- ulimit -c unlimited -S +- ulimit -c unlimited -S - sudo rm -f /cores/core.* script: -- coverage run runtests.py +- pytest -v --cov after_success: - codecov after_failure: -- ls /cores/core.* +- ls /cores/core.* - lldb --core `ls /cores/core.*` --batch --one-line "bt" diff --git a/appveyor.yml b/appveyor.yml index 0f62ef51..6715e5b6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,14 +19,14 @@ install: - conda update -q conda - conda create --quiet --name cqtest -c cadquery -c conda-forge -c dlr-sc pythonocc-core=0.18.2 python=%PYTHON_VERSION% pyparsing mock coverage codecov - activate cqtest - - pip install codecov + - pip install .[dev] - python setup.py install build: false test_script: - - coverage run runtests.py - + - pytest -v --cov + on_success: - codecov diff --git a/environment.yml b/environment.yml index dfc98e17..924a4972 100644 --- a/environment.yml +++ b/environment.yml @@ -6,9 +6,6 @@ dependencies: - python=3.6 - pythonocc-core=0.18.2 - oce=0.18.2 - - pyparsing + - pip - pip: - - "--editable=." - # Documentation - - sphinx - - sphinx_rtd_theme + - "--editable=.[dev]" diff --git a/runtests.py b/runtests.py deleted file mode 100644 index f1052ca9..00000000 --- a/runtests.py +++ /dev/null @@ -1,22 +0,0 @@ -import sys -from tests import * -import cadquery -import unittest - -#if you are on python 2.7, you can use -m uniitest discover. -#but this is required for python 2.6.6 on windows. FreeCAD0.12 will not load -#on py 2.7.x on win -suite = unittest.TestSuite() - -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadObjects.TestCadObjects)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQGI.TestCQGI)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExporters)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestImporters.TestImporters)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestJupyter.TestJupyter)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes)) - -if __name__ == '__main__': - result = unittest.TextTestRunner().run(suite) - sys.exit(not result.wasSuccessful()) diff --git a/setup.py b/setup.py index 24d2a8e1..a2873e33 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,17 @@ setup( long_description=open('README.md').read(), packages=['cadquery','cadquery.contrib','cadquery.occ_impl','cadquery.plugins','tests'], install_requires=['pyparsing'], + extras_require={ + 'dev': [ + # Documentation + 'sphinx', + 'sphinx_rtd_theme', + # Testing + 'codecov', + 'pytest', + 'pytest-cov', + ], + }, include_package_data=True, zip_safe=False, platforms='any', diff --git a/tests/TestCadObjects.py b/tests/test_cad_objects.py similarity index 100% rename from tests/TestCadObjects.py rename to tests/test_cad_objects.py diff --git a/tests/TestCadQuery.py b/tests/test_cadquery.py similarity index 100% rename from tests/TestCadQuery.py rename to tests/test_cadquery.py diff --git a/tests/TestCQGI.py b/tests/test_cqgi.py similarity index 100% rename from tests/TestCQGI.py rename to tests/test_cqgi.py diff --git a/tests/TestExporters.py b/tests/test_exporters.py similarity index 100% rename from tests/TestExporters.py rename to tests/test_exporters.py diff --git a/tests/TestImporters.py b/tests/test_importers.py similarity index 100% rename from tests/TestImporters.py rename to tests/test_importers.py diff --git a/tests/TestJupyter.py b/tests/test_jupyter.py similarity index 100% rename from tests/TestJupyter.py rename to tests/test_jupyter.py diff --git a/tests/TestCQSelectors.py b/tests/test_selectors.py similarity index 100% rename from tests/TestCQSelectors.py rename to tests/test_selectors.py diff --git a/tests/TestWorkplanes.py b/tests/test_workplanes.py similarity index 100% rename from tests/TestWorkplanes.py rename to tests/test_workplanes.py From 883db3cdf254810c6a11025ac63b941b90230252 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Fri, 13 Dec 2019 08:22:59 +0100 Subject: [PATCH 07/70] Update shapes.py BRepBuilderAPI_MakeWire::Add(const TopTools_ListOfShape & L) offers the option to accept a list of shapes directly as argument: "Adds the edges of to the current wire. The edges are not to be consecutive. But they are to be all connected geometrically or topologically. If some of them are not connected the Status give DisconnectedWire but the "Maker" is Done() and you can get the partial result. (ie connected to the first edgeof the list )" Following this I modified the assembledEdges in shapes.py to be able to provide a list of unordered edges to BRepBuilderAPI_MakeWire. This way, when the list of edges is generated by another function, there is not need to make them consecutive. --- cadquery/occ_impl/shapes.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 4adcacf4..092a5734 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -809,12 +809,15 @@ class Wire(Shape, Mixin1D): """ Attempts to build a wire that consists of the edges in the provided list :param cls: - :param listOfEdges: a list of Edge objects + :param listOfEdges: a list of Edge objects. The edges are not to be consecutive. :return: a wire with the edges assembled """ + wire_builder = BRepBuilderAPI_MakeWire() - for edge in listOfEdges: - wire_builder.Add(edge.wrapped) + edges_list = TopTools_ListOfShape() + for e in listOfEdges: + edges_list.Append(e.wrapped) + wire_builder.Add(edges_list) return cls(wire_builder.Wire()) From 46c237581905d86d82e0c28d4328cc555f1027ec Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sun, 15 Dec 2019 09:12:06 +0100 Subject: [PATCH 08/70] Update shapes.py --- cadquery/occ_impl/shapes.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 092a5734..5a8b2e33 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -112,6 +112,7 @@ from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier from math import pi, sqrt from functools import reduce +import warnings TOLERANCE = 1e-6 DEG2RAD = 2 * pi / 360. @@ -818,6 +819,11 @@ class Wire(Shape, Mixin1D): for e in listOfEdges: edges_list.Append(e.wrapped) wire_builder.Add(edges_list) + if not wire_builder.IsDone(): + w1 = 'BRepBuilderAPI_MakeWire::IsDone(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = '+ str(wire_builder.IsDone()) + w2 = 'BRepBuilderAPI_MakeWire::Error(): returns true if this algorithm contains a valid wire. IsDone returns false if: there are no edges in the wire, or the last edge which you tried to add was not connectable = ' + str(wire_builder.Error()) + warnings.warn(w1) + warnings.warn(w2) return cls(wire_builder.Wire()) From 23f2ba679f69e2c15ab8b87dcda9b1efedf00442 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sun, 15 Dec 2019 09:26:59 +0100 Subject: [PATCH 09/70] Update shapes.py --- cadquery/occ_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 5a8b2e33..487391f4 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -825,7 +825,7 @@ class Wire(Shape, Mixin1D): warnings.warn(w1) warnings.warn(w2) - return cls(wire_builder.Wire()) + return cls(wire_builder.Wire()) @classmethod def makeCircle(cls, radius, center, normal): From b19b8ff541f665f61a57fcdd4c6cb72bb3a7fcdd Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sun, 15 Dec 2019 09:38:56 +0100 Subject: [PATCH 10/70] Update shapes.py --- cadquery/occ_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 487391f4..5a8b2e33 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -825,7 +825,7 @@ class Wire(Shape, Mixin1D): warnings.warn(w1) warnings.warn(w2) - return cls(wire_builder.Wire()) + return cls(wire_builder.Wire()) @classmethod def makeCircle(cls, radius, center, normal): From e9924ba6e4cb06f784e90c00eae0cc9c859d8190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20S=C3=A1nchez=20de=20Le=C3=B3n=20Peque?= Date: Thu, 12 Dec 2019 19:38:58 +0100 Subject: [PATCH 11/70] Fix plane rotation method The vector defining the rotation is expected to be in local coordinates and therefore needs to be converted to world coordinates before applying the rotation. --- cadquery/occ_impl/geom.py | 2 +- tests/test_cadquery.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 0c31fe22..d959702a 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -619,7 +619,7 @@ class Plane(object): :param rotate: Vector [xDegrees, yDegrees, zDegrees] :return: a copy of this plane rotated as requested. """ - rotate = Vector(rotate) + rotate = Vector(self.toWorldCoords(rotate)) # Convert to radians. rotate = rotate.multiply(math.pi / 180.0) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index ddd7cc45..64b85237 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -4,6 +4,9 @@ """ # system modules import math,os.path,time,tempfile +from random import choice +from random import random +from random import randrange # my modules from cadquery import * @@ -246,6 +249,32 @@ class TestCadQuery(BaseTest): self.assertEqual(-0.5, endPoint[1]) self.assertEqual(2.5, endPoint[2]) + def testPlaneRotateZNormal(self): + """ + Rotation of a plane in the Z direction should never alter its normal. + + This test creates random planes, with the normal in a random direction + among positive and negative X, Y and Z. The plane is defined with this + normal and another random perpendicular vector (the X-direction of the + plane). The plane is finally rotated a random angle in the Z-direction + to verify that the resulting plane maintains the same normal. + """ + for _ in range(100): + normal_sign = choice((-1, 1)) + normal_dir = randrange(3) + angle = (random() - 0.5) * 720 + + normal = [0, 0, 0] + normal[normal_dir] = normal_sign + xdir = [random(), random(), random()] + xdir[normal_dir] = 0 + + plane = Plane(origin=(0, 0, 0), xDir=xdir, normal=normal) + rotated = plane.rotated((0, 0, angle)).zDir.toTuple() + self.assertAlmostEqual(rotated[0], normal[0]) + self.assertAlmostEqual(rotated[1], normal[1]) + self.assertAlmostEqual(rotated[2], normal[2]) + def testLoft(self): """ Test making a lofted solid From a7ca48e34e16ab75f3abe1ccb0b91bb00d067bb8 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sun, 15 Dec 2019 18:30:03 +0100 Subject: [PATCH 12/70] Add files via upload --- tests/TestAssembleEdgesWarnings.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/TestAssembleEdgesWarnings.py diff --git a/tests/TestAssembleEdgesWarnings.py b/tests/TestAssembleEdgesWarnings.py new file mode 100644 index 00000000..d2efda02 --- /dev/null +++ b/tests/TestAssembleEdgesWarnings.py @@ -0,0 +1,19 @@ +""" + Tests assembleEdges warnings functionality +""" + +import cadquery as cq + +class TestAssembleEdgesWarning(BaseTest): + + def plate(self): + + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + thickness = 0.1 + edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] + edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) + edge_wire = edge_wire.add(cq.Workplane('XY').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) # Triggers BRepBuilderAPI_MakeWire error + surface_points = [[-3.,-3.,-3.], [3.,3.,3.]] + plate_1 = cq.Workplane('XY').interpPlate(surface_points, edge_wire, thickness) + + return plate_1 From fd1e959aa7d1448e540917d87918239247eaaf43 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sun, 15 Dec 2019 18:31:40 +0100 Subject: [PATCH 13/70] Update shapes.py --- cadquery/occ_impl/shapes.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 5a8b2e33..946c8843 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -819,9 +819,9 @@ class Wire(Shape, Mixin1D): for e in listOfEdges: edges_list.Append(e.wrapped) wire_builder.Add(edges_list) - if not wire_builder.IsDone(): - w1 = 'BRepBuilderAPI_MakeWire::IsDone(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = '+ str(wire_builder.IsDone()) - w2 = 'BRepBuilderAPI_MakeWire::Error(): returns true if this algorithm contains a valid wire. IsDone returns false if: there are no edges in the wire, or the last edge which you tried to add was not connectable = ' + str(wire_builder.Error()) + if wire_builder.Error(): + w1 = 'BRepBuilderAPI_MakeWire::IsDone(): returns true if this algorithm contains a valid wire. IsDone returns false if: there are no edges in the wire, or the last edge which you tried to add was not connectable = '+ str(wire_builder.IsDone()) + w2 = 'BRepBuilderAPI_MakeWire::Error(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = ' + str(wire_builder.Error()) warnings.warn(w1) warnings.warn(w2) From 80b93dadc5cfc85b45d6fd83961c8dd5c535dfea Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Mon, 16 Dec 2019 08:45:28 +0100 Subject: [PATCH 14/70] Update TestAssembleEdgesWarnings.py --- tests/TestAssembleEdgesWarnings.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/TestAssembleEdgesWarnings.py b/tests/TestAssembleEdgesWarnings.py index d2efda02..e4244739 100644 --- a/tests/TestAssembleEdgesWarnings.py +++ b/tests/TestAssembleEdgesWarnings.py @@ -12,8 +12,8 @@ class TestAssembleEdgesWarning(BaseTest): thickness = 0.1 edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) - edge_wire = edge_wire.add(cq.Workplane('XY').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) # Triggers BRepBuilderAPI_MakeWire error - surface_points = [[-3.,-3.,-3.], [3.,3.,3.]] - plate_1 = cq.Workplane('XY').interpPlate(surface_points, edge_wire, thickness) + edge_wire = edge_wire.add(cq.Workplane('XY').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) # Triggers BRepBuilderAPI_MakeWire error ('YZ' is correct) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) - return plate_1 + return edge_wire From e69b2f83bde43ad6a0159a72bc058165e684e72e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Tue, 17 Dec 2019 18:28:22 +0100 Subject: [PATCH 15/70] Use pytest in azure as well (#239) * Use pytest in azure as well * Specify test requirements properly * Delete circle ci config --- .circleci/config.yml | 35 ----------------------------------- conda/meta.yaml | 5 +++-- 2 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 51ba9fad..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: 2 - -jobs: - runtests: - - macos: - xcode: "9.0" - - environment: - PYTEST_QT_API: pyqt5 - PYTHON_VERSION: 3.6 - - steps: - - checkout - - run: HOMEBREW_NO_AUTO_UPDATE=1 brew install wget - - run: cd && rm -rf ~/.pyenv && rm -rf ~/virtualenvs - - run: wget https://repo.continuum.io/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -O ~/miniconda.sh - - run: chmod +x ~/miniconda.sh && ~/miniconda.sh -b - - run: echo "export PATH=~/miniconda3/bin:$PATH" >> $BASH_ENV - - run: conda config --set always_yes yes - - run: conda create --quiet --name cqtest -c cadquery -c conda-forge -c dlr-sc pythonocc-core=0.18.2 oce=0.18.2 python=$PYTHON_VERSION pyparsing mock lldb - - run: | - source activate cqtest - pip install coverage - python setup.py install - conda env list - conda list - coverage run runtests.py - -workflows: - version: 2 - - workflow: - jobs: - - runtests \ No newline at end of file diff --git a/conda/meta.yaml b/conda/meta.yaml index 2fe8c171..ce663fda 100644 --- a/conda/meta.yaml +++ b/conda/meta.yaml @@ -20,11 +20,12 @@ requirements: - pyparsing 2.* test: + requires: + - pytest source_files: - - runtests.py - tests/ commands: - - python runtests.py + - pytest -v about: summary: CadQuery fork based on PythonOCC From f40a8370f704a750acfce5110e8a339c245a34d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sat, 21 Dec 2019 00:12:26 +0100 Subject: [PATCH 16/70] Update environment.yaml --- environment.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index 924a4972..bf8e1c25 100644 --- a/environment.yml +++ b/environment.yml @@ -7,5 +7,11 @@ dependencies: - pythonocc-core=0.18.2 - oce=0.18.2 - pip + - pyparsing + - sphinx + - sphinx_rtd_theme + - codecov + - pytest + - pytest-cov - pip: - - "--editable=.[dev]" + - "--editable=." From 853611d7a131f436b1ce0909edbb4fd4faac51ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sat, 21 Dec 2019 00:14:52 +0100 Subject: [PATCH 17/70] Remove explicit dependencies from setup.py Rationale: they were not complete anyhow --- setup.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/setup.py b/setup.py index a2873e33..3faff965 100644 --- a/setup.py +++ b/setup.py @@ -31,18 +31,6 @@ setup( description='CadQuery is a parametric scripting language for creating and traversing CAD models', long_description=open('README.md').read(), packages=['cadquery','cadquery.contrib','cadquery.occ_impl','cadquery.plugins','tests'], - install_requires=['pyparsing'], - extras_require={ - 'dev': [ - # Documentation - 'sphinx', - 'sphinx_rtd_theme', - # Testing - 'codecov', - 'pytest', - 'pytest-cov', - ], - }, include_package_data=True, zip_safe=False, platforms='any', From 9c1758f69a7edbc441a2a3dc00095d90c39df52e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Sat, 21 Dec 2019 00:17:33 +0100 Subject: [PATCH 18/70] Allow python>=3.6 --- environment.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/environment.yml b/environment.yml index bf8e1c25..5b4d5cac 100644 --- a/environment.yml +++ b/environment.yml @@ -3,7 +3,7 @@ channels: - conda-forge - cadquery dependencies: - - python=3.6 + - python>=3.6 - pythonocc-core=0.18.2 - oce=0.18.2 - pip From 6041766227413e407f5bfe295452494f719f8020 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 21 Dec 2019 01:40:52 +0100 Subject: [PATCH 19/70] Further env file cleanup --- environment.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/environment.yml b/environment.yml index 5b4d5cac..108bc9e8 100644 --- a/environment.yml +++ b/environment.yml @@ -1,17 +1,17 @@ name: cadquery channels: - - conda-forge - cadquery + - conda-forge + - defaults dependencies: - python>=3.6 - - pythonocc-core=0.18.2 - - oce=0.18.2 - - pip + - cadquery::pythonocc-core - pyparsing - sphinx - sphinx_rtd_theme - codecov - pytest - pytest-cov + - pip - pip: - "--editable=." From 6e1bb56805fef8d8dfa561c2b4238edc25b83c34 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 21 Dec 2019 01:41:34 +0100 Subject: [PATCH 20/70] CI pipelines cleanup --- .travis.yml | 33 +++++++++------------------------ appveyor.yml | 30 +++++------------------------- 2 files changed, 14 insertions(+), 49 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2f963664..e1c8d62f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,6 @@ dist: trusty branches: only: - - adam-urbanczyk-OCC-version-update - master - "/\\d+\\.\\d+\\.?d*\\-*[a-z]*/" @@ -25,17 +24,17 @@ env: matrix: include: - - env: TRAVIS_PYTHON_VERSION=3.6 + - env: PYTHON_VERSION=3.6 os: osx - - env: TRAVIS_PYTHON_VERSION=3.6 + - env: PYTHON_VERSION=3.6 os: linux - - env: TRAVIS_PYTHON_VERSION=3.7 + - env: PYTHON_VERSION=3.7 os: osx - - env: TRAVIS_PYTHON_VERSION=3.7 + - env: PYTHON_VERSION=3.7 os: linux before_install: -- if [[ "$TRAVIS_PYTHON_VERSION" == "2.7" ]]; then +- if [[ "$PYTHON_VERSION" == "2.7" ]]; then PY_MAJOR=2 ; else PY_MAJOR=3 ; @@ -48,12 +47,10 @@ before_install: wget https://repo.continuum.io/miniconda/Miniconda$PY_MAJOR-latest-$OS-x86_64.sh -O miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda; - export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH"; -- hash -r; - conda config --set always_yes yes --set changeps1 no; -- conda create -y -q -n test_cq -c cadquery -c conda-forge pythonocc-core=0.18.2 oce=0.18.2 python=$TRAVIS_PYTHON_VERSION -- source ~/miniconda/bin/activate test_cq; -- pip install .[dev] -- python -c 'import OCC.gp as gp; print(gp.gp_Vec())' +- conda env create -f environment.yml +- conda activate cadquery +- conda install python=$PYTHON_VERSION install: - python setup.py install @@ -70,16 +67,4 @@ after_success: after_failure: - ls /cores/core.* -- lldb --core `ls /cores/core.*` --batch --one-line "bt" - - -before_deploy: -- conda install anaconda-client conda-build -- sh conda_build.sh - -deploy: -- provider: script - skip_cleanup: true - script: anaconda -v -t $ANACONDA_TOKEN upload --force --user cadquery /tmp/cbld/**/cadquery-*.tar.bz2 - on: - tags: true +- lldb --core `ls /cores/core.*` --batch --one-line "bt" \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 6715e5b6..f598a077 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ environment: MINICONDA_DIRNAME: C:\Miniconda36-x64 - PYTHON_VERSION: 3.7 MINICONDA_DIRNAME: C:\Miniconda37-x64 - + ANACONDA_TOKEN: secure: nxF/a2f3iS9KXGu7B/wKJYAk7Sm5wyAjoZoqJvPbRoVK4saaozVwOxDrjwJjJAYb @@ -17,10 +17,9 @@ install: - set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" - conda config --set always_yes yes - conda update -q conda - - conda create --quiet --name cqtest -c cadquery -c conda-forge -c dlr-sc pythonocc-core=0.18.2 python=%PYTHON_VERSION% pyparsing mock coverage codecov - - activate cqtest - - pip install .[dev] - - python setup.py install + - conda env create -f environment.yml + - conda activate cadquery + - conda install python=%PYTHON_VERSION% build: false @@ -28,23 +27,4 @@ test_script: - pytest -v --cov on_success: - - codecov - -#deploy: -#- provider: Script -# on: -# APPVEYOR_REPO_TAG: true - -#before_deploy: -# - set "PATH=%MINICONDA_DIRNAME%;%MINICONDA_DIRNAME%\\Scripts;%PATH%" -# - set "TRAVIS_TAG=%APPVEYOR_REPO_TAG_NAME%" -# - set "TRAVIS_COMMIT=%APPVEYOR_REPO_COMMIT%" -# - set "TRAVIS_PYTHON_VERSION=%PYTHON_VERSION%" -# - conda config --set always_yes yes -# - activate cqtest -# - conda install anaconda-client conda-build -# - conda info --envs -# - call conda_build.bat - -#deploy_script: -# - anaconda -v -t %ANACONDA_TOKEN% upload --force --user cadquery ./win-64/cadquery-*.tar.bz2 + - codecov \ No newline at end of file From e18fcfad5ad8982e4145c6b1a5dfabd3aa80950a Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 21 Dec 2019 01:49:01 +0100 Subject: [PATCH 21/70] Fixed env activation --- .travis.yml | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index e1c8d62f..36396b05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ before_install: - export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH"; - conda config --set always_yes yes --set changeps1 no; - conda env create -f environment.yml -- conda activate cadquery +- source ~/miniconda/bin/activate cadquery - conda install python=$PYTHON_VERSION install: diff --git a/appveyor.yml b/appveyor.yml index f598a077..58d19f90 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -18,7 +18,7 @@ install: - conda config --set always_yes yes - conda update -q conda - conda env create -f environment.yml - - conda activate cadquery + - activate cadquery - conda install python=%PYTHON_VERSION% build: false From 8720db645ef9819ed91e6dac3e2b4583023078d3 Mon Sep 17 00:00:00 2001 From: adam-urbanczyk Date: Sat, 21 Dec 2019 01:59:44 +0100 Subject: [PATCH 22/70] Specify channels when updating python --- .travis.yml | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 36396b05..247371cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -50,7 +50,7 @@ before_install: - conda config --set always_yes yes --set changeps1 no; - conda env create -f environment.yml - source ~/miniconda/bin/activate cadquery -- conda install python=$PYTHON_VERSION +- conda install -c conda-forge -c defaults -c cadquery python=$PYTHON_VERSION install: - python setup.py install diff --git a/appveyor.yml b/appveyor.yml index 58d19f90..2aac4e12 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,7 +19,7 @@ install: - conda update -q conda - conda env create -f environment.yml - activate cadquery - - conda install python=%PYTHON_VERSION% + - conda install -c conda-forge -c defaults -c cadquery python=%PYTHON_VERSION% build: false From fc4e9218488bdb5fff90a5c328ac562ad995e3ca Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sat, 21 Dec 2019 16:29:00 +0100 Subject: [PATCH 23/70] Add files via upload --- tests/TestInterpPlate.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 tests/TestInterpPlate.py diff --git a/tests/TestInterpPlate.py b/tests/TestInterpPlate.py new file mode 100644 index 00000000..b32e9427 --- /dev/null +++ b/tests/TestInterpPlate.py @@ -0,0 +1,16 @@ +""" + Tests interpPlate functionality +""" + +import cadquery as cq + +class TestinterpPlate(BaseTest): + + def plate(self): + # example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. + thickness = 0 + edge_points = [[0.,0.,0.], [0.,10.,0.], [0.,10.,10.], [0.,0.,10.]] + surface_points = [[5.,5.,5.]] + plate_0 = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness) + + return plate_0 From 5765e24d95c207a877ad237d3bb7da0b7bbbccc6 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sat, 21 Dec 2019 16:29:34 +0100 Subject: [PATCH 24/70] Add files via upload --- examples/Ex101_InterpPlate.py | 86 +++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 examples/Ex101_InterpPlate.py diff --git a/examples/Ex101_InterpPlate.py b/examples/Ex101_InterpPlate.py new file mode 100644 index 00000000..f838f6be --- /dev/null +++ b/examples/Ex101_InterpPlate.py @@ -0,0 +1,86 @@ +import numpy as np +from numpy import sin, cos, pi, sqrt +import cadquery as cq + +# TEST_1 +# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. +thickness = 0 +edge_points = [[0.,0.,0.], [0.,10.,0.], [0.,10.,10.], [0.,0.,10.]] +surface_points = [[5.,5.,5.]] +plate_0 = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness) +plate_0 = plate_0.translate((0,6*12,0)) +show_object(plate_0) + +# EXAMPLE 1 +# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides +thickness = 0.1 +edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] +edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) +#edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(0, 45, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)])) +edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) +surface_points = [[-3.,-3.,-3.], [3.,3.,3.]] +plate_1 = cq.Workplane('XY').interpPlate(edge_wire, surface_points, thickness) +#plate_1 = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness) # list of (x,y,z) points instead of wires for edges +show_object(plate_1) + +# EXAMPLE 2 +# Embossed star, need to change optional parameters to obtain nice looking result. +r1=3. +r2=10. +fn=6 +thickness = 0.1 +edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] +edge_wire = cq.Workplane('XY').polyline(edge_points) +r2=4.5 +surface_points = [[r2*cos(i * pi/fn), r2*sin(i * pi/fn), 1.] for i in range(2*fn)] + [[0.,0.,-2.]] +plate_2 = cq.Workplane('XY').interpPlate(edge_wire, surface_points, thickness, combine=True, clean=True, Degree=3, NbPtsOnCur=15, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=49) +#plate_2 = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness, combine=True, clean=True, Degree=3, NbPtsOnCur=15, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=49) # list of (x,y,z) points instead of wires for edges +plate_2 = plate_2.translate((0,2*12,0)) +show_object(plate_2) + +# EXAMPLE 3 +# Points on hexagonal pattern coordinates, use of pushpoints. +r1 = 1. +N = 3 +ca = cos(30. * pi/180.) +sa = sin(30. * pi/180.) +# EVEN ROWS +x_p = np.arange(-N*r1, N*r1, ca*2*r1) +y_p = np.arange(-N*r1, N*r1, 3*r1) +x_p, y_p = np.meshgrid(x_p, y_p) +xy_p_even = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] +print("xy_p_even = ", xy_p_even) +# ODD ROWS +x_p = np.arange(-(N-0.5)*r1*ca, (N+1.5)*r1*ca, ca*2*r1) +y_p = np.arange(-(N-2+sa)*r1, (N+1+sa)*r1, 3*r1) +x_p, y_p = np.meshgrid(x_p, y_p) +xy_p_odd = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] +pts = xy_p_even + xy_p_odd +# Spike surface +thickness = 0.1 +fn = 6 +edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] +surface_points = [[0.25,0,0.75], [-0.25,0,0.75], [0,0.25,0.75], [0,-0.25,0.75], [0,0,2]] +edge_wire = cq.Workplane('XY').polyline(edge_points) +be = cq.Workplane('XY').interpPlate(edge_wire, surface_points, thickness, combine=True, clean=True, Degree=2, NbPtsOnCur=20, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=9) +#be = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness, combine=True, clean=True, Degree=2, NbPtsOnCur=20, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=9) # list of (x,y,z) points instead of wires for edges +# Pattern on sphere +def face(pos): # If pushpoints is used directly with interpPlate --> crash! Use with each() + return be.rotate((0,0,0),(0,0,1), 30).translate(pos).val() +plate_3 = cq.Workplane('XY').pushPoints(pts).each(face) +plate_3 = plate_3.translate((0,4*11,0)) +show_object(plate_3) + +# EXAMPLE 4 +# Gyroïd, all edges are splines on different workplanes. +thickness = 0.1 +edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] +plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] +offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] +edge_wire = cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) +for i in range(len(edge_points)-1): + edge_wire = edge_wire.add(cq.Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) +surface_points = [[0,0,0]] +plate_4 = cq.Workplane('XY').interpPlate(edge_wire, surface_points, thickness) +plate_4 = plate_4.translate((0,5*12,0)) +show_object(plate_4) From ef49b6faad204a10cfa14daced59bd0d8f8c50b4 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sat, 21 Dec 2019 16:32:40 +0100 Subject: [PATCH 25/70] Delete TestInterpPlate.py --- tests/TestInterpPlate.py | 16 ---------------- 1 file changed, 16 deletions(-) delete mode 100644 tests/TestInterpPlate.py diff --git a/tests/TestInterpPlate.py b/tests/TestInterpPlate.py deleted file mode 100644 index b32e9427..00000000 --- a/tests/TestInterpPlate.py +++ /dev/null @@ -1,16 +0,0 @@ -""" - Tests interpPlate functionality -""" - -import cadquery as cq - -class TestinterpPlate(BaseTest): - - def plate(self): - # example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. - thickness = 0 - edge_points = [[0.,0.,0.], [0.,10.,0.], [0.,10.,10.], [0.,0.,10.]] - surface_points = [[5.,5.,5.]] - plate_0 = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness) - - return plate_0 From d9893eb7f7d08ac1c81dbf1d618aed7db420d3c1 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Sat, 21 Dec 2019 16:33:04 +0100 Subject: [PATCH 26/70] Delete Ex101_InterpPlate.py --- examples/Ex101_InterpPlate.py | 86 ----------------------------------- 1 file changed, 86 deletions(-) delete mode 100644 examples/Ex101_InterpPlate.py diff --git a/examples/Ex101_InterpPlate.py b/examples/Ex101_InterpPlate.py deleted file mode 100644 index f838f6be..00000000 --- a/examples/Ex101_InterpPlate.py +++ /dev/null @@ -1,86 +0,0 @@ -import numpy as np -from numpy import sin, cos, pi, sqrt -import cadquery as cq - -# TEST_1 -# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. -thickness = 0 -edge_points = [[0.,0.,0.], [0.,10.,0.], [0.,10.,10.], [0.,0.,10.]] -surface_points = [[5.,5.,5.]] -plate_0 = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness) -plate_0 = plate_0.translate((0,6*12,0)) -show_object(plate_0) - -# EXAMPLE 1 -# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides -thickness = 0.1 -edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] -edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) -#edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(0, 45, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)])) -edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) -surface_points = [[-3.,-3.,-3.], [3.,3.,3.]] -plate_1 = cq.Workplane('XY').interpPlate(edge_wire, surface_points, thickness) -#plate_1 = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness) # list of (x,y,z) points instead of wires for edges -show_object(plate_1) - -# EXAMPLE 2 -# Embossed star, need to change optional parameters to obtain nice looking result. -r1=3. -r2=10. -fn=6 -thickness = 0.1 -edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] -edge_wire = cq.Workplane('XY').polyline(edge_points) -r2=4.5 -surface_points = [[r2*cos(i * pi/fn), r2*sin(i * pi/fn), 1.] for i in range(2*fn)] + [[0.,0.,-2.]] -plate_2 = cq.Workplane('XY').interpPlate(edge_wire, surface_points, thickness, combine=True, clean=True, Degree=3, NbPtsOnCur=15, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=49) -#plate_2 = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness, combine=True, clean=True, Degree=3, NbPtsOnCur=15, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=49) # list of (x,y,z) points instead of wires for edges -plate_2 = plate_2.translate((0,2*12,0)) -show_object(plate_2) - -# EXAMPLE 3 -# Points on hexagonal pattern coordinates, use of pushpoints. -r1 = 1. -N = 3 -ca = cos(30. * pi/180.) -sa = sin(30. * pi/180.) -# EVEN ROWS -x_p = np.arange(-N*r1, N*r1, ca*2*r1) -y_p = np.arange(-N*r1, N*r1, 3*r1) -x_p, y_p = np.meshgrid(x_p, y_p) -xy_p_even = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] -print("xy_p_even = ", xy_p_even) -# ODD ROWS -x_p = np.arange(-(N-0.5)*r1*ca, (N+1.5)*r1*ca, ca*2*r1) -y_p = np.arange(-(N-2+sa)*r1, (N+1+sa)*r1, 3*r1) -x_p, y_p = np.meshgrid(x_p, y_p) -xy_p_odd = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] -pts = xy_p_even + xy_p_odd -# Spike surface -thickness = 0.1 -fn = 6 -edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] -surface_points = [[0.25,0,0.75], [-0.25,0,0.75], [0,0.25,0.75], [0,-0.25,0.75], [0,0,2]] -edge_wire = cq.Workplane('XY').polyline(edge_points) -be = cq.Workplane('XY').interpPlate(edge_wire, surface_points, thickness, combine=True, clean=True, Degree=2, NbPtsOnCur=20, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=9) -#be = cq.Workplane('XY').interpPlate(edge_points, surface_points, thickness, combine=True, clean=True, Degree=2, NbPtsOnCur=20, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=9) # list of (x,y,z) points instead of wires for edges -# Pattern on sphere -def face(pos): # If pushpoints is used directly with interpPlate --> crash! Use with each() - return be.rotate((0,0,0),(0,0,1), 30).translate(pos).val() -plate_3 = cq.Workplane('XY').pushPoints(pts).each(face) -plate_3 = plate_3.translate((0,4*11,0)) -show_object(plate_3) - -# EXAMPLE 4 -# Gyroïd, all edges are splines on different workplanes. -thickness = 0.1 -edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] -plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] -offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] -edge_wire = cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) -for i in range(len(edge_points)-1): - edge_wire = edge_wire.add(cq.Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) -surface_points = [[0,0,0]] -plate_4 = cq.Workplane('XY').interpPlate(edge_wire, surface_points, thickness) -plate_4 = plate_4.translate((0,5*12,0)) -show_object(plate_4) From f9eeb128779e5709280482a21593ba89ab42b5be Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Mon, 23 Dec 2019 14:24:05 +0100 Subject: [PATCH 27/70] Delete TestAssembleEdgesWarnings.py --- tests/TestAssembleEdgesWarnings.py | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 tests/TestAssembleEdgesWarnings.py diff --git a/tests/TestAssembleEdgesWarnings.py b/tests/TestAssembleEdgesWarnings.py deleted file mode 100644 index e4244739..00000000 --- a/tests/TestAssembleEdgesWarnings.py +++ /dev/null @@ -1,19 +0,0 @@ -""" - Tests assembleEdges warnings functionality -""" - -import cadquery as cq - -class TestAssembleEdgesWarning(BaseTest): - - def plate(self): - - # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides - thickness = 0.1 - edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] - edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) - edge_wire = edge_wire.add(cq.Workplane('XY').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) # Triggers BRepBuilderAPI_MakeWire error ('YZ' is correct) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) - - return edge_wire From bb2eb064ef14fd45ab70e85e3eb75e358c8f48ca Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Mon, 23 Dec 2019 14:24:36 +0100 Subject: [PATCH 28/70] Add files via upload --- tests/TestAssembleEdges.py | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/TestAssembleEdges.py diff --git a/tests/TestAssembleEdges.py b/tests/TestAssembleEdges.py new file mode 100644 index 00000000..00e7e38f --- /dev/null +++ b/tests/TestAssembleEdges.py @@ -0,0 +1,78 @@ +""" + Tests interpPlate functionality +""" + +import cadquery as cq + +class TestAssembleEdges(BaseTest): + + def edge_wire_1(self): + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + thickness = 0.1 + edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] + edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) + edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + return edge_wire + + + def edge_wire_2(self): + # Embossed star, need to change optional parameters to obtain nice looking result. + r1=3. + r2=10. + fn=6 + thickness = 0.1 + edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] + edge_wire = cq.Workplane('XY').polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + return edge_wire + + + def edge_wire_3(self): + # Points on hexagonal pattern coordinates, use of pushpoints. + r1 = 1. + N = 3 + ca = cos(30. * pi/180.) + sa = sin(30. * pi/180.) + # EVEN ROWS + x_p = np.arange(-N*r1, N*r1, ca*2*r1) + y_p = np.arange(-N*r1, N*r1, 3*r1) + x_p, y_p = np.meshgrid(x_p, y_p) + xy_p_even = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] + # ODD ROWS + x_p = np.arange(-(N-0.5)*r1*ca, (N+1.5)*r1*ca, ca*2*r1) + y_p = np.arange(-(N-2+sa)*r1, (N+1+sa)*r1, 3*r1) + x_p, y_p = np.meshgrid(x_p, y_p) + xy_p_odd = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] + pts = xy_p_even + xy_p_odd + # Spike surface + thickness = 0.1 + fn = 6 + edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] + surface_points = [[0.25,0,0.75], [-0.25,0,0.75], [0,0.25,0.75], [0,-0.25,0.75], [0,0,2]] + edge_wire = cq.Workplane('XY').polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + return edge_wire + + + def edge_wire_4(self): + # Gyroïd, all edges are splines on different workplanes. + thickness = 0.1 + edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] + plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] + offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] + edge_wire = cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) + for i in range(len(edge_points)-1): + edge_wire = edge_wire.add(cq.Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + return edge_wire + + From 9b2c8da425e332706dfad80a9c00338b2c00a00f Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Mon, 23 Dec 2019 15:18:50 +0100 Subject: [PATCH 29/70] Update TestAssembleEdges.py --- tests/TestAssembleEdges.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/TestAssembleEdges.py b/tests/TestAssembleEdges.py index 00e7e38f..b1d0fac3 100644 --- a/tests/TestAssembleEdges.py +++ b/tests/TestAssembleEdges.py @@ -1,5 +1,5 @@ """ - Tests interpPlate functionality + Tests AssembleEdges functionality """ import cadquery as cq From 57c4c9924849b39e850c16785695f6e8f213835f Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Mon, 23 Dec 2019 15:22:07 +0100 Subject: [PATCH 30/70] Update TestAssembleEdges.py --- tests/TestAssembleEdges.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/TestAssembleEdges.py b/tests/TestAssembleEdges.py index b1d0fac3..838a797c 100644 --- a/tests/TestAssembleEdges.py +++ b/tests/TestAssembleEdges.py @@ -3,6 +3,7 @@ """ import cadquery as cq +from tests import BaseTest class TestAssembleEdges(BaseTest): From 84775d4973f9d6450573b98d2f58dd7e9abc097c Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Mon, 23 Dec 2019 15:25:42 +0100 Subject: [PATCH 31/70] Add files via upload --- tests/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/__init__.py b/tests/__init__.py index bae4439d..2c0ca15d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -59,4 +59,5 @@ __all__ = [ 'TestImporters', 'TestJupyter', 'TestWorkplanes', + 'TestAssembleEdges', ] From b0c9f1b40df8a8f28f144f808480b508bfd6529b Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Mon, 23 Dec 2019 15:26:07 +0100 Subject: [PATCH 32/70] Add files via upload --- runtests.py | 1 + 1 file changed, 1 insertion(+) diff --git a/runtests.py b/runtests.py index f1052ca9..e73e19ab 100644 --- a/runtests.py +++ b/runtests.py @@ -16,6 +16,7 @@ suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExp suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestImporters.TestImporters)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestJupyter.TestJupyter)) suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes)) +suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestAssembleEdges.TestAssembleEdges)) if __name__ == '__main__': result = unittest.TextTestRunner().run(suite) From e64ac5d1d195065e61773f839e81e4fbaf69dcb0 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 10:45:32 +0100 Subject: [PATCH 33/70] Delete runtests.py --- runtests.py | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 runtests.py diff --git a/runtests.py b/runtests.py deleted file mode 100644 index e73e19ab..00000000 --- a/runtests.py +++ /dev/null @@ -1,23 +0,0 @@ -import sys -from tests import * -import cadquery -import unittest - -#if you are on python 2.7, you can use -m uniitest discover. -#but this is required for python 2.6.6 on windows. FreeCAD0.12 will not load -#on py 2.7.x on win -suite = unittest.TestSuite() - -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadObjects.TestCadObjects)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCadQuery.TestCadQuery)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQGI.TestCQGI)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestCQSelectors.TestCQSelectors)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestExporters.TestExporters)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestImporters.TestImporters)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestJupyter.TestJupyter)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestWorkplanes.TestWorkplanes)) -suite.addTests(unittest.TestLoader().loadTestsFromTestCase(TestAssembleEdges.TestAssembleEdges)) - -if __name__ == '__main__': - result = unittest.TextTestRunner().run(suite) - sys.exit(not result.wasSuccessful()) From 10a72993507222febdd32c29d0b0241fd8840d6d Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 10:50:03 +0100 Subject: [PATCH 34/70] Update test_cadquery.py --- tests/test_cadquery.py | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 64b85237..c9324cc9 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2139,3 +2139,61 @@ class TestCadQuery(BaseTest): point = result.faces(">Z").edges(">X").first().val().startPoint().toTuple() self.assertTupleAlmostEquals(point, (0.707106781, 1.414213562, 1.0), decimal_places) + def test_assembleEdges(self): + """ + Tests TestAssembleEdges functionality + """ + + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + thickness = 0.1 + edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] + edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) + edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + # Embossed star, need to change optional parameters to obtain nice looking result. + r1=3. + r2=10. + fn=6 + thickness = 0.1 + edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] + edge_wire = cq.Workplane('XY').polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + # Points on hexagonal pattern coordinates, use of pushpoints. + r1 = 1. + N = 3 + ca = cos(30. * pi/180.) + sa = sin(30. * pi/180.) + # EVEN ROWS + x_p = np.arange(-N*r1, N*r1, ca*2*r1) + y_p = np.arange(-N*r1, N*r1, 3*r1) + x_p, y_p = np.meshgrid(x_p, y_p) + xy_p_even = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] + # ODD ROWS + x_p = np.arange(-(N-0.5)*r1*ca, (N+1.5)*r1*ca, ca*2*r1) + y_p = np.arange(-(N-2+sa)*r1, (N+1+sa)*r1, 3*r1) + x_p, y_p = np.meshgrid(x_p, y_p) + xy_p_odd = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] + pts = xy_p_even + xy_p_odd + # Spike surface + thickness = 0.1 + fn = 6 + edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] + surface_points = [[0.25,0,0.75], [-0.25,0,0.75], [0,0.25,0.75], [0,-0.25,0.75], [0,0,2]] + edge_wire = cq.Workplane('XY').polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + # Gyroïd, all edges are splines on different workplanes. + thickness = 0.1 + edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] + plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] + offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] + edge_wire = cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) + for i in range(len(edge_points)-1): + edge_wire = edge_wire.add(cq.Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) From cdb165807c432750bfbb2b5da99d6d372271ff95 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 11:12:57 +0100 Subject: [PATCH 35/70] Delete TestAssembleEdges.py --- tests/TestAssembleEdges.py | 79 -------------------------------------- 1 file changed, 79 deletions(-) delete mode 100644 tests/TestAssembleEdges.py diff --git a/tests/TestAssembleEdges.py b/tests/TestAssembleEdges.py deleted file mode 100644 index 838a797c..00000000 --- a/tests/TestAssembleEdges.py +++ /dev/null @@ -1,79 +0,0 @@ -""" - Tests AssembleEdges functionality -""" - -import cadquery as cq -from tests import BaseTest - -class TestAssembleEdges(BaseTest): - - def edge_wire_1(self): - # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides - thickness = 0.1 - edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] - edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) - edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) - - return edge_wire - - - def edge_wire_2(self): - # Embossed star, need to change optional parameters to obtain nice looking result. - r1=3. - r2=10. - fn=6 - thickness = 0.1 - edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] - edge_wire = cq.Workplane('XY').polyline(edge_points) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) - - return edge_wire - - - def edge_wire_3(self): - # Points on hexagonal pattern coordinates, use of pushpoints. - r1 = 1. - N = 3 - ca = cos(30. * pi/180.) - sa = sin(30. * pi/180.) - # EVEN ROWS - x_p = np.arange(-N*r1, N*r1, ca*2*r1) - y_p = np.arange(-N*r1, N*r1, 3*r1) - x_p, y_p = np.meshgrid(x_p, y_p) - xy_p_even = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] - # ODD ROWS - x_p = np.arange(-(N-0.5)*r1*ca, (N+1.5)*r1*ca, ca*2*r1) - y_p = np.arange(-(N-2+sa)*r1, (N+1+sa)*r1, 3*r1) - x_p, y_p = np.meshgrid(x_p, y_p) - xy_p_odd = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] - pts = xy_p_even + xy_p_odd - # Spike surface - thickness = 0.1 - fn = 6 - edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] - surface_points = [[0.25,0,0.75], [-0.25,0,0.75], [0,0.25,0.75], [0,-0.25,0.75], [0,0,2]] - edge_wire = cq.Workplane('XY').polyline(edge_points) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) - - return edge_wire - - - def edge_wire_4(self): - # Gyroïd, all edges are splines on different workplanes. - thickness = 0.1 - edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] - plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] - offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] - edge_wire = cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) - for i in range(len(edge_points)-1): - edge_wire = edge_wire.add(cq.Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) - - return edge_wire - - From 38c3d27bb4900482d8e7b68c893548d87e30fea5 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 11:14:05 +0100 Subject: [PATCH 36/70] Update test_cadquery.py --- tests/test_cadquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index c9324cc9..f56ecd76 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2141,7 +2141,7 @@ class TestCadQuery(BaseTest): def test_assembleEdges(self): """ - Tests TestAssembleEdges functionality + Tests TestAssembleEdges functionality """ # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides From be3ad421a6e1477d30473f2d57754983f2916d0b Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 11:14:20 +0100 Subject: [PATCH 37/70] Update test_cadquery.py From 81b3f57f8db758b5a4f4d77ae9506bfbfac52efe Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 11:16:02 +0100 Subject: [PATCH 38/70] Update test_cadquery.py --- tests/test_cadquery.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index f56ecd76..a100e0cf 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2164,25 +2164,8 @@ class TestCadQuery(BaseTest): # Points on hexagonal pattern coordinates, use of pushpoints. r1 = 1. - N = 3 - ca = cos(30. * pi/180.) - sa = sin(30. * pi/180.) - # EVEN ROWS - x_p = np.arange(-N*r1, N*r1, ca*2*r1) - y_p = np.arange(-N*r1, N*r1, 3*r1) - x_p, y_p = np.meshgrid(x_p, y_p) - xy_p_even = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] - # ODD ROWS - x_p = np.arange(-(N-0.5)*r1*ca, (N+1.5)*r1*ca, ca*2*r1) - y_p = np.arange(-(N-2+sa)*r1, (N+1+sa)*r1, 3*r1) - x_p, y_p = np.meshgrid(x_p, y_p) - xy_p_odd = [(x,y) for x,y in zip(x_p.flatten(), y_p.flatten())] - pts = xy_p_even + xy_p_odd - # Spike surface - thickness = 0.1 fn = 6 edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] - surface_points = [[0.25,0,0.75], [-0.25,0,0.75], [0,0.25,0.75], [0,-0.25,0.75], [0,0,2]] edge_wire = cq.Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = cq.Wire.assembleEdges(edge_wire) From 707dac5bc426ce0d726bf4b867c3fb0b45033ecb Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 11:39:58 +0100 Subject: [PATCH 39/70] Update test_cadquery.py --- tests/test_cadquery.py | 75 ++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 39 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index a100e0cf..a563f813 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2140,43 +2140,40 @@ class TestCadQuery(BaseTest): self.assertTupleAlmostEquals(point, (0.707106781, 1.414213562, 1.0), decimal_places) def test_assembleEdges(self): - """ - Tests TestAssembleEdges functionality - """ - # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides - thickness = 0.1 - edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] - edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) - edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) - - # Embossed star, need to change optional parameters to obtain nice looking result. - r1=3. - r2=10. - fn=6 - thickness = 0.1 - edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] - edge_wire = cq.Workplane('XY').polyline(edge_points) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) - - # Points on hexagonal pattern coordinates, use of pushpoints. - r1 = 1. - fn = 6 - edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] - edge_wire = cq.Workplane('XY').polyline(edge_points) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) - - # Gyroïd, all edges are splines on different workplanes. - thickness = 0.1 - edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] - plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] - offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] - edge_wire = cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) - for i in range(len(edge_points)-1): - edge_wire = edge_wire.add(cq.Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + thickness = 0.1 + edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] + edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) + edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + # Embossed star, need to change optional parameters to obtain nice looking result. + r1=3. + r2=10. + fn=6 + thickness = 0.1 + edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] + edge_wire = cq.Workplane('XY').polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + # Points on hexagonal pattern coordinates, use of pushpoints. + r1 = 1. + fn = 6 + edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] + edge_wire = cq.Workplane('XY').polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) + + # Gyroïd, all edges are splines on different workplanes. + thickness = 0.1 + edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] + plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] + offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] + edge_wire = cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) + for i in range(len(edge_points)-1): + edge_wire = edge_wire.add(cq.Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = cq.Wire.assembleEdges(edge_wire) From 3ab5b512912793369acc54500cf3adf601bedef7 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 11:52:00 +0100 Subject: [PATCH 40/70] Update test_cadquery.py --- tests/test_cadquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index a563f813..039e7e2a 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2140,7 +2140,7 @@ class TestCadQuery(BaseTest): self.assertTupleAlmostEquals(point, (0.707106781, 1.414213562, 1.0), decimal_places) def test_assembleEdges(self): - + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides thickness = 0.1 edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] From a26766af50c5eb9db94af4f87733562d4944fc71 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 12:00:09 +0100 Subject: [PATCH 41/70] Update test_cadquery.py --- tests/test_cadquery.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 039e7e2a..f124e169 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2142,38 +2142,35 @@ class TestCadQuery(BaseTest): def test_assembleEdges(self): # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides - thickness = 0.1 edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] - edge_wire = cq.Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) - edge_wire = edge_wire.add(cq.Workplane('YZ').workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) + edge_wire = Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) + edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) + edge_wire = Wire.assembleEdges(edge_wire) # Embossed star, need to change optional parameters to obtain nice looking result. r1=3. r2=10. fn=6 - thickness = 0.1 edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] - edge_wire = cq.Workplane('XY').polyline(edge_points) + edge_wire = Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) + edge_wire = Wire.assembleEdges(edge_wire) # Points on hexagonal pattern coordinates, use of pushpoints. r1 = 1. fn = 6 edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] - edge_wire = cq.Workplane('XY').polyline(edge_points) + edge_wire = Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) + edge_wire = Wire.assembleEdges(edge_wire) # Gyroïd, all edges are splines on different workplanes. - thickness = 0.1 edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] - edge_wire = cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) + edge_wire = Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) for i in range(len(edge_points)-1): - edge_wire = edge_wire.add(cq.Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) + edge_wire = edge_wire.add(Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = cq.Wire.assembleEdges(edge_wire) + edge_wire = Wire.assembleEdges(edge_wire) From 428a8d58a11e12250d615dbe825bb37f939ad729 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 12:32:39 +0100 Subject: [PATCH 42/70] Update test_cadquery.py --- tests/test_cadquery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index f124e169..fe1cfe3c 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2152,7 +2152,7 @@ class TestCadQuery(BaseTest): r1=3. r2=10. fn=6 - edge_points = [[r1*cos(i * pi/fn), r1*sin(i * pi/fn)] if i%2==0 else [r2*cos(i * pi/fn), r2*sin(i * pi/fn)] for i in range(2*fn+1)] + edge_points = [[r1*math.cos(i * pi/fn), r1*math.sin(i * pi/fn)] if i%2==0 else [r2*math.cos(i * pi/fn), r2*math.sin(i * pi/fn)] for i in range(2*fn+1)] edge_wire = Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) @@ -2160,7 +2160,7 @@ class TestCadQuery(BaseTest): # Points on hexagonal pattern coordinates, use of pushpoints. r1 = 1. fn = 6 - edge_points = [[r1*cos(i * 2*pi/fn), r1*sin(i * 2*pi/fn)] for i in range(fn+1)] + edge_points = [[r1*math.cos(i * 2*pi/fn), r1*math.sin(i * 2*pi/fn)] for i in range(fn+1)] edge_wire = Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) From d2c029cbeb3e581de4ddb8d9fa4673e6f4127edf Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 12:49:37 +0100 Subject: [PATCH 43/70] Update test_cadquery.py --- tests/test_cadquery.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index fe1cfe3c..5956457d 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2152,7 +2152,7 @@ class TestCadQuery(BaseTest): r1=3. r2=10. fn=6 - edge_points = [[r1*math.cos(i * pi/fn), r1*math.sin(i * pi/fn)] if i%2==0 else [r2*math.cos(i * pi/fn), r2*math.sin(i * pi/fn)] for i in range(2*fn+1)] + edge_points = [[r1*math.cos(i * math.pi/fn), r1*math.sin(i * math.pi/fn)] if i%2==0 else [r2*math.cos(i * math.pi/fn), r2*math.sin(i * math.pi/fn)] for i in range(2*fn+1)] edge_wire = Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) @@ -2160,7 +2160,7 @@ class TestCadQuery(BaseTest): # Points on hexagonal pattern coordinates, use of pushpoints. r1 = 1. fn = 6 - edge_points = [[r1*math.cos(i * 2*pi/fn), r1*math.sin(i * 2*pi/fn)] for i in range(fn+1)] + edge_points = [[r1*math.cos(i * 2*math.pi/fn), r1*math.sin(i * 2*math.pi/fn)] for i in range(fn+1)] edge_wire = Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) From 6196729384b8ce88f05eb6b2342e4aa9844af845 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 16:31:19 +0100 Subject: [PATCH 44/70] Update shapes.py --- cadquery/occ_impl/shapes.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 946c8843..cbba68d7 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -812,19 +812,24 @@ class Wire(Shape, Mixin1D): :param cls: :param listOfEdges: a list of Edge objects. The edges are not to be consecutive. :return: a wire with the edges assembled + :BRepBuilderAPI_MakeWire::Error() values + :BRepBuilderAPI_WireDone = 0 + :BRepBuilderAPI_EmptyWire = 1 + :BRepBuilderAPI_DisconnectedWire = 2 + :BRepBuilderAPI_NonManifoldWire = 3 """ - wire_builder = BRepBuilderAPI_MakeWire() + edges_list = TopTools_ListOfShape() for e in listOfEdges: edges_list.Append(e.wrapped) wire_builder.Add(edges_list) - if wire_builder.Error(): - w1 = 'BRepBuilderAPI_MakeWire::IsDone(): returns true if this algorithm contains a valid wire. IsDone returns false if: there are no edges in the wire, or the last edge which you tried to add was not connectable = '+ str(wire_builder.IsDone()) - w2 = 'BRepBuilderAPI_MakeWire::Error(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = ' + str(wire_builder.Error()) - warnings.warn(w1) - warnings.warn(w2) - + if wire_builder.Error()!=0: + w1 = 'BRepBuilderAPI_MakeWire::IsDone(): returns true if this algorithm contains a valid wire. IsDone returns false if: there are no edges in the wire, or the last edge which you tried to add was not connectable = '+ str(wire_builder.IsDone()) + w2 = 'BRepBuilderAPI_MakeWire::Error(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = ' + str(wire_builder.Error()) + warnings.warn(w1) + warnings.warn(w2) + return cls(wire_builder.Wire()) @classmethod From 1d7b11baf9b8c60a7e4d17b1ea3dfdbb591ecd8b Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Tue, 24 Dec 2019 16:32:38 +0100 Subject: [PATCH 45/70] Add files via upload --- tests/test_cadquery.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 5956457d..b1b852b7 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2140,14 +2140,14 @@ class TestCadQuery(BaseTest): self.assertTupleAlmostEquals(point, (0.707106781, 1.414213562, 1.0), decimal_places) def test_assembleEdges(self): - + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] edge_wire = Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) - edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) + edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) - + # Embossed star, need to change optional parameters to obtain nice looking result. r1=3. r2=10. @@ -2156,15 +2156,16 @@ class TestCadQuery(BaseTest): edge_wire = Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) - + # Points on hexagonal pattern coordinates, use of pushpoints. r1 = 1. fn = 6 edge_points = [[r1*math.cos(i * 2*math.pi/fn), r1*math.sin(i * 2*math.pi/fn)] for i in range(fn+1)] + surface_points = [[0.25,0,0.75], [-0.25,0,0.75], [0,0.25,0.75], [0,-0.25,0.75], [0,0,2]] edge_wire = Workplane('XY').polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) - + # Gyroïd, all edges are splines on different workplanes. edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] From 42c35b70892d7c30d10b15ea141f3923fd9f7dd1 Mon Sep 17 00:00:00 2001 From: Bruno Agostini Date: Wed, 25 Dec 2019 09:21:15 +0100 Subject: [PATCH 46/70] Update test_cadquery.py --- tests/test_cadquery.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index b1b852b7..01562c6e 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2142,6 +2142,7 @@ class TestCadQuery(BaseTest): def test_assembleEdges(self): # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + # Passes an open wire to assembleEdges so that IsDone is true but Error returns 2 to test the warning functionality. edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] edge_wire = Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) From 102c16c14e42b6acca484efbf017f9ac2cc75789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20S=C3=A1nchez=20de=20Le=C3=B3n=20Peque?= Date: Mon, 20 Jan 2020 20:52:12 +0100 Subject: [PATCH 47/70] Add Black formatting check to CI (#255) * Add Black formatting check to CI * Add some documentation for code contributors * Use uncompromised code formatting --- .travis.yml | 28 +- README.md | 32 + cadquery/__init__.py | 58 +- cadquery/cq.py | 399 +++-- cadquery/cq_directive.py | 44 +- cadquery/cqgi.py | 142 +- cadquery/occ_impl/exporters.py | 88 +- cadquery/occ_impl/geom.py | 264 +-- cadquery/occ_impl/importers.py | 4 +- cadquery/occ_impl/jupyter_tools.py | 130 +- cadquery/occ_impl/shapes.py | 809 +++++---- cadquery/selectors.py | 185 +- doc/conf.py | 146 +- environment.yml | 1 + examples/Ex001_Simple_Block.py | 6 +- .../Ex002_Block_With_Bored_Center_Hole.py | 17 +- ...03_Pillow_Block_With_Counterbored_Holes.py | 36 +- examples/Ex004_Extruded_Cylindrical_Plate.py | 17 +- examples/Ex005_Extruded_Lines_and_Arcs.py | 20 +- .../Ex006_Moving_the_Current_Working_Point.py | 4 +- examples/Ex007_Using_Point_Lists.py | 4 +- examples/Ex008_Polygon_Creation.py | 21 +- examples/Ex009_Polylines.py | 19 +- .../Ex010_Defining_an_Edge_with_a_Spline.py | 2 +- examples/Ex015_Rotated_Workplanes.py | 14 +- examples/Ex016_Using_Construction_Geometry.py | 13 +- examples/Ex018_Making_Lofts.py | 13 +- examples/Ex019_Counter_Sunk_Holes.py | 12 +- examples/Ex021_Splitting_an_Object.py | 3 +- examples/Ex022_Revolution.py | 10 +- examples/Ex023_Sweep.py | 8 +- .../Ex024_Sweep_With_Multiple_Sections.py | 81 +- examples/Ex100_Lego_Brick.py | 40 +- setup.py | 65 +- tests/__init__.py | 32 +- tests/test_cad_objects.py | 153 +- tests/test_cadquery.py | 1568 +++++++++++------ tests/test_cqgi.py | 35 +- tests/test_exporters.py | 20 +- tests/test_importers.py | 25 +- tests/test_jupyter.py | 3 +- tests/test_selectors.py | 210 +-- tests/test_workplanes.py | 79 +- 43 files changed, 2968 insertions(+), 1892 deletions(-) diff --git a/.travis.yml b/.travis.yml index 247371cc..368c9f2d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,27 +24,31 @@ env: matrix: include: - - env: PYTHON_VERSION=3.6 + - name: "Python 3.6 - osx" + env: PYTHON_VERSION=3.6 os: osx - - env: PYTHON_VERSION=3.6 + - name: "Python 3.6 - linux" + env: PYTHON_VERSION=3.6 os: linux - - env: PYTHON_VERSION=3.7 + - name: "Python 3.7 - osx" + env: PYTHON_VERSION=3.7 os: osx - - env: PYTHON_VERSION=3.7 + - name: "Python 3.7 - linux" + env: PYTHON_VERSION=3.7 os: linux + - name: "Lint" + env: PYTHON_VERSION=3.7 + os: linux + script: + - black . --diff --check before_install: -- if [[ "$PYTHON_VERSION" == "2.7" ]]; then - PY_MAJOR=2 ; - else - PY_MAJOR=3 ; - fi ; - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then +- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then OS=Linux ; else OS=MacOSX ; fi ; - wget https://repo.continuum.io/miniconda/Miniconda$PY_MAJOR-latest-$OS-x86_64.sh -O miniconda.sh + wget https://repo.anaconda.com/miniconda/Miniconda3-latest-$OS-x86_64.sh -O miniconda.sh - bash miniconda.sh -b -p $HOME/miniconda; - export PATH="$HOME/miniconda/bin:$HOME/miniconda/lib:$PATH"; - conda config --set always_yes yes --set changeps1 no; @@ -67,4 +71,4 @@ after_success: after_failure: - ls /cores/core.* -- lldb --core `ls /cores/core.*` --batch --one-line "bt" \ No newline at end of file +- lldb --core `ls /cores/core.*` --batch --one-line "bt" diff --git a/README.md b/README.md index e78e5203..2cd7aeb1 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,38 @@ You do not need to be a software developer to have a big impact on this project. It is asked that all contributions to this project be made in a respectful and considerate way. Please use the [Python Community Code of Conduct's](https://www.python.org/psf/codeofconduct/) guidelines as a reference. +### Contributing code + +If you are going to contribute code, make sure to follow this steps: + +- Consider opening an issue first to discuss what you have in mind +- Try to keep it as short and simple as possible (if you want to change several + things, start with just one!) +- Fork the CadQuery repository, clone your fork and create a new branch to + start working on your changes +- Start with the tests! How should CadQuery behave after your changes? Make + sure to add some tests to the test suite to ensure proper behavior +- Make sure your tests have assertions checking all the expected results +- Add a nice docstring to the test indicating what the test is doing; if there + is too much to explain, consider splitting the test in two! +- Go ahead and implement the changes +- Add a nice docstring to the functions/methods/classes you implement + describing what they do, what the expected parameters are and what it returns + (if anything) +- Update the documentation if there is any change to the public API +- Consider adding an example to the documentation showing your cool new + feature! +- Make sure nothing is broken (run the complete test suite with `pytest`) +- Run `black` to autoformat your code and make sure your code style complies + with CadQuery's +- Push the changes to your fork and open a pull-request upstream +- Keep an eye on the automated feedback you will receive from the CI pipelines; + if there is a test failing or some code is not properly formatted, you will + be notified without human intervention +- Be prepared for constructive feedback and criticism! +- Be patient and respectful, remember that those reviewing your code are also + working hard (sometimes reviewing changes is harder than implementing them!) + ### How to Report a Bug When filing a bug report [issue](https://github.com/CadQuery/cadquery/issues), please be sure to answer these questions: diff --git a/cadquery/__init__.py b/cadquery/__init__.py index 57e4148a..652ec5e6 100644 --- a/cadquery/__init__.py +++ b/cadquery/__init__.py @@ -1,25 +1,65 @@ # these items point to the OCC implementation from .occ_impl.geom import Plane, BoundBox, Vector, Matrix -from .occ_impl.shapes import (Shape, Vertex, Edge, Face, Wire, Solid, Shell, - Compound, sortWiresByBuildOrder) +from .occ_impl.shapes import ( + Shape, + Vertex, + Edge, + Face, + Wire, + Solid, + Shell, + Compound, + sortWiresByBuildOrder, +) from .occ_impl import exporters from .occ_impl import importers # these items are the common implementation # the order of these matter -from .selectors import (NearestToPointSelector, ParallelDirSelector, - DirectionSelector, PerpendicularDirSelector, TypeSelector, - DirectionMinMaxSelector, StringSyntaxSelector, Selector) +from .selectors import ( + NearestToPointSelector, + ParallelDirSelector, + DirectionSelector, + PerpendicularDirSelector, + TypeSelector, + DirectionMinMaxSelector, + StringSyntaxSelector, + Selector, +) from .cq import CQ, Workplane, selectors from . import plugins __all__ = [ - 'CQ', 'Workplane', 'plugins', 'selectors', 'Plane', 'BoundBox', 'Matrix', 'Vector', 'sortWiresByBuildOrder', - 'Shape', 'Vertex', 'Edge', 'Wire', 'Face', 'Solid', 'Shell', 'Compound', 'exporters', 'importers', - 'NearestToPointSelector', 'ParallelDirSelector', 'DirectionSelector', 'PerpendicularDirSelector', - 'TypeSelector', 'DirectionMinMaxSelector', 'StringSyntaxSelector', 'Selector', 'plugins' + "CQ", + "Workplane", + "plugins", + "selectors", + "Plane", + "BoundBox", + "Matrix", + "Vector", + "sortWiresByBuildOrder", + "Shape", + "Vertex", + "Edge", + "Wire", + "Face", + "Solid", + "Shell", + "Compound", + "exporters", + "importers", + "NearestToPointSelector", + "ParallelDirSelector", + "DirectionSelector", + "PerpendicularDirSelector", + "TypeSelector", + "DirectionMinMaxSelector", + "StringSyntaxSelector", + "Selector", + "plugins", ] __version__ = "2.0.0dev" diff --git a/cadquery/cq.py b/cadquery/cq.py index 23bb30e4..61496f2f 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -18,8 +18,19 @@ """ import math -from . import Vector, Plane, Shape, Edge, Wire, Face, Solid, Compound, \ - sortWiresByBuildOrder, selectors, exporters +from . import ( + Vector, + Plane, + Shape, + Edge, + Wire, + Face, + Solid, + Compound, + sortWiresByBuildOrder, + selectors, + exporters, +) class CQContext(object): @@ -31,7 +42,9 @@ class CQContext(object): """ def __init__(self): - self.pendingWires = [] # a list of wires that have been created and need to be extruded + self.pendingWires = ( + [] + ) # a list of wires that have been created and need to be extruded # a list of created pending edges that need to be joined into wires self.pendingEdges = [] # a reference to the first point for a set of edges. @@ -99,8 +112,12 @@ class CQ(object): # tricky-- if an object is a compound of solids, # do not return all of the solids underneath-- typically # then we'll keep joining to ourself - if propName == 'Solids' and isinstance(o, Solid) and o.ShapeType() == 'Compound': - for i in getattr(o, 'Compounds')(): + if ( + propName == "Solids" + and isinstance(o, Solid) + and o.ShapeType() == "Compound" + ): + for i in getattr(o, "Compounds")(): all[i.hashCode()] = i else: if hasattr(o, propName): @@ -259,8 +276,9 @@ class CQ(object): return self.objects[0].wrapped - def workplane(self, offset=0.0, invert=False, centerOption='CenterOfMass', - origin=None): + def workplane( + self, offset=0.0, invert=False, centerOption="CenterOfMass", origin=None + ): """ Creates a new 2-D workplane, located relative to the first face on the stack. @@ -310,6 +328,7 @@ class CQ(object): For now you can work around by creating a workplane and then offsetting the center afterwards. """ + def _isCoPlanar(f0, f1): """Test if two faces are on the same plane.""" p0 = f0.Center() @@ -318,9 +337,11 @@ class CQ(object): n1 = f1.normalAt() # test normals (direction of planes) - if not ((abs(n0.x - n1.x) < self.ctx.tolerance) or - (abs(n0.y - n1.y) < self.ctx.tolerance) or - (abs(n0.z - n1.z) < self.ctx.tolerance)): + if not ( + (abs(n0.x - n1.x) < self.ctx.tolerance) + or (abs(n0.y - n1.y) < self.ctx.tolerance) + or (abs(n0.z - n1.z) < self.ctx.tolerance) + ): return False # test if p1 is on the plane of f0 (offset of planes) @@ -339,22 +360,23 @@ class CQ(object): xd = Vector(1, 0, 0) return xd - if centerOption not in {'CenterOfMass', 'ProjectedOrigin', 'CenterOfBoundBox'}: - raise ValueError('Undefined centerOption value provided.') + if centerOption not in {"CenterOfMass", "ProjectedOrigin", "CenterOfBoundBox"}: + raise ValueError("Undefined centerOption value provided.") if len(self.objects) > 1: # are all objects 'PLANE'? - if not all(o.geomType() in ('PLANE', 'CIRCLE') for o in self.objects): + if not all(o.geomType() in ("PLANE", "CIRCLE") for o in self.objects): raise ValueError( - "If multiple objects selected, they all must be planar faces.") + "If multiple objects selected, they all must be planar faces." + ) # are all faces co-planar with each other? if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]): raise ValueError("Selected faces must be co-planar.") - if centerOption in {'CenterOfMass', 'ProjectedOrigin'}: + if centerOption in {"CenterOfMass", "ProjectedOrigin"}: center = Shape.CombinedCenter(self.objects) - elif centerOption == 'CenterOfBoundBox': + elif centerOption == "CenterOfBoundBox": center = Shape.CombinedCenterOfBoundBox(self.objects) normal = self.objects[0].normalAt() @@ -364,26 +386,27 @@ class CQ(object): obj = self.objects[0] if isinstance(obj, Face): - if centerOption in {'CenterOfMass', 'ProjectedOrigin'}: + if centerOption in {"CenterOfMass", "ProjectedOrigin"}: center = obj.Center() - elif centerOption == 'CenterOfBoundBox': + elif centerOption == "CenterOfBoundBox": center = obj.CenterOfBoundBox() normal = obj.normalAt(center) xDir = _computeXdir(normal) else: - if hasattr(obj, 'Center'): - if centerOption in {'CenterOfMass', 'ProjectedOrigin'}: + if hasattr(obj, "Center"): + if centerOption in {"CenterOfMass", "ProjectedOrigin"}: center = obj.Center() - elif centerOption == 'CenterOfBoundBox': + elif centerOption == "CenterOfBoundBox": center = obj.CenterOfBoundBox() normal = self.plane.zDir xDir = self.plane.xDir else: raise ValueError( - "Needs a face or a vertex or point on a work plane") + "Needs a face or a vertex or point on a work plane" + ) # update center to projected origin if desired - if centerOption == 'ProjectedOrigin': + if centerOption == "ProjectedOrigin": if origin is None: origin = self.plane.origin elif isinstance(origin, tuple): @@ -459,9 +482,7 @@ class CQ(object): return rv[0] if searchParents and self.parent is not None: - return self.parent._findType(types, - searchStack=True, - searchParents=True) + return self.parent._findType(types, searchStack=True, searchParents=True) return None @@ -554,7 +575,7 @@ class CQ(object): :py:class:`StringSyntaxSelector` """ - return self._selectObjects('Vertices', selector) + return self._selectObjects("Vertices", selector) def faces(self, selector=None): """ @@ -586,7 +607,7 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Faces', selector) + return self._selectObjects("Faces", selector) def edges(self, selector=None): """ @@ -617,7 +638,7 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Edges', selector) + return self._selectObjects("Edges", selector) def wires(self, selector=None): """ @@ -640,7 +661,7 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Wires', selector) + return self._selectObjects("Wires", selector) def solids(self, selector=None): """ @@ -666,7 +687,7 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Solids', selector) + return self._selectObjects("Solids", selector) def shells(self, selector=None): """ @@ -686,7 +707,7 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Shells', selector) + return self._selectObjects("Shells", selector) def compounds(self, selector=None): """ @@ -704,7 +725,7 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects('Compounds', selector) + return self._selectObjects("Compounds", selector) def toSvg(self, opts=None): """ @@ -777,8 +798,9 @@ class CQ(object): :type angleDegrees: float :returns: a CQ object """ - return self.newObject([o.rotate(axisStartPoint, axisEndPoint, angleDegrees) - for o in self.objects]) + return self.newObject( + [o.rotate(axisStartPoint, axisEndPoint, angleDegrees) for o in self.objects] + ) def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)): """ @@ -789,8 +811,7 @@ class CQ(object): :param basePointVector: the base point to mirror about :type basePointVector: tuple """ - newS = self.newObject( - [self.objects[0].mirror(mirrorPlane, basePointVector)]) + newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)]) return newS.first() def translate(self, vec): @@ -943,7 +964,7 @@ class Workplane(CQ): :py:meth:`CQ.workplane` """ - FOR_CONSTRUCTION = 'ForConstruction' + FOR_CONSTRUCTION = "ForConstruction" def __init__(self, inPlane, origin=(0, 0, 0), obj=None): """ @@ -967,7 +988,7 @@ class Workplane(CQ): the *current point* is on the origin. """ - if inPlane.__class__.__name__ == 'Plane': + if inPlane.__class__.__name__ == "Plane": tmpPlane = inPlane elif isinstance(inPlane, str) or isinstance(inPlane, str): tmpPlane = Plane.named(inPlane, origin) @@ -976,7 +997,8 @@ class Workplane(CQ): if tmpPlane is None: raise ValueError( - 'Provided value {} is not a valid work plane'.format(inPlane)) + "Provided value {} is not a valid work plane".format(inPlane) + ) self.obj = obj self.plane = tmpPlane @@ -999,10 +1021,10 @@ class Workplane(CQ): """ # old api accepted a vector, so we'll check for that. - if rotate.__class__.__name__ == 'Vector': + if rotate.__class__.__name__ == "Vector": rotate = rotate.toTuple() - if offset.__class__.__name__ == 'Vector': + if offset.__class__.__name__ == "Vector": offset = offset.toTuple() p = self.plane.rotated(rotate) @@ -1060,8 +1082,7 @@ class Workplane(CQ): elif isinstance(obj, Vector): p = obj else: - raise RuntimeError( - "Cannot convert object type '%s' to vector " % type(obj)) + raise RuntimeError("Cannot convert object type '%s' to vector " % type(obj)) if useLocalCoords: return self.plane.toLocalCoords(p) @@ -1367,28 +1388,35 @@ class Workplane(CQ): :return: A CQ object representing a slot """ - radius = diameter/2 + radius = diameter / 2 - p1 = pnt + Vector((-length/2) + radius, diameter/2) + p1 = pnt + Vector((-length / 2) + radius, diameter / 2) p2 = p1 + Vector(length - diameter, 0) p3 = p1 + Vector(length - diameter, -diameter) p4 = p1 + Vector(0, -diameter) arc1 = p2 + Vector(radius, -radius) arc2 = p4 + Vector(-radius, radius) - edges=[(Edge.makeLine(p1,p2))] + edges = [(Edge.makeLine(p1, p2))] edges.append(Edge.makeThreePointArc(p2, arc1, p3)) edges.append(Edge.makeLine(p3, p4)) edges.append(Edge.makeThreePointArc(p4, arc2, p1)) slot = Wire.assembleEdges(edges) - return slot.rotate(pnt, pnt + Vector(0,0,1), angle) + return slot.rotate(pnt, pnt + Vector(0, 0, 1), angle) return self.eachpoint(_makeslot, True) - def spline(self, listOfXYTuple, tangents=None, periodic=False, - forConstruction=False, includeCurrent=False, makeWire=False): + def spline( + self, + listOfXYTuple, + tangents=None, + periodic=False, + forConstruction=False, + includeCurrent=False, + makeWire=False, + ): """ Create a spline interpolated through the provided points. @@ -1435,8 +1463,7 @@ class Workplane(CQ): if tangents: t1, t2 = tangents - tangents = (self.plane.toWorldCoords(t1), - self.plane.toWorldCoords(t2)) + tangents = (self.plane.toWorldCoords(t1), self.plane.toWorldCoords(t2)) e = Edge.makeSpline(allPoints, tangents=tangents, periodic=periodic) @@ -1464,9 +1491,9 @@ class Workplane(CQ): """ - allPoints = [func(start+stop*t/N) for t in range(N+1)] + allPoints = [func(start + stop * t / N) for t in range(N + 1)] - return self.spline(allPoints,includeCurrent=False,makeWire=True) + return self.spline(allPoints, includeCurrent=False, makeWire=True) def threePointArc(self, point1, point2, forConstruction=False): """ @@ -1516,10 +1543,16 @@ class Workplane(CQ): midPoint = endPoint.add(startPoint).multiply(0.5) sagVector = endPoint.sub(startPoint).normalized().multiply(abs(sag)) - if(sag > 0): - sagVector.x, sagVector.y = -sagVector.y, sagVector.x # Rotate sagVector +90 deg + if sag > 0: + sagVector.x, sagVector.y = ( + -sagVector.y, + sagVector.x, + ) # Rotate sagVector +90 deg else: - sagVector.x, sagVector.y = sagVector.y, -sagVector.x # Rotate sagVector -90 deg + sagVector.x, sagVector.y = ( + sagVector.y, + -sagVector.x, + ) # Rotate sagVector -90 deg sagPoint = midPoint.add(sagVector) @@ -1545,7 +1578,7 @@ class Workplane(CQ): # Calculate the sagitta from the radius length = endPoint.sub(startPoint).Length / 2.0 try: - sag = abs(radius) - math.sqrt(radius**2 - length**2) + sag = abs(radius) - math.sqrt(radius ** 2 - length ** 2) except ValueError: raise ValueError("Arc radius is not large enough to reach the end point.") @@ -1580,8 +1613,7 @@ class Workplane(CQ): # attempt to consolidate wires together. consolidated = n.consolidateWires() - rotatedWires = self.plane.rotateShapes( - consolidated.wires().vals(), matrix) + rotatedWires = self.plane.rotateShapes(consolidated.wires().vals(), matrix) for w in rotatedWires: consolidated.objects.append(w) @@ -1616,8 +1648,7 @@ class Workplane(CQ): # attempt to consolidate wires together. consolidated = n.consolidateWires() - mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), - 'Y') + mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), "Y") for w in mirroredWires: consolidated.objects.append(w) @@ -1646,8 +1677,7 @@ class Workplane(CQ): # attempt to consolidate wires together. consolidated = n.consolidateWires() - mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), - 'X') + mirroredWires = self.plane.mirrorInPlane(consolidated.wires().vals(), "X") for w in mirroredWires: consolidated.objects.append(w) @@ -1859,6 +1889,7 @@ class Workplane(CQ): better way to handle forConstruction project points not in the workplane plane onto the workplane plane """ + def makeRectangleWire(pnt): # Here pnt is in local coordinates due to useLocalCoords=True # (xc,yc,zc) = pnt.toTuple() @@ -1909,6 +1940,7 @@ class Workplane(CQ): project points not in the workplane plane onto the workplane plane """ + def makeCircleWire(obj): cir = Wire.makeCircle(radius, obj, Vector(0, 0, 1)) cir.forConstruction = forConstruction @@ -1927,19 +1959,25 @@ class Workplane(CQ): :param diameter: the size of the circle the polygon is inscribed into :return: a polygon wire """ + def _makePolygon(center): # pnt is a vector in local coordinates angle = 2.0 * math.pi / nSides pnts = [] for i in range(nSides + 1): - pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)), - (diameter / 2.0 * math.sin(angle * i)), 0)) + pnts.append( + center + + Vector( + (diameter / 2.0 * math.cos(angle * i)), + (diameter / 2.0 * math.sin(angle * i)), + 0, + ) + ) return Wire.makePolygon(pnts, forConstruction) return self.eachpoint(_makePolygon, True) - def polyline(self, listOfXYTuple, forConstruction=False, - includeCurrent=False): + def polyline(self, listOfXYTuple, forConstruction=False, includeCurrent=False): """ Create a polyline from a list of points @@ -2089,11 +2127,11 @@ class Workplane(CQ): boreDir = Vector(0, 0, -1) # first make the hole hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coordianates! + diameter / 2.0, depth, center, boreDir + ) # local coordianates! # add the counter bore - cbore = Solid.makeCylinder( - cboreDiameter / 2.0, cboreDepth, center, boreDir) + cbore = Solid.makeCylinder(cboreDiameter / 2.0, cboreDepth, center, boreDir) r = hole.fuse(cbore) return r @@ -2142,7 +2180,8 @@ class Workplane(CQ): # first make the hole hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coords! + diameter / 2.0, depth, center, boreDir + ) # local coords! r = cskDiameter / 2.0 h = r / math.tan(math.radians(cskAngle / 2.0)) csk = Solid.makeCone(r, 0.0, h, center, boreDir) @@ -2191,7 +2230,8 @@ class Workplane(CQ): boreDir = Vector(0, 0, -1) # first make the hole hole = Solid.makeCylinder( - diameter / 2.0, depth, center, boreDir) # local coordinates! + diameter / 2.0, depth, center, boreDir + ) # local coordinates! return hole return self.cutEach(_makeHole, True, clean) @@ -2235,8 +2275,9 @@ class Workplane(CQ): # are multiple sets r = None for ws in wireSets: - thisObj = Solid.extrudeLinearWithRotation(ws[0], ws[1:], self.plane.origin, - eDir, angleDegrees) + thisObj = Solid.extrudeLinearWithRotation( + ws[0], ws[1:], self.plane.origin, eDir, angleDegrees + ) if r is None: r = thisObj else: @@ -2277,7 +2318,8 @@ class Workplane(CQ): selected may not be planar """ r = self._extrude( - distance, both=both, taper=taper) # returns a Solid (or a compound if there were multiple) + distance, both=both, taper=taper + ) # returns a Solid (or a compound if there were multiple) if combine: newS = self._combineWithBase(r) @@ -2287,7 +2329,9 @@ class Workplane(CQ): newS = newS.clean() return newS - def revolve(self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True): + def revolve( + self, angleDegrees=360.0, axisStart=None, axisEnd=None, combine=True, clean=True + ): """ Use all un-revolved wires in the parent chain to create a solid. @@ -2344,8 +2388,17 @@ class Workplane(CQ): newS = newS.clean() return newS - def sweep(self, path, multisection=False, sweepAlongWires=None, makeSolid=True, isFrenet=False, - combine=True, clean=True, transition='right'): + def sweep( + self, + path, + multisection=False, + sweepAlongWires=None, + makeSolid=True, + isFrenet=False, + combine=True, + clean=True, + transition="right", + ): """ Use all un-extruded wires in the parent chain to create a swept solid. @@ -2360,22 +2413,27 @@ class Workplane(CQ): Possible values are {'transformed','round', 'right'} (default: 'right'). :return: a CQ object with the resulting solid selected. """ - - if not sweepAlongWires is None: - multisection=sweepAlongWires - - from warnings import warn - warn('sweepAlongWires keyword argument is is depracated and will '\ - 'be removed in the next version; use multisection instead', - DeprecationWarning) - r = self._sweep(path.wire(), multisection, makeSolid, isFrenet, - transition) # returns a Solid (or a compound if there were multiple) + if not sweepAlongWires is None: + multisection = sweepAlongWires + + from warnings import warn + + warn( + "sweepAlongWires keyword argument is is depracated and will " + "be removed in the next version; use multisection instead", + DeprecationWarning, + ) + + r = self._sweep( + path.wire(), multisection, makeSolid, isFrenet, transition + ) # returns a Solid (or a compound if there were multiple) if combine: newS = self._combineWithBase(r) else: newS = self.newObject([r]) - if clean: newS = newS.clean() + if clean: + newS = newS.clean() return newS def _combineWithBase(self, obj): @@ -2442,7 +2500,8 @@ class Workplane(CQ): solids = toUnion.solids().vals() if len(solids) < 1: raise ValueError( - "CQ object must have at least one solid on the stack to union!") + "CQ object must have at least one solid on the stack to union!" + ) newS = solids.pop(0) for s in solids: newS = newS.fuse(s) @@ -2492,7 +2551,7 @@ class Workplane(CQ): if clean: newS = newS.clean() - + return self.newObject([newS]) def intersect(self, toIntersect, clean=True): @@ -2522,8 +2581,9 @@ class Workplane(CQ): newS = solidRef.intersect(solidToIntersect) - if clean: newS = newS.clean() - + if clean: + newS = newS.clean() + return self.newObject([newS]) def cutBlind(self, distanceToCut, clean=True, taper=None): @@ -2580,19 +2640,18 @@ class Workplane(CQ): solidRef = self.findSolid() faceRef = self.findFace() - #if no faces on the stack take the nearest face parallel to the plane zDir + # if no faces on the stack take the nearest face parallel to the plane zDir if not faceRef: - #first select all with faces with good orietation + # first select all with faces with good orietation sel = selectors.PerpendicularDirSelector(self.plane.zDir) faces = sel.filter(solidRef.Faces()) - #then select the closest + # then select the closest sel = selectors.NearestToPointSelector(self.plane.origin.toTuple()) faceRef = sel.filter(faces)[0] rv = [] for solid in solidRef.Solids(): - s = solid.dprism(faceRef, wires, thruAll=True, additive=False, - taper=-taper) + s = solid.dprism(faceRef, wires, thruAll=True, additive=False, taper=-taper) if clean: s = s.clean() @@ -2635,9 +2694,8 @@ class Workplane(CQ): # group wires together into faces based on which ones are inside the others # result is a list of lists - - wireSets = sortWiresByBuildOrder( - list(self.ctx.pendingWires), []) + + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires), []) # now all of the wires have been used to create an extrusion self.ctx.pendingWires = [] @@ -2655,18 +2713,17 @@ class Workplane(CQ): toFuse = [] if taper: - for ws in wireSets: - thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper) - toFuse.append(thisObj) + for ws in wireSets: + thisObj = Solid.extrudeLinear(ws[0], [], eDir, taper) + toFuse.append(thisObj) else: - for ws in wireSets: - thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) - toFuse.append(thisObj) + for ws in wireSets: + thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir) + toFuse.append(thisObj) - if both: - thisObj = Solid.extrudeLinear( - ws[0], ws[1:], eDir.multiply(-1.)) - toFuse.append(thisObj) + if both: + thisObj = Solid.extrudeLinear(ws[0], ws[1:], eDir.multiply(-1.0)) + toFuse.append(thisObj) return Compound.makeCompound(toFuse) @@ -2685,8 +2742,7 @@ class Workplane(CQ): This method is a utility method, primarily for plugin and internal use. """ # We have to gather the wires to be revolved - wireSets = sortWiresByBuildOrder( - list(self.ctx.pendingWires)) + wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires)) # Mark that all of the wires have been used to create a revolution self.ctx.pendingWires = [] @@ -2694,14 +2750,19 @@ class Workplane(CQ): # Revolve the wires, make a compound out of them and then fuse them toFuse = [] for ws in wireSets: - thisObj = Solid.revolve( - ws[0], ws[1:], angleDegrees, axisStart, axisEnd) + thisObj = Solid.revolve(ws[0], ws[1:], angleDegrees, axisStart, axisEnd) toFuse.append(thisObj) return Compound.makeCompound(toFuse) - def _sweep(self, path, multisection=False, makeSolid=True, isFrenet=False, - transition='right'): + def _sweep( + self, + path, + multisection=False, + makeSolid=True, + isFrenet=False, + transition="right", + ): """ Makes a swept solid from an existing set of pending wires. @@ -2716,19 +2777,28 @@ class Workplane(CQ): if not multisection: wireSets = sortWiresByBuildOrder(list(self.ctx.pendingWires)) for ws in wireSets: - thisObj = Solid.sweep(ws[0], ws[1:], path.val(), makeSolid, - isFrenet, transition) + thisObj = Solid.sweep( + ws[0], ws[1:], path.val(), makeSolid, isFrenet, transition + ) toFuse.append(thisObj) else: sections = self.ctx.pendingWires thisObj = Solid.sweep_multi(sections, path.val(), makeSolid, isFrenet) toFuse.append(thisObj) - + self.ctx.pendingWires = [] return Compound.makeCompound(toFuse) - def box(self, length, width, height, centered=(True, True, True), combine=True, clean=True): + def box( + self, + length, + width, + height, + centered=(True, True, True), + combine=True, + clean=True, + ): """ Return a 3d box with specified dimensions for each object on the stack. @@ -2773,14 +2843,14 @@ class Workplane(CQ): def _makebox(pnt): - #(xp,yp,zp) = self.plane.toLocalCoords(pnt) + # (xp,yp,zp) = self.plane.toLocalCoords(pnt) (xp, yp, zp) = pnt.toTuple() if centered[0]: - xp -= (length / 2.0) + xp -= length / 2.0 if centered[1]: - yp -= (width / 2.0) + yp -= width / 2.0 if centered[2]: - zp -= (height / 2.0) + zp -= height / 2.0 return Solid.makeBox(length, width, height, Vector(xp, yp, zp)) @@ -2793,8 +2863,17 @@ class Workplane(CQ): # combine everything return self.union(boxes, clean=clean) - def sphere(self, radius, direct=(0, 0, 1), angle1=-90, angle2=90, angle3=360, - centered=(True, True, True), combine=True, clean=True): + def sphere( + self, + radius, + direct=(0, 0, 1), + angle1=-90, + angle2=90, + angle3=360, + centered=(True, True, True), + combine=True, + clean=True, + ): """ Returns a 3D sphere with the specified radius for each point on the stack @@ -2851,7 +2930,9 @@ class Workplane(CQ): if not centered[2]: zp += radius - return Solid.makeSphere(radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3) + return Solid.makeSphere( + radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3 + ) # We want a sphere for each point on the workplane spheres = self.eachpoint(_makesphere, True) @@ -2862,8 +2943,21 @@ class Workplane(CQ): else: return self.union(spheres, clean=clean) - def wedge(self, dx, dy, dz, xmin, zmin, xmax, zmax, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), - centered=(True, True, True), combine=True, clean=True): + def wedge( + self, + dx, + dy, + dz, + xmin, + zmin, + xmax, + zmax, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + centered=(True, True, True), + combine=True, + clean=True, + ): """ :param dx: Distance along the X axis :param dy: Distance along the Y axis @@ -2901,15 +2995,17 @@ class Workplane(CQ): (xp, yp, zp) = pnt.toTuple() if not centered[0]: - xp += dx / 2. + xp += dx / 2.0 if not centered[1]: - yp += dy / 2. + yp += dy / 2.0 if not centered[2]: - zp += dx / 2. + zp += dx / 2.0 - return Solid.makeWedge(dx, dy, dz, xmin, zmin, xmax, zmax, Vector(xp, yp, zp), dir) + return Solid.makeWedge( + dx, dy, dz, xmin, zmin, xmax, zmax, Vector(xp, yp, zp), dir + ) # We want a wedge for each point on the workplane wedges = self.eachpoint(_makewedge) @@ -2945,11 +3041,23 @@ class Workplane(CQ): cleanObjects = [obj.clean() for obj in self.objects] except AttributeError: raise AttributeError( - "%s object doesn't support `clean()` method!" % obj.ShapeType()) + "%s object doesn't support `clean()` method!" % obj.ShapeType() + ) return self.newObject(cleanObjects) - def text(self, txt, fontsize, distance, cut=True, combine=False, clean=True, - font="Arial", kind='regular',halign='center',valign='center'): + def text( + self, + txt, + fontsize, + distance, + cut=True, + combine=False, + clean=True, + font="Arial", + kind="regular", + halign="center", + valign="center", + ): """ Create a 3D text @@ -2976,8 +3084,16 @@ class Workplane(CQ): and the resulting solid becomes the new context solid. """ - r = Compound.makeText(txt,fontsize,distance,font=font,kind=kind, - halign=halign, valign=valign, position=self.plane) + r = Compound.makeText( + txt, + fontsize, + distance, + font=font, + kind=kind, + halign=halign, + valign=valign, + position=self.plane, + ) if cut: newS = self._cutFromBase(r) @@ -2995,7 +3111,6 @@ class Workplane(CQ): """ if type(self.objects[0]) is Vector: - return '< {} >'.format(self.__repr__()[1:-1]) + return "< {} >".format(self.__repr__()[1:-1]) else: return Compound.makeCompound(self.objects)._repr_html_() - diff --git a/cadquery/cq_directive.py b/cadquery/cq_directive.py index 11de3b5e..bfbfc0de 100644 --- a/cadquery/cq_directive.py +++ b/cadquery/cq_directive.py @@ -20,13 +20,22 @@ template = """ """ -template_content_indent = ' ' +template_content_indent = " " -def cq_directive(name, arguments, options, content, lineno, - content_offset, block_text, state, state_machine): +def cq_directive( + name, + arguments, + options, + content, + lineno, + content_offset, + block_text, + state, + state_machine, +): # only consider inline snippets - plot_code = '\n'.join(content) + plot_code = "\n".join(content) # Since we don't have a filename, use a hash based on the content # the script must define a variable called 'out', which is expected to @@ -52,22 +61,20 @@ def cq_directive(name, arguments, options, content, lineno, lines = [] # get rid of new lines - out_svg = out_svg.replace('\n', '') + out_svg = out_svg.replace("\n", "") txt_align = "left" if "align" in options: - txt_align = options['align'] + txt_align = options["align"] - lines.extend((template % locals()).split('\n')) + lines.extend((template % locals()).split("\n")) - lines.extend(['::', '']) - lines.extend([' %s' % row.rstrip() - for row in plot_code.split('\n')]) - lines.append('') + lines.extend(["::", ""]) + lines.extend([" %s" % row.rstrip() for row in plot_code.split("\n")]) + lines.append("") if len(lines): - state_machine.insert_input( - lines, state_machine.input_lines.source(0)) + state_machine.insert_input(lines, state_machine.input_lines.source(0)) return [] @@ -77,9 +84,10 @@ def setup(app): setup.config = app.config setup.confdir = app.confdir - options = {'height': directives.length_or_unitless, - 'width': directives.length_or_percentage_or_unitless, - 'align': directives.unchanged - } + options = { + "height": directives.length_or_unitless, + "width": directives.length_or_percentage_or_unitless, + "align": directives.unchanged, + } - app.add_directive('cq_plot', cq_directive, True, (0, 2, 0), **options) + app.add_directive("cq_plot", cq_directive, True, (0, 2, 0), **options) diff --git a/cadquery/cqgi.py b/cadquery/cqgi.py index e6611f46..4562fcef 100644 --- a/cadquery/cqgi.py +++ b/cadquery/cqgi.py @@ -9,6 +9,7 @@ import cadquery CQSCRIPT = "" + def parse(script_source): """ Parses the script as a model, and returns a model. @@ -34,6 +35,7 @@ class CQModel(object): the build method can be used to generate a 3d model """ + def __init__(self, script_source): """ Create an object by parsing the supplied python script. @@ -100,16 +102,20 @@ class CQModel(object): try: self.set_param_values(build_parameters) collector = ScriptCallback() - env = EnvironmentBuilder().with_real_builtins().with_cadquery_objects() \ - .add_entry("__name__", "__cqgi__") \ - .add_entry("show_object", collector.show_object) \ - .add_entry("debug", collector.debug) \ - .add_entry("describe_parameter",collector.describe_parameter) \ + env = ( + EnvironmentBuilder() + .with_real_builtins() + .with_cadquery_objects() + .add_entry("__name__", "__cqgi__") + .add_entry("show_object", collector.show_object) + .add_entry("debug", collector.debug) + .add_entry("describe_parameter", collector.describe_parameter) .build() + ) - c = compile(self.ast_tree, CQSCRIPT, 'exec') - exec (c, env) - result.set_debug(collector.debugObjects ) + c = compile(self.ast_tree, CQSCRIPT, "exec") + exec(c, env) + result.set_debug(collector.debugObjects) result.set_success_result(collector.outputObjects) except Exception as ex: @@ -124,7 +130,9 @@ class CQModel(object): for k, v in params.items(): if k not in model_parameters: - raise InvalidParameterError("Cannot set value '%s': not a parameter of the model." % k) + raise InvalidParameterError( + "Cannot set value '%s': not a parameter of the model." % k + ) p = model_parameters[k] p.set_value(v) @@ -134,10 +142,12 @@ class ShapeResult(object): """ An object created by a build, including the user parameters provided """ + def __init__(self): self.shape = None self.options = None + class BuildResult(object): """ The result of executing a CadQuery script. @@ -149,10 +159,11 @@ class BuildResult(object): If unsuccessful, the exception property contains a reference to the stack trace that occurred. """ + def __init__(self): self.buildTime = None - self.results = [] #list of ShapeResult - self.debugObjects = [] #list of ShapeResult + self.results = [] # list of ShapeResult + self.debugObjects = [] # list of ShapeResult self.first_result = None self.success = False self.exception = None @@ -176,13 +187,14 @@ class ScriptMetadata(object): Defines the metadata for a parsed CQ Script. the parameters property is a dict of InputParameter objects. """ + def __init__(self): self.parameters = {} def add_script_parameter(self, p): self.parameters[p.name] = p - def add_parameter_description(self,name,description): + def add_parameter_description(self, name, description): p = self.parameters[name] p.desc = description @@ -214,6 +226,7 @@ class InputParameter: provide additional metadata """ + def __init__(self): #: the default value for the variable. @@ -234,7 +247,9 @@ class InputParameter: self.ast_node = None @staticmethod - def create(ast_node, var_name, var_type, default_value, valid_values=None, desc=None): + def create( + ast_node, var_name, var_type, default_value, valid_values=None, desc=None + ): if valid_values is None: valid_values = [] @@ -251,8 +266,10 @@ class InputParameter: def set_value(self, new_value): if len(self.valid_values) > 0 and new_value not in self.valid_values: raise InvalidParameterError( - "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} " - .format(str(new_value), self.name, str(self.valid_values))) + "Cannot set value '{0:s}' for parameter '{1:s}': not a valid value. Valid values are {2:s} ".format( + str(new_value), self.name, str(self.valid_values) + ) + ) if self.varType == NumberParameterType: try: @@ -265,28 +282,33 @@ class InputParameter: self.ast_node.n = f except ValueError: raise InvalidParameterError( - "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric." - .format(str(new_value), self.name)) + "Cannot set value '{0:s}' for parameter '{1:s}': parameter must be numeric.".format( + str(new_value), self.name + ) + ) elif self.varType == StringParameterType: self.ast_node.s = str(new_value) elif self.varType == BooleanParameterType: if new_value: - if hasattr(ast, 'NameConstant'): + if hasattr(ast, "NameConstant"): self.ast_node.value = True else: - self.ast_node.id = 'True' + self.ast_node.id = "True" else: - if hasattr(ast, 'NameConstant'): + if hasattr(ast, "NameConstant"): self.ast_node.value = False else: - self.ast_node.id = 'False' + self.ast_node.id = "False" else: raise ValueError("Unknown Type of var: ", str(self.varType)) def __str__(self): return "InputParameter: {name=%s, type=%s, defaultValue=%s" % ( - self.name, str(self.varType), str(self.default_value)) + self.name, + str(self.varType), + str(self.default_value), + ) class ScriptCallback(object): @@ -295,22 +317,23 @@ class ScriptCallback(object): the show_object() method is exposed to CQ scripts, to allow them to return objects to the execution environment """ + def __init__(self): self.outputObjects = [] self.debugObjects = [] - def show_object(self, shape,options={}): + def show_object(self, shape, options={}): """ return an object to the executing environment, with options :param shape: a cadquery object :param options: a dictionary of options that will be made available to the executing environment """ o = ShapeResult() - o.options=options + o.options = options o.shape = shape self.outputObjects.append(o) - def debug(self,obj,args={}): + def debug(self, obj, args={}): """ Debug print/output an object, with optional arguments. """ @@ -319,7 +342,7 @@ class ScriptCallback(object): s.options = args self.debugObjects.append(s) - def describe_parameter(self,var_data ): + def describe_parameter(self, var_data): """ Do Nothing-- we parsed the ast ahead of execution to get what we need. """ @@ -335,12 +358,12 @@ class ScriptCallback(object): return len(self.outputObjects) > 0 - class InvalidParameterError(Exception): """ Raised when an attempt is made to provide a new parameter value that cannot be assigned to the model """ + pass @@ -349,6 +372,7 @@ class NoOutputError(Exception): Raised when the script does not execute the show_object() method to return a solid """ + pass @@ -386,6 +410,7 @@ class EnvironmentBuilder(object): The environment includes the builtins, as well as the other methods the script will need. """ + def __init__(self): self.env = {} @@ -393,12 +418,12 @@ class EnvironmentBuilder(object): return self.with_builtins(__builtins__) def with_builtins(self, env_dict): - self.env['__builtins__'] = env_dict + self.env["__builtins__"] = env_dict return self def with_cadquery_objects(self): - self.env['cadquery'] = cadquery - self.env['cq'] = cadquery + self.env["cadquery"] = cadquery + self.env["cq"] = cadquery return self def add_entry(self, name, value): @@ -408,30 +433,33 @@ class EnvironmentBuilder(object): def build(self): return self.env + class ParameterDescriptionFinder(ast.NodeTransformer): """ Visits a parse tree, looking for function calls to describe_parameter(var, description ) """ + def __init__(self, cq_model): self.cqModel = cq_model - def visit_Call(self,node): - """ + def visit_Call(self, node): + """ Called when we see a function call. Is it describe_parameter? """ - try: - if node.func.id == 'describe_parameter': + try: + if node.func.id == "describe_parameter": # looks like we have a call to our function. # first parameter is the variable, # second is the description varname = node.args[0].id desc = node.args[1].s - self.cqModel.add_parameter_description(varname,desc) + self.cqModel.add_parameter_description(varname, desc) - except: - #print "Unable to handle function call" + except: + # print "Unable to handle function call" pass - return node + return node + class ConstantAssignmentFinder(ast.NodeTransformer): """ @@ -446,24 +474,42 @@ class ConstantAssignmentFinder(ast.NodeTransformer): if type(value_node) == ast.Num: self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, NumberParameterType, value_node.n)) + InputParameter.create( + value_node, var_name, NumberParameterType, value_node.n + ) + ) elif type(value_node) == ast.Str: self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, StringParameterType, value_node.s)) + InputParameter.create( + value_node, var_name, StringParameterType, value_node.s + ) + ) elif type(value_node) == ast.Name: - if value_node.id == 'True': + if value_node.id == "True": self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, BooleanParameterType, True)) - elif value_node.id == 'False': + InputParameter.create( + value_node, var_name, BooleanParameterType, True + ) + ) + elif value_node.id == "False": self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, BooleanParameterType, False)) - elif hasattr(ast, 'NameConstant') and type(value_node) == ast.NameConstant: + InputParameter.create( + value_node, var_name, BooleanParameterType, False + ) + ) + elif hasattr(ast, "NameConstant") and type(value_node) == ast.NameConstant: if value_node.value == True: self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, BooleanParameterType, True)) + InputParameter.create( + value_node, var_name, BooleanParameterType, True + ) + ) else: self.cqModel.add_script_parameter( - InputParameter.create(value_node, var_name, BooleanParameterType, False)) + InputParameter.create( + value_node, var_name, BooleanParameterType, False + ) + ) except: print("Unable to handle assignment for variable '%s'" % var_name) pass @@ -479,7 +525,7 @@ class ConstantAssignmentFinder(ast.NodeTransformer): # Handle the NamedConstant type that is only present in Python 3 astTypes = [ast.Num, ast.Str, ast.Name] - if hasattr(ast, 'NameConstant'): + if hasattr(ast, "NameConstant"): astTypes.append(ast.NameConstant) if type(node.value) in astTypes: diff --git a/cadquery/occ_impl/exporters.py b/cadquery/occ_impl/exporters.py index f3528216..39898b1b 100644 --- a/cadquery/occ_impl/exporters.py +++ b/cadquery/occ_impl/exporters.py @@ -4,6 +4,7 @@ from OCC.Core.Visualization import Tesselator import tempfile import os import sys + if sys.version_info.major == 2: import cStringIO as StringIO else: @@ -56,7 +57,7 @@ def exportShape(shape, exportType, fileLike, tolerance=0.1): The object should be already open and ready to write. The caller is responsible for closing the object """ - + from ..cq import CQ def tessellate(shape): @@ -116,7 +117,7 @@ def readAndDeleteFile(fileName): return the contents as a string """ res = "" - with open(fileName, 'r') as f: + with open(fileName, "r") as f: res = "{}".format(f.read()) os.remove(fileName) @@ -152,34 +153,34 @@ class AmfWriter(object): self.tessellation = tessellation def writeAmf(self, outFile): - amf = ET.Element('amf', units=self.units) + amf = ET.Element("amf", units=self.units) # TODO: if result is a compound, we need to loop through them - object = ET.SubElement(amf, 'object', id="0") - mesh = ET.SubElement(object, 'mesh') - vertices = ET.SubElement(mesh, 'vertices') - volume = ET.SubElement(mesh, 'volume') + object = ET.SubElement(amf, "object", id="0") + mesh = ET.SubElement(object, "mesh") + vertices = ET.SubElement(mesh, "vertices") + volume = ET.SubElement(mesh, "volume") # add vertices for i_vert in range(self.tessellation.ObjGetVertexCount()): v = self.tessellation.GetVertex(i_vert) - vtx = ET.SubElement(vertices, 'vertex') - coord = ET.SubElement(vtx, 'coordinates') - x = ET.SubElement(coord, 'x') + vtx = ET.SubElement(vertices, "vertex") + coord = ET.SubElement(vtx, "coordinates") + x = ET.SubElement(coord, "x") x.text = str(v[0]) - y = ET.SubElement(coord, 'y') + y = ET.SubElement(coord, "y") y.text = str(v[1]) - z = ET.SubElement(coord, 'z') + z = ET.SubElement(coord, "z") z.text = str(v[2]) # add triangles for i_tr in range(self.tessellation.ObjGetTriangleCount()): t = self.tessellation.GetTriangleIndex(i_tr) - triangle = ET.SubElement(volume, 'triangle') - v1 = ET.SubElement(triangle, 'v1') + triangle = ET.SubElement(volume, "triangle") + v1 = ET.SubElement(triangle, "v1") v1.text = str(t[0]) - v2 = ET.SubElement(triangle, 'v2') + v2 = ET.SubElement(triangle, "v2") v2.text = str(t[1]) - v3 = ET.SubElement(triangle, 'v3') + v3 = ET.SubElement(triangle, "v3") v3.text = str(t[2]) amf = ET.ElementTree(amf).write(outFile, xml_declaration=True) @@ -217,11 +218,11 @@ class JsonMesh(object): def toJson(self): return JSON_TEMPLATE % { - 'vertices': str(self.vertices), - 'faces': str(self.faces), - 'nVertices': self.nVertices, - 'nFaces': self.nFaces - }; + "vertices": str(self.vertices), + "faces": str(self.faces), + "nVertices": self.nVertices, + "nFaces": self.nFaces, + } def makeSVGedge(e): @@ -235,20 +236,16 @@ def makeSVGedge(e): start = curve.FirstParameter() end = curve.LastParameter() - points = GCPnts_QuasiUniformDeflection(curve, - DISCRETIZATION_TOLERANCE, - start, - end) + points = GCPnts_QuasiUniformDeflection(curve, DISCRETIZATION_TOLERANCE, start, end) if points.IsDone(): - point_it = (points.Value(i + 1) for i in - range(points.NbPoints())) + point_it = (points.Value(i + 1) for i in range(points.NbPoints())) p = next(point_it) - cs.write('M{},{} '.format(p.X(), p.Y())) + cs.write("M{},{} ".format(p.X(), p.Y())) for p in point_it: - cs.write('L{},{} '.format(p.X(), p.Y())) + cs.write("L{},{} ".format(p.X(), p.Y())) return cs.getvalue() @@ -277,7 +274,7 @@ def getSVG(shape, opts=None): Export a shape to SVG """ - d = {'width': 800, 'height': 240, 'marginLeft': 200, 'marginTop': 20} + d = {"width": 800, "height": 240, "marginLeft": 200, "marginTop": 20} if opts: d.update(opts) @@ -285,17 +282,15 @@ def getSVG(shape, opts=None): # need to guess the scale and the coordinate center uom = guessUnitOfMeasure(shape) - width = float(d['width']) - height = float(d['height']) - marginLeft = float(d['marginLeft']) - marginTop = float(d['marginTop']) + width = float(d["width"]) + height = float(d["height"]) + marginLeft = float(d["marginLeft"]) + marginTop = float(d["marginTop"]) hlr = HLRBRep_Algo() hlr.Add(shape.wrapped) - projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), - DEFAULT_DIR) - ) + projector = HLRAlgo_Projector(gp_Ax2(gp_Pnt(), DEFAULT_DIR)) hlr.Projector(projector) hlr.Update() @@ -336,8 +331,7 @@ def getSVG(shape, opts=None): # convert to native CQ objects visible = list(map(Shape, visible)) hidden = list(map(Shape, hidden)) - (hiddenPaths, visiblePaths) = getPaths(visible, - hidden) + (hiddenPaths, visiblePaths) = getPaths(visible, hidden) # get bounding box -- these are all in 2-d space bb = Compound.makeCompound(hidden + visible).BoundingBox() @@ -346,8 +340,10 @@ def getSVG(shape, opts=None): unitScale = min(width / bb.xlen * 0.75, height / bb.ylen * 0.75) # compute amount to translate-- move the top left into view - (xTranslate, yTranslate) = ((0 - bb.xmin) + marginLeft / - unitScale, (0 - bb.ymax) - marginTop / unitScale) + (xTranslate, yTranslate) = ( + (0 - bb.xmin) + marginLeft / unitScale, + (0 - bb.ymax) - marginTop / unitScale, + ) # compute paths ( again -- had to strip out freecad crap ) hiddenContent = "" @@ -362,19 +358,19 @@ def getSVG(shape, opts=None): { "unitScale": str(unitScale), "strokeWidth": str(1.0 / unitScale), - "hiddenContent": hiddenContent, + "hiddenContent": hiddenContent, "visibleContent": visibleContent, "xTranslate": str(xTranslate), "yTranslate": str(yTranslate), "width": str(width), "height": str(height), "textboxY": str(height - 30), - "uom": str(uom) + "uom": str(uom), } ) # svg = SVG_TEMPLATE % ( # {"content": projectedContent} - #) + # ) return svg @@ -386,7 +382,7 @@ def exportSVG(shape, fileName): """ svg = getSVG(shape.val()) - f = open(fileName, 'w') + f = open(fileName, "w") f.write(svg) f.close() @@ -471,4 +467,4 @@ SVG_TEMPLATE = """ """ -PATHTEMPLATE = "\t\t\t\n" +PATHTEMPLATE = '\t\t\t\n' diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index d959702a..812f6dc2 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -1,6 +1,16 @@ import math - -from OCC.Core.gp import gp_Vec, gp_Ax1, gp_Ax3, gp_Pnt, gp_Dir, gp_Trsf, gp_GTrsf, gp, gp_XYZ + +from OCC.Core.gp import ( + gp_Vec, + gp_Ax1, + gp_Ax3, + gp_Pnt, + gp_Dir, + gp_Trsf, + gp_GTrsf, + gp, + gp_XYZ, +) from OCC.Core.Bnd import Bnd_Box from OCC.Core.BRepBndLib import brepbndlib_Add # brepbndlib_AddOptimal from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh @@ -27,16 +37,16 @@ class Vector(object): if len(args) == 3: fV = gp_Vec(*args) elif len(args) == 2: - fV = gp_Vec(*args,0) + fV = gp_Vec(*args, 0) elif len(args) == 1: if isinstance(args[0], Vector): fV = gp_Vec(args[0].wrapped.XYZ()) elif isinstance(args[0], (tuple, list)): arg = args[0] - if len(arg)==3: + if len(arg) == 3: fV = gp_Vec(*arg) - elif len(arg)==2: - fV = gp_Vec(*arg,0) + elif len(arg) == 2: + fV = gp_Vec(*arg, 0) elif isinstance(args[0], (gp_Vec, gp_Pnt, gp_Dir)): fV = gp_Vec(args[0].XYZ()) elif isinstance(args[0], gp_XYZ): @@ -53,25 +63,25 @@ class Vector(object): @property def x(self): return self.wrapped.X() - + @x.setter - def x(self,value): + def x(self, value): self.wrapped.SetX(value) @property def y(self): return self.wrapped.Y() - + @y.setter - def y(self,value): + def y(self, value): self.wrapped.SetY(value) @property def z(self): return self.wrapped.Z() - + @z.setter - def z(self,value): + def z(self, value): self.wrapped.SetZ(value) @property @@ -132,16 +142,13 @@ class Vector(object): return self.wrapped.Angle(v.wrapped) def distanceToLine(self): - raise NotImplementedError( - "Have not needed this yet, but FreeCAD supports it!") + raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") def projectToLine(self): - raise NotImplementedError( - "Have not needed this yet, but FreeCAD supports it!") + raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") def distanceToPlane(self): - raise NotImplementedError( - "Have not needed this yet, but FreeCAD supports it!") + raise NotImplementedError("Have not needed this yet, but FreeCAD supports it!") def projectToPlane(self, plane): """ @@ -154,7 +161,7 @@ class Vector(object): base = plane.origin normal = plane.zDir - return self-normal*(((self-base).dot(normal))/normal.Length**2) + return self - normal * (((self - base).dot(normal)) / normal.Length ** 2) def __neg__(self): return self * -1 @@ -163,18 +170,19 @@ class Vector(object): return self.Length def __repr__(self): - return 'Vector: ' + str((self.x, self.y, self.z)) + return "Vector: " + str((self.x, self.y, self.z)) def __str__(self): - return 'Vector: ' + str((self.x, self.y, self.z)) + return "Vector: " + str((self.x, self.y, self.z)) def __eq__(self, other): return self.wrapped.IsEqual(other.wrapped, 0.00001, 0.00001) - ''' + + """ is not implemented in OCC def __ne__(self, other): return self.wrapped.__ne__(other) - ''' + """ def toPnt(self): @@ -222,44 +230,48 @@ class Matrix: elif isinstance(matrix, (list, tuple)): # Validate matrix size & 4x4 last row value valid_sizes = all( - (isinstance(row, (list, tuple)) and (len(row) == 4)) - for row in matrix + (isinstance(row, (list, tuple)) and (len(row) == 4)) for row in matrix ) and len(matrix) in (3, 4) if not valid_sizes: - raise TypeError("Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format(matrix)) - elif (len(matrix) == 4) and (tuple(matrix[3]) != (0,0,0,1)): - raise ValueError("Expected the last row to be [0,0,0,1], but got: {!r}".format(matrix[3])) + raise TypeError( + "Matrix constructor requires 2d list of 4x3 or 4x4, but got: {!r}".format( + matrix + ) + ) + elif (len(matrix) == 4) and (tuple(matrix[3]) != (0, 0, 0, 1)): + raise ValueError( + "Expected the last row to be [0,0,0,1], but got: {!r}".format( + matrix[3] + ) + ) # Assign values to matrix self.wrapped = gp_GTrsf() - [self.wrapped.SetValue(i+1,j+1,e) - for i,row in enumerate(matrix[:3]) - for j,e in enumerate(row)] - + [ + self.wrapped.SetValue(i + 1, j + 1, e) + for i, row in enumerate(matrix[:3]) + for j, e in enumerate(row) + ] + else: - raise TypeError( - "Invalid param to matrix constructor: {}".format(matrix)) + raise TypeError("Invalid param to matrix constructor: {}".format(matrix)) def rotateX(self, angle): - self._rotate(gp.OX(), - angle) + self._rotate(gp.OX(), angle) def rotateY(self, angle): - self._rotate(gp.OY(), - angle) + self._rotate(gp.OY(), angle) def rotateZ(self, angle): - self._rotate(gp.OZ(), - angle) + self._rotate(gp.OZ(), angle) def _rotate(self, direction, angle): new = gp_Trsf() - new.SetRotation(direction, - angle) + new.SetRotation(direction, angle) self.wrapped = self.wrapped * gp_GTrsf(new) @@ -277,11 +289,12 @@ class Matrix: def transposed_list(self): """Needed by the cqparts gltf exporter """ - + trsf = self.wrapped - data = [[trsf.Value(i,j) for j in range(1,5)] for i in range(1,4)] + \ - [[0.,0.,0.,1.]] - + data = [[trsf.Value(i, j) for j in range(1, 5)] for i in range(1, 4)] + [ + [0.0, 0.0, 0.0, 1.0] + ] + return [data[j][i] for i in range(4) for j in range(4)] def __getitem__(self, rc): @@ -298,7 +311,7 @@ class Matrix: else: # gp_GTrsf doesn't provide access to the 4th row because it has # an implied value as below: - return [0., 0., 0., 1.][c] + return [0.0, 0.0, 0.0, 1.0][c] else: raise IndexError("Out of bounds access into 4x4 matrix: {!r}".format(rc)) @@ -352,95 +365,94 @@ class Plane(object): namedPlanes = { # origin, xDir, normal - 'XY': Plane(origin, (1, 0, 0), (0, 0, 1)), - 'YZ': Plane(origin, (0, 1, 0), (1, 0, 0)), - 'ZX': Plane(origin, (0, 0, 1), (0, 1, 0)), - 'XZ': Plane(origin, (1, 0, 0), (0, -1, 0)), - 'YX': Plane(origin, (0, 1, 0), (0, 0, -1)), - 'ZY': Plane(origin, (0, 0, 1), (-1, 0, 0)), - 'front': Plane(origin, (1, 0, 0), (0, 0, 1)), - 'back': Plane(origin, (-1, 0, 0), (0, 0, -1)), - 'left': Plane(origin, (0, 0, 1), (-1, 0, 0)), - 'right': Plane(origin, (0, 0, -1), (1, 0, 0)), - 'top': Plane(origin, (1, 0, 0), (0, 1, 0)), - 'bottom': Plane(origin, (1, 0, 0), (0, -1, 0)) + "XY": Plane(origin, (1, 0, 0), (0, 0, 1)), + "YZ": Plane(origin, (0, 1, 0), (1, 0, 0)), + "ZX": Plane(origin, (0, 0, 1), (0, 1, 0)), + "XZ": Plane(origin, (1, 0, 0), (0, -1, 0)), + "YX": Plane(origin, (0, 1, 0), (0, 0, -1)), + "ZY": Plane(origin, (0, 0, 1), (-1, 0, 0)), + "front": Plane(origin, (1, 0, 0), (0, 0, 1)), + "back": Plane(origin, (-1, 0, 0), (0, 0, -1)), + "left": Plane(origin, (0, 0, 1), (-1, 0, 0)), + "right": Plane(origin, (0, 0, -1), (1, 0, 0)), + "top": Plane(origin, (1, 0, 0), (0, 1, 0)), + "bottom": Plane(origin, (1, 0, 0), (0, -1, 0)), } try: return namedPlanes[stdName] except KeyError: - raise ValueError('Supported names are {}'.format( - list(namedPlanes.keys()))) + raise ValueError("Supported names are {}".format(list(namedPlanes.keys()))) @classmethod def XY(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('XY', origin) + plane = Plane.named("XY", origin) plane._setPlaneDir(xDir) return plane @classmethod def YZ(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)): - plane = Plane.named('YZ', origin) + plane = Plane.named("YZ", origin) plane._setPlaneDir(xDir) return plane @classmethod def ZX(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): - plane = Plane.named('ZX', origin) + plane = Plane.named("ZX", origin) plane._setPlaneDir(xDir) return plane @classmethod def XZ(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('XZ', origin) + plane = Plane.named("XZ", origin) plane._setPlaneDir(xDir) return plane @classmethod def YX(cls, origin=(0, 0, 0), xDir=Vector(0, 1, 0)): - plane = Plane.named('YX', origin) + plane = Plane.named("YX", origin) plane._setPlaneDir(xDir) return plane @classmethod def ZY(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): - plane = Plane.named('ZY', origin) + plane = Plane.named("ZY", origin) plane._setPlaneDir(xDir) return plane @classmethod def front(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('front', origin) + plane = Plane.named("front", origin) plane._setPlaneDir(xDir) return plane @classmethod def back(cls, origin=(0, 0, 0), xDir=Vector(-1, 0, 0)): - plane = Plane.named('back', origin) + plane = Plane.named("back", origin) plane._setPlaneDir(xDir) return plane @classmethod def left(cls, origin=(0, 0, 0), xDir=Vector(0, 0, 1)): - plane = Plane.named('left', origin) + plane = Plane.named("left", origin) plane._setPlaneDir(xDir) return plane @classmethod def right(cls, origin=(0, 0, 0), xDir=Vector(0, 0, -1)): - plane = Plane.named('right', origin) + plane = Plane.named("right", origin) plane._setPlaneDir(xDir) return plane @classmethod def top(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('top', origin) + plane = Plane.named("top", origin) plane._setPlaneDir(xDir) return plane @classmethod def bottom(cls, origin=(0, 0, 0), xDir=Vector(1, 0, 0)): - plane = Plane.named('bottom', origin) + plane = Plane.named("bottom", origin) plane._setPlaneDir(xDir) return plane @@ -458,12 +470,12 @@ class Plane(object): :return: a plane in the global space, with the xDirection of the plane in the specified direction. """ zDir = Vector(normal) - if (zDir.Length == 0.0): - raise ValueError('normal should be non null') + if zDir.Length == 0.0: + raise ValueError("normal should be non null") xDir = Vector(xDir) - if (xDir.Length == 0.0): - raise ValueError('xDir should be non null') + if xDir.Length == 0.0: + raise ValueError("xDir should be non null") self.zDir = zDir.normalized() self._setPlaneDir(xDir) @@ -489,7 +501,8 @@ class Plane(object): @property def origin(self): return self._origin -# TODO is this property rly needed -- why not handle this in the constructor + + # TODO is this property rly needed -- why not handle this in the constructor @origin.setter def origin(self, value): @@ -545,7 +558,7 @@ class Plane(object): pass - ''' + """ # TODO: also use a set of points along the wire to test as well. # TODO: would it be more efficient to create objects in the local # coordinate system, and then transform to global @@ -562,7 +575,7 @@ class Plane(object): # findOutsideBox actually inspects both ways, here we only want to # know if one is inside the other return bb == BoundBox.findOutsideBox2D(bb, tb) - ''' + """ def toLocalCoords(self, obj): """Project the provided coordinates onto this plane @@ -580,7 +593,7 @@ class Plane(object): """ from .shapes import Shape - + if isinstance(obj, Vector): return obj.transform(self.fG) elif isinstance(obj, Shape): @@ -588,7 +601,9 @@ class Plane(object): else: raise ValueError( "Don't know how to convert type {} to local coordinates".format( - type(obj))) + type(obj) + ) + ) def toWorldCoords(self, tuplePoint): """Convert a point in local coordinates to global coordinates @@ -655,7 +670,7 @@ class Plane(object): raise NotImplementedError - ''' + """ resultWires = [] for w in listOfShapes: mirrored = w.transformGeometry(rotationMatrix.wrapped) @@ -681,21 +696,19 @@ class Plane(object): resultWires.append(cadquery.Shape.cast(mirroredWire)) - return resultWires''' + return resultWires""" - def mirrorInPlane(self, listOfShapes, axis='X'): + def mirrorInPlane(self, listOfShapes, axis="X"): - local_coord_system = gp_Ax3(self.origin.toPnt(), - self.zDir.toDir(), - self.xDir.toDir()) + local_coord_system = gp_Ax3( + self.origin.toPnt(), self.zDir.toDir(), self.xDir.toDir() + ) T = gp_Trsf() - if axis == 'X': - T.SetMirror(gp_Ax1(self.origin.toPnt(), - local_coord_system.XDirection())) - elif axis == 'Y': - T.SetMirror(gp_Ax1(self.origin.toPnt(), - local_coord_system.YDirection())) + if axis == "X": + T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.XDirection())) + elif axis == "Y": + T.SetMirror(gp_Ax1(self.origin.toPnt(), local_coord_system.YDirection())) else: raise NotImplementedError @@ -726,22 +739,21 @@ class Plane(object): # the double-inverting is strange, and I don't understand it. forward = Matrix() inverse = Matrix() - + forwardT = gp_Trsf() inverseT = gp_Trsf() global_coord_system = gp_Ax3() - local_coord_system = gp_Ax3(gp_Pnt(*self.origin.toTuple()), - gp_Dir(*self.zDir.toTuple()), - gp_Dir(*self.xDir.toTuple()) - ) + local_coord_system = gp_Ax3( + gp_Pnt(*self.origin.toTuple()), + gp_Dir(*self.zDir.toTuple()), + gp_Dir(*self.xDir.toTuple()), + ) - forwardT.SetTransformation(global_coord_system, - local_coord_system) + forwardT.SetTransformation(global_coord_system, local_coord_system) forward.wrapped = gp_GTrsf(forwardT) - - inverseT.SetTransformation(local_coord_system, - global_coord_system) + + inverseT.SetTransformation(local_coord_system, global_coord_system) inverse.wrapped = gp_GTrsf(inverseT) # TODO verify if this is OK @@ -767,11 +779,9 @@ class BoundBox(object): self.zmax = ZMax self.zlen = ZMax - ZMin - self.center = Vector((XMax + XMin) / 2, - (YMax + YMin) / 2, - (ZMax + ZMin) / 2) + self.center = Vector((XMax + XMin) / 2, (YMax + YMin) / 2, (ZMax + ZMin) / 2) - self.DiagonalLength = self.wrapped.SquareExtent()**0.5 + self.DiagonalLength = self.wrapped.SquareExtent() ** 0.5 def add(self, obj, tol=1e-8): """Returns a modified (expanded) bounding box @@ -810,25 +820,29 @@ class BoundBox(object): the built-in implementation i do not understand. """ - if (bb1.XMin < bb2.XMin and - bb1.XMax > bb2.XMax and - bb1.YMin < bb2.YMin and - bb1.YMax > bb2.YMax): + if ( + bb1.XMin < bb2.XMin + and bb1.XMax > bb2.XMax + and bb1.YMin < bb2.YMin + and bb1.YMax > bb2.YMax + ): return bb1 - if (bb2.XMin < bb1.XMin and - bb2.XMax > bb1.XMax and - bb2.YMin < bb1.YMin and - bb2.YMax > bb1.YMax): + if ( + bb2.XMin < bb1.XMin + and bb2.XMax > bb1.XMax + and bb2.YMin < bb1.YMin + and bb2.YMax > bb1.YMax + ): return bb2 return None @classmethod def _fromTopoDS(cls, shape, tol=None, optimal=False): - ''' + """ Constructs a bounding box from a TopoDS_Shape - ''' + """ tol = TOL if tol is None else tol # tol = TOL (by default) bbox = Bnd_Box() bbox.SetGap(tol) @@ -845,12 +859,14 @@ class BoundBox(object): def isInside(self, b2): """Is the provided bounding box inside this one?""" - if (b2.xmin > self.xmin and - b2.ymin > self.ymin and - b2.zmin > self.zmin and - b2.xmax < self.xmax and - b2.ymax < self.ymax and - b2.zmax < self.zmax): + if ( + b2.xmin > self.xmin + and b2.ymin > self.ymin + and b2.zmin > self.zmin + and b2.xmax < self.xmax + and b2.ymax < self.ymax + and b2.zmax < self.zmax + ): return True else: return False diff --git a/cadquery/occ_impl/importers.py b/cadquery/occ_impl/importers.py index 892144c4..60a7c937 100644 --- a/cadquery/occ_impl/importers.py +++ b/cadquery/occ_impl/importers.py @@ -34,14 +34,14 @@ def importStep(fileName): Accepts a file name and loads the STEP file into a cadquery shape :param fileName: The path and name of the STEP file to be imported """ - + # Now read and return the shape reader = STEPControl_Reader() readStatus = reader.ReadFile(fileName) if readStatus != OCC.Core.IFSelect.IFSelect_RetDone: raise ValueError("STEP File could not be loaded") for i in range(reader.NbRootsForTransfer()): - reader.TransferRoot(i+1) + reader.TransferRoot(i + 1) occ_shapes = [] for i in range(reader.NbShapes()): diff --git a/cadquery/occ_impl/jupyter_tools.py b/cadquery/occ_impl/jupyter_tools.py index 0c4f64b8..1714d4ab 100644 --- a/cadquery/occ_impl/jupyter_tools.py +++ b/cadquery/occ_impl/jupyter_tools.py @@ -6,8 +6,7 @@ from xml.etree import ElementTree from .geom import BoundBox -BOILERPLATE = \ -''' +BOILERPLATE = """
@@ -35,69 +34,86 @@ BOILERPLATE = \ //document.getElementById('{id}').runtime.fitAll() -''' +""" -#https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file -#better if else +# https://stackoverflow.com/questions/950087/how-do-i-include-a-javascript-file-in-another-javascript-file +# better if else -ROT = (0.77,0.3,0.55,1.28) -ROT = (0.,0,0,1.) +ROT = (0.77, 0.3, 0.55, 1.28) +ROT = (0.0, 0, 0, 1.0) FOV = 0.2 -def add_x3d_boilerplate(src, height=400, center=(0,0,0), d=(0,0,15), fov=FOV, rot='{} {} {} {} '.format(*ROT)): - return BOILERPLATE.format(src=src, - id=uuid4(), - height=height, - x=d[0], - y=d[1], - z=d[2], - x0=center[0], - y0=center[1], - z0=center[2], - fov=fov, - rot=rot) +def add_x3d_boilerplate( + src, + height=400, + center=(0, 0, 0), + d=(0, 0, 15), + fov=FOV, + rot="{} {} {} {} ".format(*ROT), +): -def x3d_display(shape, - vertex_shader=None, - fragment_shader=None, - export_edges=True, - color=(1,1,0), - specular_color=(1,1,1), - shininess=0.4, - transparency=0.4, - line_color=(0,0,0), - line_width=2., - mesh_quality=.3): + return BOILERPLATE.format( + src=src, + id=uuid4(), + height=height, + x=d[0], + y=d[1], + z=d[2], + x0=center[0], + y0=center[1], + z0=center[2], + fov=fov, + rot=rot, + ) - # Export to XML tag - exporter = X3DExporter(shape, - vertex_shader, - fragment_shader, - export_edges, - color, - specular_color, - shininess, - transparency, - line_color, - line_width, - mesh_quality) - exporter.compute() - x3d_str = exporter.to_x3dfile_string(shape_id=0) - xml_et = ElementTree.fromstring(x3d_str) - scene_tag = xml_et.find('./Scene') +def x3d_display( + shape, + vertex_shader=None, + fragment_shader=None, + export_edges=True, + color=(1, 1, 0), + specular_color=(1, 1, 1), + shininess=0.4, + transparency=0.4, + line_color=(0, 0, 0), + line_width=2.0, + mesh_quality=0.3, +): - # Viewport Parameters - bb = BoundBox._fromTopoDS(shape) - d = max(bb.xlen,bb.ylen,bb.zlen) - c = bb.center + # Export to XML tag + exporter = X3DExporter( + shape, + vertex_shader, + fragment_shader, + export_edges, + color, + specular_color, + shininess, + transparency, + line_color, + line_width, + mesh_quality, + ) - vec = gp_Vec(0,0,d/1.5/tan(FOV/2)) - quat = gp_Quaternion(*ROT) - vec = quat*(vec) + c.wrapped + exporter.compute() + x3d_str = exporter.to_x3dfile_string(shape_id=0) + xml_et = ElementTree.fromstring(x3d_str) + scene_tag = xml_et.find("./Scene") - # return boilerplate + Scene - return add_x3d_boilerplate(ElementTree.tostring(scene_tag).decode('utf-8'), - d=(vec.X(),vec.Y(),vec.Z()), - center=(c.x,c.y,c.z)) + # Viewport Parameters + bb = BoundBox._fromTopoDS(shape) + d = max(bb.xlen, bb.ylen, bb.zlen) + c = bb.center + + vec = gp_Vec(0, 0, d / 1.5 / tan(FOV / 2)) + quat = gp_Quaternion(*ROT) + vec = quat * (vec) + c.wrapped + + # return boilerplate + Scene + return add_x3d_boilerplate( + ElementTree.tostring(scene_tag).decode("utf-8"), + d=(vec.X(), vec.Y(), vec.Z()), + center=(c.x, c.y, c.z), + ) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index cbba68d7..caa83f69 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -3,84 +3,104 @@ from .geom import Vector, BoundBox, Plane import OCC.Core.TopAbs as ta # Tolopolgy type enum import OCC.Core.GeomAbs as ga # Geometry type enum -from OCC.Core.gp import (gp_Vec, gp_Pnt, gp_Ax1, gp_Ax2, gp_Ax3, gp_Dir, gp_Circ, - gp_Trsf, gp_Pln, gp_GTrsf, gp_Pnt2d, gp_Dir2d) +from OCC.Core.gp import ( + gp_Vec, + gp_Pnt, + gp_Ax1, + gp_Ax2, + gp_Ax3, + gp_Dir, + gp_Circ, + gp_Trsf, + gp_Pln, + gp_GTrsf, + gp_Pnt2d, + gp_Dir2d, +) # collection of pints (used for spline construction) from OCC.Core.TColgp import TColgp_HArray1OfPnt from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface -from OCC.Core.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex, - BRepBuilderAPI_MakeEdge, - BRepBuilderAPI_MakeFace, - BRepBuilderAPI_MakePolygon, - BRepBuilderAPI_MakeWire, - BRepBuilderAPI_Sewing, - BRepBuilderAPI_MakeSolid, - BRepBuilderAPI_Copy, - BRepBuilderAPI_GTransform, - BRepBuilderAPI_Transform, - BRepBuilderAPI_Transformed, - BRepBuilderAPI_RightCorner, - BRepBuilderAPI_RoundCorner) +from OCC.Core.BRepBuilderAPI import ( + BRepBuilderAPI_MakeVertex, + BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeFace, + BRepBuilderAPI_MakePolygon, + BRepBuilderAPI_MakeWire, + BRepBuilderAPI_Sewing, + BRepBuilderAPI_MakeSolid, + BRepBuilderAPI_Copy, + BRepBuilderAPI_GTransform, + BRepBuilderAPI_Transform, + BRepBuilderAPI_Transformed, + BRepBuilderAPI_RightCorner, + BRepBuilderAPI_RoundCorner, +) + # properties used to store mass calculation result from OCC.Core.GProp import GProp_GProps -from OCC.Core.BRepGProp import BRepGProp_Face, \ - brepgprop_LinearProperties, \ - brepgprop_SurfaceProperties, \ - brepgprop_VolumeProperties # used for mass calculation +from OCC.Core.BRepGProp import ( + BRepGProp_Face, + brepgprop_LinearProperties, + brepgprop_SurfaceProperties, + brepgprop_VolumeProperties, +) # used for mass calculation from OCC.Core.BRepLProp import BRepLProp_CLProps # local curve properties -from OCC.Core.BRepPrimAPI import (BRepPrimAPI_MakeBox, - BRepPrimAPI_MakeCone, - BRepPrimAPI_MakeCylinder, - BRepPrimAPI_MakeTorus, - BRepPrimAPI_MakeWedge, - BRepPrimAPI_MakePrism, - BRepPrimAPI_MakeRevol, - BRepPrimAPI_MakeSphere) +from OCC.Core.BRepPrimAPI import ( + BRepPrimAPI_MakeBox, + BRepPrimAPI_MakeCone, + BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakeTorus, + BRepPrimAPI_MakeWedge, + BRepPrimAPI_MakePrism, + BRepPrimAPI_MakeRevol, + BRepPrimAPI_MakeSphere, +) from OCC.Core.TopExp import TopExp_Explorer # Toplogy explorer -from OCC.Core.BRepTools import (breptools_UVBounds, - breptools_OuterWire) +from OCC.Core.BRepTools import breptools_UVBounds, breptools_OuterWire + # used for getting underlying geoetry -- is this equvalent to brep adaptor? from OCC.Core.BRep import BRep_Tool, BRep_Tool_Degenerated -from OCC.Core.TopoDS import (topods_Vertex, # downcasting functions - topods_Edge, - topods_Wire, - topods_Face, - topods_Shell, - topods_Compound, - topods_Solid) +from OCC.Core.TopoDS import ( + topods_Vertex, # downcasting functions + topods_Edge, + topods_Wire, + topods_Face, + topods_Shell, + topods_Compound, + topods_Solid, +) -from OCC.Core.TopoDS import (TopoDS_Compound, - TopoDS_Builder) +from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Builder from OCC.Core.GC import GC_MakeArcOfCircle # geometry construction from OCC.Core.GCE2d import GCE2d_MakeSegment -from OCC.Core.GeomAPI import (GeomAPI_Interpolate, - GeomAPI_ProjectPointOnSurf) +from OCC.Core.GeomAPI import GeomAPI_Interpolate, GeomAPI_ProjectPointOnSurf from OCC.Core.BRepFill import brepfill_Shell, brepfill_Face -from OCC.Core.BRepAlgoAPI import (BRepAlgoAPI_Common, - BRepAlgoAPI_Fuse, - BRepAlgoAPI_Cut) +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Fuse, BRepAlgoAPI_Cut from OCC.Core.Geom import Geom_ConicalSurface, Geom_CylindricalSurface from OCC.Core.Geom2d import Geom2d_Line from OCC.Core.BRepLib import breplib_BuildCurves3d -from OCC.Core.BRepOffsetAPI import (BRepOffsetAPI_ThruSections, - BRepOffsetAPI_MakePipeShell, - BRepOffsetAPI_MakeThickSolid) +from OCC.Core.BRepOffsetAPI import ( + BRepOffsetAPI_ThruSections, + BRepOffsetAPI_MakePipeShell, + BRepOffsetAPI_MakeThickSolid, +) -from OCC.Core.BRepFilletAPI import (BRepFilletAPI_MakeChamfer, - BRepFilletAPI_MakeFillet) +from OCC.Core.BRepFilletAPI import BRepFilletAPI_MakeChamfer, BRepFilletAPI_MakeFillet -from OCC.Core.TopTools import (TopTools_IndexedDataMapOfShapeListOfShape, - TopTools_ListOfShape) +from OCC.Core.TopTools import ( + TopTools_IndexedDataMapOfShapeListOfShape, + TopTools_ListOfShape, +) from OCC.Core.TopExp import topexp_MapShapesAndAncestors @@ -101,10 +121,7 @@ from OCC.Core.LocOpe import LocOpe_DPrism from OCC.Core.BRepCheck import BRepCheck_Analyzer -from OCC.Core.Addons import (text_to_brep, - Font_FA_Regular, - Font_FA_Italic, - Font_FA_Bold) +from OCC.Core.Addons import text_to_brep, Font_FA_Regular, Font_FA_Italic, Font_FA_Bold from OCC.Core.BRepFeat import BRepFeat_MakeDPrism @@ -115,75 +132,81 @@ from functools import reduce import warnings TOLERANCE = 1e-6 -DEG2RAD = 2 * pi / 360. +DEG2RAD = 2 * pi / 360.0 HASH_CODE_MAX = 2147483647 # max 32bit signed int, required by OCC.Core.HashCode -shape_LUT = \ - {ta.TopAbs_VERTEX: 'Vertex', - ta.TopAbs_EDGE: 'Edge', - ta.TopAbs_WIRE: 'Wire', - ta.TopAbs_FACE: 'Face', - ta.TopAbs_SHELL: 'Shell', - ta.TopAbs_SOLID: 'Solid', - ta.TopAbs_COMPOUND: 'Compound'} +shape_LUT = { + ta.TopAbs_VERTEX: "Vertex", + ta.TopAbs_EDGE: "Edge", + ta.TopAbs_WIRE: "Wire", + ta.TopAbs_FACE: "Face", + ta.TopAbs_SHELL: "Shell", + ta.TopAbs_SOLID: "Solid", + ta.TopAbs_COMPOUND: "Compound", +} -shape_properties_LUT = \ - {ta.TopAbs_VERTEX: None, - ta.TopAbs_EDGE: brepgprop_LinearProperties, - ta.TopAbs_WIRE: brepgprop_LinearProperties, - ta.TopAbs_FACE: brepgprop_SurfaceProperties, - ta.TopAbs_SHELL: brepgprop_SurfaceProperties, - ta.TopAbs_SOLID: brepgprop_VolumeProperties, - ta.TopAbs_COMPOUND: brepgprop_VolumeProperties} +shape_properties_LUT = { + ta.TopAbs_VERTEX: None, + ta.TopAbs_EDGE: brepgprop_LinearProperties, + ta.TopAbs_WIRE: brepgprop_LinearProperties, + ta.TopAbs_FACE: brepgprop_SurfaceProperties, + ta.TopAbs_SHELL: brepgprop_SurfaceProperties, + ta.TopAbs_SOLID: brepgprop_VolumeProperties, + ta.TopAbs_COMPOUND: brepgprop_VolumeProperties, +} inverse_shape_LUT = {v: k for k, v in shape_LUT.items()} -downcast_LUT = \ - {ta.TopAbs_VERTEX: topods_Vertex, - ta.TopAbs_EDGE: topods_Edge, - ta.TopAbs_WIRE: topods_Wire, - ta.TopAbs_FACE: topods_Face, - ta.TopAbs_SHELL: topods_Shell, - ta.TopAbs_SOLID: topods_Solid, - ta.TopAbs_COMPOUND: topods_Compound} +downcast_LUT = { + ta.TopAbs_VERTEX: topods_Vertex, + ta.TopAbs_EDGE: topods_Edge, + ta.TopAbs_WIRE: topods_Wire, + ta.TopAbs_FACE: topods_Face, + ta.TopAbs_SHELL: topods_Shell, + ta.TopAbs_SOLID: topods_Solid, + ta.TopAbs_COMPOUND: topods_Compound, +} -geom_LUT = \ - {ta.TopAbs_VERTEX: 'Vertex', - ta.TopAbs_EDGE: BRepAdaptor_Curve, - ta.TopAbs_WIRE: 'Wire', - ta.TopAbs_FACE: BRepAdaptor_Surface, - ta.TopAbs_SHELL: 'Shell', - ta.TopAbs_SOLID: 'Solid', - ta.TopAbs_COMPOUND: 'Compound'} +geom_LUT = { + ta.TopAbs_VERTEX: "Vertex", + ta.TopAbs_EDGE: BRepAdaptor_Curve, + ta.TopAbs_WIRE: "Wire", + ta.TopAbs_FACE: BRepAdaptor_Surface, + ta.TopAbs_SHELL: "Shell", + ta.TopAbs_SOLID: "Solid", + ta.TopAbs_COMPOUND: "Compound", +} -geom_LUT_FACE = \ - {ga.GeomAbs_Plane : 'PLANE', - ga.GeomAbs_Cylinder : 'CYLINDER', - ga.GeomAbs_Cone : 'CONE', - ga.GeomAbs_Sphere : 'SPHERE', - ga.GeomAbs_Torus : 'TORUS', - ga.GeomAbs_BezierSurface : 'BEZIER', - ga.GeomAbs_BSplineSurface : 'BSPLINE', - ga.GeomAbs_SurfaceOfRevolution : 'REVOLUTION', - ga.GeomAbs_SurfaceOfExtrusion : 'EXTRUSION', - ga.GeomAbs_OffsetSurface : 'OFFSET', - ga.GeomAbs_OtherSurface : 'OTHER'} +geom_LUT_FACE = { + ga.GeomAbs_Plane: "PLANE", + ga.GeomAbs_Cylinder: "CYLINDER", + ga.GeomAbs_Cone: "CONE", + ga.GeomAbs_Sphere: "SPHERE", + ga.GeomAbs_Torus: "TORUS", + ga.GeomAbs_BezierSurface: "BEZIER", + ga.GeomAbs_BSplineSurface: "BSPLINE", + ga.GeomAbs_SurfaceOfRevolution: "REVOLUTION", + ga.GeomAbs_SurfaceOfExtrusion: "EXTRUSION", + ga.GeomAbs_OffsetSurface: "OFFSET", + ga.GeomAbs_OtherSurface: "OTHER", +} -geom_LUT_EDGE = \ - {ga.GeomAbs_Line : 'LINE', - ga.GeomAbs_Circle : 'CIRCLE', - ga.GeomAbs_Ellipse : 'ELLIPSE', - ga.GeomAbs_Hyperbola : 'HYPERBOLA', - ga.GeomAbs_Parabola : 'PARABOLA', - ga.GeomAbs_BezierCurve : 'BEZIER', - ga.GeomAbs_BSplineCurve : 'BSPLINE', - ga.GeomAbs_OtherCurve : 'OTHER'} +geom_LUT_EDGE = { + ga.GeomAbs_Line: "LINE", + ga.GeomAbs_Circle: "CIRCLE", + ga.GeomAbs_Ellipse: "ELLIPSE", + ga.GeomAbs_Hyperbola: "HYPERBOLA", + ga.GeomAbs_Parabola: "PARABOLA", + ga.GeomAbs_BezierCurve: "BEZIER", + ga.GeomAbs_BSplineCurve: "BSPLINE", + ga.GeomAbs_OtherCurve: "OTHER", +} def downcast(topods_obj): - ''' + """ Downcasts a TopoDS object to suitable specialized type - ''' + """ return downcast_LUT[topods_obj.ShapeType()](topods_obj) @@ -204,46 +227,46 @@ class Shape(object): def clean(self): """Experimental clean using ShapeUpgrade""" - upgrader = ShapeUpgrade_UnifySameDomain( - self.wrapped, True, True, True) + upgrader = ShapeUpgrade_UnifySameDomain(self.wrapped, True, True, True) upgrader.Build() return self.cast(upgrader.Shape()) - + def fix(self): """Try to fix shape if not valid""" if not BRepCheck_Analyzer(self.wrapped).IsValid(): sf = ShapeFix_Shape(self.wrapped) sf.Perform() fixed = downcast(sf.Shape()) - + return self.cast(fixed) - + return self @classmethod def cast(cls, obj, forConstruction=False): "Returns the right type of wrapper, given a FreeCAD object" - + tr = None # define the shape lookup table for casting - constructor_LUT = {ta.TopAbs_VERTEX: Vertex, - ta.TopAbs_EDGE: Edge, - ta.TopAbs_WIRE: Wire, - ta.TopAbs_FACE: Face, - ta.TopAbs_SHELL: Shell, - ta.TopAbs_SOLID: Solid, - ta.TopAbs_COMPOUND: Compound} + constructor_LUT = { + ta.TopAbs_VERTEX: Vertex, + ta.TopAbs_EDGE: Edge, + ta.TopAbs_WIRE: Wire, + ta.TopAbs_FACE: Face, + ta.TopAbs_SHELL: Shell, + ta.TopAbs_SOLID: Solid, + ta.TopAbs_COMPOUND: Compound, + } t = obj.ShapeType() # NB downcast is nedded to handly TopoDS_Shape types tr = constructor_LUT[t](downcast(obj)) tr.forConstruction = forConstruction - return tr - + def exportStl(self, fileName, precision=1e-5): mesh = BRepMesh_IncrementalMesh(self.wrapped, precision, True) @@ -266,7 +289,7 @@ class Shape(object): """ return breptools_Write(self.wrapped, fileName) - + def geomType(self): """ Gets the underlying geometry type @@ -300,7 +323,7 @@ class Shape(object): return geom_LUT_EDGE[tr(self.wrapped).GetType()] else: return geom_LUT_FACE[tr(self.wrapped).GetType()] - + def hashCode(self): return self.wrapped.HashCode(HASH_CODE_MAX) @@ -332,8 +355,7 @@ class Shape(object): basePointVector = Vector(basePointVector) T = gp_Trsf() - T.SetMirror(gp_Ax2(gp_Pnt(*basePointVector.toTuple()), - mirrorPlaneNormalVector)) + T.SetMirror(gp_Ax2(gp_Pnt(*basePointVector.toTuple()), mirrorPlaneNormalVector)) return self._apply_transform(T) @@ -341,15 +363,14 @@ class Shape(object): def _center_of_mass(shape): Properties = GProp_GProps() - brepgprop_VolumeProperties(shape, - Properties) + brepgprop_VolumeProperties(shape, Properties) return Vector(Properties.CentreOfMass()) def Center(self): - ''' + """ Center of mass - ''' + """ return Shape.centerOfMass(self) @@ -364,14 +385,15 @@ class Shape(object): :param objects: a list of objects with mass """ total_mass = sum(Shape.computeMass(o) for o in objects) - weighted_centers = [Shape.centerOfMass(o).multiply( - Shape.computeMass(o)) for o in objects] + weighted_centers = [ + Shape.centerOfMass(o).multiply(Shape.computeMass(o)) for o in objects + ] sum_wc = weighted_centers[0] for wc in weighted_centers[1:]: sum_wc = sum_wc.add(wc) - return Vector(sum_wc.multiply(1. / total_mass)) + return Vector(sum_wc.multiply(1.0 / total_mass)) @staticmethod def computeMass(obj): @@ -419,7 +441,7 @@ class Shape(object): for wc in weighted_centers[1:]: sum_wc = sum_wc.add(wc) - return Vector(sum_wc.multiply(1. / total_mass)) + return Vector(sum_wc.multiply(1.0 / total_mass)) def Closed(self): return self.wrapped.Closed() @@ -442,30 +464,29 @@ class Shape(object): def Vertices(self): - return [Vertex(i) for i in self._entities('Vertex')] + return [Vertex(i) for i in self._entities("Vertex")] def Edges(self): - return [Edge(i) for i in self._entities('Edge') if not BRep_Tool_Degenerated(i)] + return [Edge(i) for i in self._entities("Edge") if not BRep_Tool_Degenerated(i)] def Compounds(self): - return [Compound(i) for i in self._entities('Compound')] + return [Compound(i) for i in self._entities("Compound")] def Wires(self): - return [Wire(i) for i in self._entities('Wire')] + return [Wire(i) for i in self._entities("Wire")] def Faces(self): - return [Face(i) for i in self._entities('Face')] + return [Face(i) for i in self._entities("Face")] def Shells(self): - return [Shell(i) for i in self._entities('Shell')] + return [Shell(i) for i in self._entities("Shell")] def Solids(self): - return [Solid(i) for i in self._entities('Solid')] + return [Solid(i) for i in self._entities("Solid")] def Area(self): Properties = GProp_GProps() - brepgprop_SurfaceProperties(self.wrapped, - Properties) + brepgprop_SurfaceProperties(self.wrapped, Properties) return Properties.Mass() @@ -475,9 +496,7 @@ class Shape(object): def _apply_transform(self, T): - return Shape.cast(BRepBuilderAPI_Transform(self.wrapped, - T, - True).Shape()) + return Shape.cast(BRepBuilderAPI_Transform(self.wrapped, T, True).Shape()) def rotate(self, startVector, endVector, angleDegrees): """ @@ -494,9 +513,10 @@ class Shape(object): endVector = Vector(endVector) T = gp_Trsf() - T.SetRotation(gp_Ax1(startVector.toPnt(), - (endVector - startVector).toDir()), - angleDegrees * DEG2RAD) + T.SetRotation( + gp_Ax1(startVector.toPnt(), (endVector - startVector).toDir()), + angleDegrees * DEG2RAD, + ) return self._apply_transform(T) @@ -513,8 +533,7 @@ class Shape(object): def scale(self, factor): T = gp_Trsf() - T.SetScale(gp_Pnt(), - factor) + T.SetScale(gp_Pnt(), factor) return self._apply_transform(T) @@ -529,8 +548,9 @@ class Shape(object): with all objects keeping their type """ - r = Shape.cast(BRepBuilderAPI_Transform(self.wrapped, - tMatrix.wrapped.Trsf()).Shape()) + r = Shape.cast( + BRepBuilderAPI_Transform(self.wrapped, tMatrix.wrapped.Trsf()).Shape() + ) r.forConstruction = self.forConstruction return r @@ -548,9 +568,9 @@ class Shape(object): If your transformation is only translation and rotation, it is safer to use transformShape, which doesnt change the underlying type of the geometry, but cannot handle skew transformations """ - r = Shape.cast(BRepBuilderAPI_GTransform(self.wrapped, - tMatrix.wrapped, - True).Shape()) + r = Shape.cast( + BRepBuilderAPI_GTransform(self.wrapped, tMatrix.wrapped, True).Shape() + ) r.forConstruction = self.forConstruction return r @@ -562,8 +582,7 @@ class Shape(object): """ Remove a shape from another one """ - return Shape.cast(BRepAlgoAPI_Cut(self.wrapped, - toCut.wrapped).Shape()) + return Shape.cast(BRepAlgoAPI_Cut(self.wrapped, toCut.wrapped).Shape()) def fuse(self, toFuse): """ @@ -582,8 +601,7 @@ class Shape(object): """ Construct shape intersection """ - return Shape.cast(BRepAlgoAPI_Common(self.wrapped, - toIntersect.wrapped).Shape()) + return Shape.cast(BRepAlgoAPI_Common(self.wrapped, toIntersect.wrapped).Shape()) def _repr_html_(self): """ @@ -591,6 +609,7 @@ class Shape(object): """ from .jupyter_tools import x3d_display + return x3d_display(self.wrapped, export_edges=True) @@ -611,9 +630,7 @@ class Vertex(Shape): def toTuple(self): geom_point = BRep_Tool.Pnt(self.wrapped) - return (geom_point.X(), - geom_point.Y(), - geom_point.Z()) + return (geom_point.X(), geom_point.Y(), geom_point.Z()) def Center(self): """ @@ -624,12 +641,10 @@ class Vertex(Shape): @classmethod def makeVertex(cls, x, y, z): - return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z) - ).Vertex()) + return cls(BRepBuilderAPI_MakeVertex(gp_Pnt(x, y, z)).Vertex()) class Mixin1D(object): - def Length(self): Properties = GProp_GProps() @@ -690,8 +705,8 @@ class Edge(Shape, Mixin1D): curve = self._geomAdaptor() umin, umax = curve.FirstParameter(), curve.LastParameter() - umid = (1-locationParam)*umin + locationParam*umax - + umid = (1 - locationParam) * umin + locationParam * umax + curve_props = BRepLProp_CLProps(curve, 2, curve.Tolerance()) curve_props.SetParameter(umid) @@ -704,35 +719,32 @@ class Edge(Shape, Mixin1D): def Center(self): Properties = GProp_GProps() - brepgprop_LinearProperties(self.wrapped, - Properties) + brepgprop_LinearProperties(self.wrapped, Properties) return Vector(Properties.CentreOfMass()) @classmethod - def makeCircle(cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angle1=360.0, angle2=360): + def makeCircle( + cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angle1=360.0, angle2=360 + ): """ """ pnt = Vector(pnt) dir = Vector(dir) - circle_gp = gp_Circ(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius) + circle_gp = gp_Circ(gp_Ax2(pnt.toPnt(), dir.toDir()), radius) if angle1 == angle2: # full circle case return cls(BRepBuilderAPI_MakeEdge(circle_gp).Edge()) else: # arc case - circle_geom = GC_MakeArcOfCircle(circle_gp, - angle1 * DEG2RAD, - angle2 * DEG2RAD, - True).Value() + circle_geom = GC_MakeArcOfCircle( + circle_gp, angle1 * DEG2RAD, angle2 * DEG2RAD, True + ).Value() return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) @classmethod - def makeSpline(cls, listOfVector, tangents=None, periodic=False, - tol = 1e-6): + def makeSpline(cls, listOfVector, tangents=None, periodic=False, tol=1e-6): """ Interpolate a spline through the provided points. :param cls: @@ -744,12 +756,12 @@ class Edge(Shape, Mixin1D): """ pnts = TColgp_HArray1OfPnt(1, len(listOfVector)) for ix, v in enumerate(listOfVector): - pnts.SetValue(ix+1, v.toPnt()) + pnts.SetValue(ix + 1, v.toPnt()) spline_builder = GeomAPI_Interpolate(pnts, periodic, tol) if tangents: - v1,v2 = tangents - spline_builder.Load(v1.wrapped,v2.wrapped) + v1, v2 = tangents + spline_builder.Load(v1.wrapped, v2.wrapped) spline_builder.Perform() spline_geom = spline_builder.Curve() @@ -766,9 +778,7 @@ class Edge(Shape, Mixin1D): :param v3: end vector :return: an edge object through the three points """ - circle_geom = GC_MakeArcOfCircle(v1.toPnt(), - v2.toPnt(), - v3.toPnt()).Value() + circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2.toPnt(), v3.toPnt()).Value() return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) @@ -780,8 +790,7 @@ class Edge(Shape, Mixin1D): :param v2: Vector that represents the second point :return: A linear edge between the two provided points """ - return cls(BRepBuilderAPI_MakeEdge(v1.toPnt(), - v2.toPnt()).Edge()) + return cls(BRepBuilderAPI_MakeEdge(v1.toPnt(), v2.toPnt()).Edge()) class Wire(Shape, Mixin1D): @@ -819,17 +828,23 @@ class Wire(Shape, Mixin1D): :BRepBuilderAPI_NonManifoldWire = 3 """ wire_builder = BRepBuilderAPI_MakeWire() - + edges_list = TopTools_ListOfShape() for e in listOfEdges: - edges_list.Append(e.wrapped) + edges_list.Append(e.wrapped) wire_builder.Add(edges_list) - if wire_builder.Error()!=0: - w1 = 'BRepBuilderAPI_MakeWire::IsDone(): returns true if this algorithm contains a valid wire. IsDone returns false if: there are no edges in the wire, or the last edge which you tried to add was not connectable = '+ str(wire_builder.IsDone()) - w2 = 'BRepBuilderAPI_MakeWire::Error(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = ' + str(wire_builder.Error()) + if wire_builder.Error() != 0: + w1 = ( + "BRepBuilderAPI_MakeWire::IsDone(): returns true if this algorithm contains a valid wire. IsDone returns false if: there are no edges in the wire, or the last edge which you tried to add was not connectable = " + + str(wire_builder.IsDone()) + ) + w2 = ( + "BRepBuilderAPI_MakeWire::Error(): returns the construction status. BRepBuilderAPI_WireDone if the wire is built, or another value of the BRepBuilderAPI_WireError enumeration indicating why the construction failed = " + + str(wire_builder.Error()) + ) warnings.warn(w1) warnings.warn(w2) - + return cls(wire_builder.Wire()) @classmethod @@ -860,8 +875,16 @@ class Wire(Shape, Mixin1D): return w @classmethod - def makeHelix(cls, pitch, height, radius, center=Vector(0, 0, 0), - dir=Vector(0, 0, 1), angle=360.0, lefthand=False): + def makeHelix( + cls, + pitch, + height, + radius, + center=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + angle=360.0, + lefthand=False, + ): """ Make a helix with a given pitch, height and radius By default a cylindrical surface is used to create the helix. If @@ -869,13 +892,14 @@ class Wire(Shape, Mixin1D): """ # 1. build underlying cylindrical/conical surface - if angle == 360.: - geom_surf = Geom_CylindricalSurface(gp_Ax3(center.toPnt(), dir.toDir()), - radius) + if angle == 360.0: + geom_surf = Geom_CylindricalSurface( + gp_Ax3(center.toPnt(), dir.toDir()), radius + ) else: - geom_surf = Geom_ConicalSurface(gp_Ax3(center.toPnt(), dir.toDir()), - angle * DEG2RAD, - radius) + geom_surf = Geom_ConicalSurface( + gp_Ax3(center.toPnt(), dir.toDir()), angle * DEG2RAD, radius + ) # 2. construct an semgent in the u,v domain if lefthand: @@ -885,8 +909,8 @@ class Wire(Shape, Mixin1D): # 3. put it together into a wire n_turns = height / pitch - u_start = geom_line.Value(0.) - u_stop = geom_line.Value(sqrt(n_turns * ((2 * pi)**2 + pitch**2))) + u_start = geom_line.Value(0.0) + u_stop = geom_line.Value(sqrt(n_turns * ((2 * pi) ** 2 + pitch ** 2))) geom_seg = GCE2d_MakeSegment(u_start, u_stop).Value() e = BRepBuilderAPI_MakeEdge(geom_seg, geom_surf).Edge() @@ -940,8 +964,7 @@ class Face(Shape): v = 0.5 * (v0 + v1) else: # project point on surface - projector = GeomAPI_ProjectPointOnSurf(locationVector.toPnt(), - surface) + projector = GeomAPI_ProjectPointOnSurf(locationVector.toPnt(), surface) u, v = projector.LowerDistanceParameters() @@ -954,19 +977,18 @@ class Face(Shape): def Center(self): Properties = GProp_GProps() - brepgprop_SurfaceProperties(self.wrapped, - Properties) + brepgprop_SurfaceProperties(self.wrapped, Properties) return Vector(Properties.CentreOfMass()) - + def outerWire(self): - + return self.cast(breptools_OuterWire(self.wrapped)) - + def innerWires(self): - + outer = self.outerWire() - + return [w for w in self.Wires() if not w.isSame(outer)] @classmethod @@ -976,11 +998,11 @@ class Face(Shape): pln_geom = gp_Pln(basePnt.toPnt(), dir.toDir()) - return cls(BRepBuilderAPI_MakeFace(pln_geom, - -width * 0.5, - width * 0.5, - -length * 0.5, - length * 0.5).Face()) + return cls( + BRepBuilderAPI_MakeFace( + pln_geom, -width * 0.5, width * 0.5, -length * 0.5, length * 0.5 + ).Face() + ) @classmethod def makeRuledSurface(cls, edgeOrWire1, edgeOrWire2, dist=None): @@ -991,26 +1013,24 @@ class Face(Shape): """ if isinstance(edgeOrWire1, Wire): - return cls.cast(brepfill_Shell(edgeOrWire1.wrapped, - edgeOrWire1.wrapped)) + return cls.cast(brepfill_Shell(edgeOrWire1.wrapped, edgeOrWire1.wrapped)) else: - return cls.cast(brepfill_Face(edgeOrWire1.wrapped, - edgeOrWire1.wrapped)) + return cls.cast(brepfill_Face(edgeOrWire1.wrapped, edgeOrWire1.wrapped)) @classmethod def makeFromWires(cls, outerWire, innerWires=[]): - ''' + """ Makes a planar face from one or more wires - ''' - - face_builder = BRepBuilderAPI_MakeFace(outerWire.wrapped,True) + """ + + face_builder = BRepBuilderAPI_MakeFace(outerWire.wrapped, True) for w in innerWires: face_builder.Add(w.wrapped) - + face_builder.Build() face = face_builder.Shape() - + return cls.cast(face).fix() @@ -1028,18 +1048,17 @@ class Shell(Shape): shell_builder.Add(face.wrapped) shell_builder.Perform() - + return cls.cast(shell_builder.SewedShape()) class Mixin3D(object): - def tessellate(self, tolerance): tess = Tesselator(self.wrapped) tess.Compute(compute_edges=True, mesh_quality=tolerance) vertices = [] - indexes = [] + indexes = [] # add vertices for i_vert in range(tess.ObjGetVertexCount()): @@ -1080,10 +1099,9 @@ class Mixin3D(object): # make a edge --> faces mapping edge_face_map = TopTools_IndexedDataMapOfShapeListOfShape() - topexp_MapShapesAndAncestors(self.wrapped, - ta.TopAbs_EDGE, - ta.TopAbs_FACE, - edge_face_map) + topexp_MapShapesAndAncestors( + self.wrapped, ta.TopAbs_EDGE, ta.TopAbs_FACE, edge_face_map + ) # note: we prefer 'length' word to 'radius' as opposed to FreeCAD's API chamfer_builder = BRepFilletAPI_MakeChamfer(self.wrapped) @@ -1097,10 +1115,9 @@ class Mixin3D(object): for e in nativeEdges: face = edge_face_map.FindFromKey(e).First() - chamfer_builder.Add(d1, - d2, - e, - topods_Face(face)) # NB: edge_face_map return a generic TopoDS_Shape + chamfer_builder.Add( + d1, d2, e, topods_Face(face) + ) # NB: edge_face_map return a generic TopoDS_Shape return self.__class__(chamfer_builder.Shape()) def shell(self, faceList, thickness, tolerance=0.0001): @@ -1117,10 +1134,9 @@ class Mixin3D(object): for f in faceList: occ_faces_list.Append(f.wrapped) - shell_builder = BRepOffsetAPI_MakeThickSolid(self.wrapped, - occ_faces_list, - thickness, - tolerance) + shell_builder = BRepOffsetAPI_MakeThickSolid( + self.wrapped, occ_faces_list, thickness, tolerance + ) shell_builder.Build() @@ -1141,29 +1157,29 @@ class Mixin3D(object): solid_classifier = BRepClass3d_SolidClassifier(self.wrapped) solid_classifier.Perform(gp_Pnt(*point), tolerance) - return (solid_classifier.State() == ta.TopAbs_IN or - solid_classifier.IsOnAFace()) + return solid_classifier.State() == ta.TopAbs_IN or solid_classifier.IsOnAFace() class Solid(Shape, Mixin3D): """ a single solid """ - + @classmethod def isSolid(cls, obj): """ Returns true if the object is a FreeCAD solid, false otherwise """ - if hasattr(obj, 'ShapeType'): - if obj.ShapeType == 'Solid' or \ - (obj.ShapeType == 'Compound' and len(obj.Solids) > 0): + if hasattr(obj, "ShapeType"): + if obj.ShapeType == "Solid" or ( + obj.ShapeType == "Compound" and len(obj.Solids) > 0 + ): return True return False - + @classmethod def makeSolid(cls, shell): - + return cls(BRepBuilderAPI_MakeSolid(shell.wrapped).Solid()) @classmethod @@ -1172,53 +1188,77 @@ class Solid(Shape, Mixin3D): makeBox(length,width,height,[pnt,dir]) -- Make a box located in pnt with the dimensions (length,width,height) By default pnt=Vector(0,0,0) and dir=Vector(0,0,1)' """ - return cls(BRepPrimAPI_MakeBox(gp_Ax2(pnt.toPnt(), - dir.toDir()), - length, - width, - height).Shape()) + return cls( + BRepPrimAPI_MakeBox( + gp_Ax2(pnt.toPnt(), dir.toDir()), length, width, height + ).Shape() + ) @classmethod - def makeCone(cls, radius1, radius2, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360): + def makeCone( + cls, + radius1, + radius2, + height, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + angleDegrees=360, + ): """ Make a cone with given radii and height By default pnt=Vector(0,0,0), dir=Vector(0,0,1) and angle=360' """ - return cls(BRepPrimAPI_MakeCone(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius1, - radius2, - height, - angleDegrees * DEG2RAD).Shape()) + return cls( + BRepPrimAPI_MakeCone( + gp_Ax2(pnt.toPnt(), dir.toDir()), + radius1, + radius2, + height, + angleDegrees * DEG2RAD, + ).Shape() + ) @classmethod - def makeCylinder(cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360): + def makeCylinder( + cls, radius, height, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees=360 + ): """ makeCylinder(radius,height,[pnt,dir,angle]) -- Make a cylinder with a given radius and height By default pnt=Vector(0,0,0),dir=Vector(0,0,1) and angle=360' """ - return cls(BRepPrimAPI_MakeCylinder(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius, - height, - angleDegrees * DEG2RAD).Shape()) + return cls( + BRepPrimAPI_MakeCylinder( + gp_Ax2(pnt.toPnt(), dir.toDir()), radius, height, angleDegrees * DEG2RAD + ).Shape() + ) @classmethod - def makeTorus(cls, radius1, radius2, pnt=None, dir=None, angleDegrees1=None, angleDegrees2=None): + def makeTorus( + cls, + radius1, + radius2, + pnt=None, + dir=None, + angleDegrees1=None, + angleDegrees2=None, + ): """ makeTorus(radius1,radius2,[pnt,dir,angle1,angle2,angle]) -- Make a torus with agiven radii and angles By default pnt=Vector(0,0,0),dir=Vector(0,0,1),angle1=0 ,angle1=360 and angle=360' """ - return cls(BRepPrimAPI_MakeTorus(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius1, - radius2, - angleDegrees1 * DEG2RAD, - angleDegrees2 * DEG2RAD).Shape()) + return cls( + BRepPrimAPI_MakeTorus( + gp_Ax2(pnt.toPnt(), dir.toDir()), + radius1, + radius2, + angleDegrees1 * DEG2RAD, + angleDegrees2 * DEG2RAD, + ).Shape() + ) @classmethod def makeLoft(cls, listOfWire, ruled=False): @@ -1240,35 +1280,52 @@ class Solid(Shape, Mixin3D): return cls(loft_builder.Shape()) @classmethod - def makeWedge(cls, dx, dy, dz, xmin, zmin, xmax, zmax, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1)): + def makeWedge( + cls, + dx, + dy, + dz, + xmin, + zmin, + xmax, + zmax, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + ): """ Make a wedge located in pnt By default pnt=Vector(0,0,0) and dir=Vector(0,0,1) """ - return cls(BRepPrimAPI_MakeWedge( - gp_Ax2(pnt.toPnt(), - dir.toDir()), - dx, - dy, - dz, - xmin, - zmin, - xmax, - zmax).Solid()) + return cls( + BRepPrimAPI_MakeWedge( + gp_Ax2(pnt.toPnt(), dir.toDir()), dx, dy, dz, xmin, zmin, xmax, zmax + ).Solid() + ) @classmethod - def makeSphere(cls, radius, pnt=Vector(0, 0, 0), dir=Vector(0, 0, 1), angleDegrees1=0, angleDegrees2=90, angleDegrees3=360): + def makeSphere( + cls, + radius, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + angleDegrees1=0, + angleDegrees2=90, + angleDegrees3=360, + ): """ Make a sphere with a given radius By default pnt=Vector(0,0,0), dir=Vector(0,0,1), angle1=0, angle2=90 and angle3=360 """ - return cls(BRepPrimAPI_MakeSphere(gp_Ax2(pnt.toPnt(), - dir.toDir()), - radius, - angleDegrees1 * DEG2RAD, - angleDegrees2 * DEG2RAD, - angleDegrees3 * DEG2RAD).Shape()) + return cls( + BRepPrimAPI_MakeSphere( + gp_Ax2(pnt.toPnt(), dir.toDir()), + radius, + angleDegrees1 * DEG2RAD, + angleDegrees2 * DEG2RAD, + angleDegrees3 * DEG2RAD, + ).Shape() + ) @classmethod def _extrudeAuxSpine(cls, wire, spine, auxSpine): @@ -1283,7 +1340,9 @@ class Solid(Shape, Mixin3D): return extrude_builder.Shape() @classmethod - def extrudeLinearWithRotation(cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees): + def extrudeLinearWithRotation( + cls, outerWire, innerWires, vecCenter, vecNormal, angleDegrees + ): """ Creates a 'twisted prism' by extruding, while simultaneously rotating around the extrusion vector. @@ -1306,26 +1365,25 @@ class Solid(Shape, Mixin3D): """ # make straight spine straight_spine_e = Edge.makeLine(vecCenter, vecCenter.add(vecNormal)) - straight_spine_w = Wire.combine([straight_spine_e, ]).wrapped + straight_spine_w = Wire.combine([straight_spine_e,]).wrapped # make an auxliliary spine - pitch = 360. / angleDegrees * vecNormal.Length + pitch = 360.0 / angleDegrees * vecNormal.Length radius = 1 - aux_spine_w = Wire.makeHelix(pitch, - vecNormal.Length, - radius, - center=vecCenter, - dir=vecNormal).wrapped + aux_spine_w = Wire.makeHelix( + pitch, vecNormal.Length, radius, center=vecCenter, dir=vecNormal + ).wrapped # extrude the outer wire - outer_solid = cls._extrudeAuxSpine(outerWire.wrapped, - straight_spine_w, - aux_spine_w) + outer_solid = cls._extrudeAuxSpine( + outerWire.wrapped, straight_spine_w, aux_spine_w + ) # extrude inner wires - inner_solids = [cls._extrudeAuxSpine(w.wrapped, - straight_spine_w. - aux_spine_w) for w in innerWires] + inner_solids = [ + cls._extrudeAuxSpine(w.wrapped, straight_spine_w.aux_spine_w) + for w in innerWires + ] # combine the inner solids into compund inner_comp = Compound._makeCompound(inner_solids) @@ -1358,21 +1416,19 @@ class Solid(Shape, Mixin3D): reliable. """ - if taper==0: + if taper == 0: face = Face.makeFromWires(outerWire, innerWires) - prism_builder = BRepPrimAPI_MakePrism( - face.wrapped, vecNormal.wrapped, True) + prism_builder = BRepPrimAPI_MakePrism(face.wrapped, vecNormal.wrapped, True) else: face = Face.makeFromWires(outerWire) faceNormal = face.normalAt() - d = 1 if vecNormal.getAngle(faceNormal)<90 * DEG2RAD else -1 - prism_builder = LocOpe_DPrism(face.wrapped, - d * vecNormal.Length, - d * taper * DEG2RAD) + d = 1 if vecNormal.getAngle(faceNormal) < 90 * DEG2RAD else -1 + prism_builder = LocOpe_DPrism( + face.wrapped, d * vecNormal.Length, d * taper * DEG2RAD + ) return cls(prism_builder.Shape()) - @classmethod def revolve(cls, outerWire, innerWires, angleDegrees, axisStart, axisEnd): """ @@ -1404,20 +1460,28 @@ class Solid(Shape, Mixin3D): v1 = Vector(axisStart) v2 = Vector(axisEnd) v2 = v2 - v1 - revol_builder = BRepPrimAPI_MakeRevol(face.wrapped, - gp_Ax1(v1.toPnt(), v2.toDir()), - angleDegrees * DEG2RAD, - True) + revol_builder = BRepPrimAPI_MakeRevol( + face.wrapped, gp_Ax1(v1.toPnt(), v2.toDir()), angleDegrees * DEG2RAD, True + ) return cls(revol_builder.Shape()) - _transModeDict = {'transformed' : BRepBuilderAPI_Transformed, - 'round' : BRepBuilderAPI_RoundCorner, - 'right' : BRepBuilderAPI_RightCorner} + _transModeDict = { + "transformed": BRepBuilderAPI_Transformed, + "round": BRepBuilderAPI_RoundCorner, + "right": BRepBuilderAPI_RightCorner, + } @classmethod - def sweep(cls, outerWire, innerWires, path, makeSolid=True, isFrenet=False, - transitionMode='transformed'): + def sweep( + cls, + outerWire, + innerWires, + path, + makeSolid=True, + isFrenet=False, + transitionMode="transformed", + ): """ Attempt to sweep the list of wires into a prismatic solid along the provided path @@ -1431,11 +1495,11 @@ class Solid(Shape, Mixin3D): Possible values are {'transformed','round', 'right'} (default: 'right'). :return: a Solid object """ - if path.ShapeType() == 'Edge': - path = Wire.assembleEdges([path, ]) + if path.ShapeType() == "Edge": + path = Wire.assembleEdges([path,]) shapes = [] - for w in [outerWire]+innerWires: + for w in [outerWire] + innerWires: builder = BRepOffsetAPI_MakePipeShell(path.wrapped) builder.SetMode(isFrenet) builder.SetTransitionMode(cls._transModeDict[transitionMode]) @@ -1447,10 +1511,10 @@ class Solid(Shape, Mixin3D): shapes.append(cls(builder.Shape())) - rv,inner_shapes = shapes[0],shapes[1:] + rv, inner_shapes = shapes[0], shapes[1:] if inner_shapes: - inner_shapes = reduce(lambda a,b: a.fuse(b),inner_shapes) + inner_shapes = reduce(lambda a, b: a.fuse(b), inner_shapes) rv = rv.cut(inner_shapes) return rv @@ -1464,8 +1528,8 @@ class Solid(Shape, Mixin3D): :param path: The wire to sweep the face resulting from the wires over :return: a Solid object """ - if path.ShapeType() == 'Edge': - path = Wire.assembleEdges([path, ]) + if path.ShapeType() == "Edge": + path = Wire.assembleEdges([path,]) builder = BRepOffsetAPI_MakePipeShell(path.wrapped) @@ -1480,8 +1544,7 @@ class Solid(Shape, Mixin3D): return cls(builder.Shape()) - def dprism(self, basis, profiles, depth=None, taper=0, thruAll=True, - additive=True): + def dprism(self, basis, profiles, depth=None, taper=0, thruAll=True, additive=True): """ Make a prismatic feature (additive or subtractive) @@ -1496,13 +1559,10 @@ class Solid(Shape, Mixin3D): shape = self.wrapped basis = basis.wrapped for p in sorted_profiles: - face = Face.makeFromWires(p[0],p[1:]) - feat = BRepFeat_MakeDPrism(shape, - face.wrapped, - basis, - taper*DEG2RAD, - additive, - False) + face = Face.makeFromWires(p[0], p[1:]) + feat = BRepFeat_MakeDPrism( + shape, face.wrapped, basis, taper * DEG2RAD, additive, False + ) if thruAll: feat.PerformThruAll() @@ -1513,21 +1573,22 @@ class Solid(Shape, Mixin3D): return self.__class__(shape) + class Compound(Shape, Mixin3D): """ a collection of disconnected solids """ - + @staticmethod def _makeCompound(listOfShapes): - + comp = TopoDS_Compound() comp_builder = TopoDS_Builder() comp_builder.MakeCompound(comp) for s in listOfShapes: comp_builder.Add(comp, s) - + return comp @classmethod @@ -1539,39 +1600,51 @@ class Compound(Shape, Mixin3D): return cls(cls._makeCompound((s.wrapped for s in listOfShapes))) @classmethod - def makeText(cls, text, size, height, font="Arial", kind='regular', - halign='center', valign='center',position=Plane.XY()): + def makeText( + cls, + text, + size, + height, + font="Arial", + kind="regular", + halign="center", + valign="center", + position=Plane.XY(), + ): """ Create a 3D text """ - font_kind = {'regular' : Font_FA_Regular, - 'bold' : Font_FA_Bold, - 'italic' : Font_FA_Italic}[kind] + font_kind = { + "regular": Font_FA_Regular, + "bold": Font_FA_Bold, + "italic": Font_FA_Italic, + }[kind] text_flat = Shape(text_to_brep(text, font, font_kind, size, False)) bb = text_flat.BoundingBox() - + t = Vector() - - if halign == 'center': - t.x = -bb.xlen/2 - elif halign == 'right': + + if halign == "center": + t.x = -bb.xlen / 2 + elif halign == "right": t.x = -bb.xlen - - if valign == 'center': - t.y = -bb.ylen/2 - elif valign == 'top': + + if valign == "center": + t.y = -bb.ylen / 2 + elif valign == "top": t.y = -bb.ylen - + text_flat = text_flat.translate(t) - - vecNormal = text_flat.Faces()[0].normalAt()*height + + vecNormal = text_flat.Faces()[0].normalAt() * height text_3d = BRepPrimAPI_MakePrism(text_flat.wrapped, vecNormal.wrapped) return cls(text_3d.Shape()).transformShape(position.rG) + def sortWiresByBuildOrder(wireList, result={}): """Tries to determine how wires should be combined into faces. @@ -1591,12 +1664,14 @@ def sortWiresByBuildOrder(wireList, result={}): # check if we have something to sort at all if len(wireList) < 2: - return [wireList, ] + return [ + wireList, + ] # make a Face, NB: this might return a compound of faces faces = Face.makeFromWires(wireList[0], wireList[1:]) - - rv = [] + + rv = [] for face in faces.Faces(): rv.append([face.outerWire(),] + face.innerWires()) diff --git a/cadquery/selectors.py b/cadquery/selectors.py index 8ead7737..f4eb361d 100644 --- a/cadquery/selectors.py +++ b/cadquery/selectors.py @@ -21,9 +21,22 @@ import re import math from cadquery import Vector, Edge, Vertex, Face, Solid, Shell, Compound from collections import defaultdict -from pyparsing import Literal, Word, nums, Optional, Combine, oneOf, upcaseTokens,\ - CaselessLiteral, Group, infixNotation, opAssoc, Forward,\ - ZeroOrMore, Keyword +from pyparsing import ( + Literal, + Word, + nums, + Optional, + Combine, + oneOf, + upcaseTokens, + CaselessLiteral, + Group, + infixNotation, + opAssoc, + Forward, + ZeroOrMore, + Keyword, +) from functools import reduce @@ -81,7 +94,6 @@ class NearestToPointSelector(Selector): self.pnt = pnt def filter(self, objectList): - def dist(tShape): return tShape.Center().sub(Vector(*self.pnt)).Length # if tShape.ShapeType == 'Vertex': @@ -121,15 +133,18 @@ class BoxSelector(Selector): def isInsideBox(p): # using XOR for checking if x/y/z is in between regardless # of order of x/y/z0 and x/y/z1 - return ((p.x < x0) ^ (p.x < x1)) and \ - ((p.y < y0) ^ (p.y < y1)) and \ - ((p.z < z0) ^ (p.z < z1)) + return ( + ((p.x < x0) ^ (p.x < x1)) + and ((p.y < y0) ^ (p.y < y1)) + and ((p.z < z0) ^ (p.z < z1)) + ) for o in objectList: if self.test_boundingbox: bb = o.BoundingBox() - if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and \ - isInsideBox(Vector(bb.xmax, bb.ymax, bb.zmax)): + if isInsideBox(Vector(bb.xmin, bb.ymin, bb.zmin)) and isInsideBox( + Vector(bb.xmax, bb.ymax, bb.zmax) + ): result.append(o) else: if isInsideBox(o.Center()): @@ -168,7 +183,9 @@ class BaseDirSelector(Selector): if self.test(normal): r.append(o) - elif type(o) == Edge and (o.geomType() == 'LINE' or o.geomType() == 'PLANE'): + elif type(o) == Edge and ( + o.geomType() == "LINE" or o.geomType() == "PLANE" + ): # an edge is parallel to a direction if its underlying geometry is plane or line tangent = o.tangentAt() if self.test(tangent): @@ -247,8 +264,7 @@ class PerpendicularDirSelector(BaseDirSelector): def test(self, vec): angle = self.direction.getAngle(vec) - r = (abs(angle) < self.TOLERANCE) or ( - abs(angle - math.pi) < self.TOLERANCE) + r = (abs(angle) < self.TOLERANCE) or (abs(angle - math.pi) < self.TOLERANCE) return not r @@ -314,17 +330,16 @@ class DirectionMinMaxSelector(Selector): self.TOLERANCE = tolerance def filter(self, objectList): - def distance(tShape): return tShape.Center().dot(self.vector) # import OrderedDict from collections import OrderedDict + # make and distance to object dict objectDict = {distance(el): el for el in objectList} # transform it into an ordered dict - objectDict = OrderedDict(sorted(list(objectDict.items()), - key=lambda x: x[0])) + objectDict = OrderedDict(sorted(list(objectDict.items()), key=lambda x: x[0])) # find out the max/min distance if self.directionMax: @@ -370,8 +385,9 @@ class DirectionNthSelector(ParallelDirSelector): objectDict[round(distance(el), digits)].append(el) # choose the Nth unique rounded distance - nth_distance = sorted(list(objectDict.keys()), - reverse=not self.directionMax)[self.N] + nth_distance = sorted(list(objectDict.keys()), reverse=not self.directionMax)[ + self.N + ] # map back to original objects and return return objectDict[nth_distance] @@ -388,8 +404,9 @@ class BinarySelector(Selector): self.right = right def filter(self, objectList): - return self.filterResults(self.left.filter(objectList), - self.right.filter(objectList)) + return self.filterResults( + self.left.filter(objectList), self.right.filter(objectList) + ) def filterResults(self, r_left, r_right): raise NotImplementedError @@ -445,52 +462,56 @@ def _makeGrammar(): """ # float definition - point = Literal('.') - plusmin = Literal('+') | Literal('-') + point = Literal(".") + plusmin = Literal("+") | Literal("-") number = Word(nums) integer = Combine(Optional(plusmin) + number) floatn = Combine(integer + Optional(point + Optional(number))) # vector definition - lbracket = Literal('(') - rbracket = Literal(')') - comma = Literal(',') - vector = Combine(lbracket + floatn('x') + comma + - floatn('y') + comma + floatn('z') + rbracket) + lbracket = Literal("(") + rbracket = Literal(")") + comma = Literal(",") + vector = Combine( + lbracket + floatn("x") + comma + floatn("y") + comma + floatn("z") + rbracket + ) # direction definition - simple_dir = oneOf(['X', 'Y', 'Z', 'XY', 'XZ', 'YZ']) - direction = simple_dir('simple_dir') | vector('vector_dir') + simple_dir = oneOf(["X", "Y", "Z", "XY", "XZ", "YZ"]) + direction = simple_dir("simple_dir") | vector("vector_dir") # CQ type definition - cqtype = oneOf(['Plane', 'Cylinder', 'Sphere', 'Cone', 'Line', 'Circle', 'Arc'], - caseless=True) + cqtype = oneOf( + ["Plane", "Cylinder", "Sphere", "Cone", "Line", "Circle", "Arc"], caseless=True + ) cqtype = cqtype.setParseAction(upcaseTokens) # type operator - type_op = Literal('%') + type_op = Literal("%") # direction operator - direction_op = oneOf(['>', '<']) + direction_op = oneOf([">", "<"]) # index definition - ix_number = Group(Optional('-') + Word(nums)) - lsqbracket = Literal('[').suppress() - rsqbracket = Literal(']').suppress() + ix_number = Group(Optional("-") + Word(nums)) + lsqbracket = Literal("[").suppress() + rsqbracket = Literal("]").suppress() - index = lsqbracket + ix_number('index') + rsqbracket + index = lsqbracket + ix_number("index") + rsqbracket # other operators - other_op = oneOf(['|', '#', '+', '-']) + other_op = oneOf(["|", "#", "+", "-"]) # named view - named_view = oneOf(['front', 'back', 'left', 'right', 'top', 'bottom']) + named_view = oneOf(["front", "back", "left", "right", "top", "bottom"]) - return direction('only_dir') | \ - (type_op('type_op') + cqtype('cq_type')) | \ - (direction_op('dir_op') + direction('dir') + Optional(index)) | \ - (other_op('other_op') + direction('dir')) | \ - named_view('named_view') + return ( + direction("only_dir") + | (type_op("type_op") + cqtype("cq_type")) + | (direction_op("dir_op") + direction("dir") + Optional(index)) + | (other_op("other_op") + direction("dir")) + | named_view("named_view") + ) _grammar = _makeGrammar() # make a grammar instance @@ -506,33 +527,34 @@ class _SimpleStringSyntaxSelector(Selector): # define all token to object mappings self.axes = { - 'X': Vector(1, 0, 0), - 'Y': Vector(0, 1, 0), - 'Z': Vector(0, 0, 1), - 'XY': Vector(1, 1, 0), - 'YZ': Vector(0, 1, 1), - 'XZ': Vector(1, 0, 1) + "X": Vector(1, 0, 0), + "Y": Vector(0, 1, 0), + "Z": Vector(0, 0, 1), + "XY": Vector(1, 1, 0), + "YZ": Vector(0, 1, 1), + "XZ": Vector(1, 0, 1), } self.namedViews = { - 'front': (Vector(0, 0, 1), True), - 'back': (Vector(0, 0, 1), False), - 'left': (Vector(1, 0, 0), False), - 'right': (Vector(1, 0, 0), True), - 'top': (Vector(0, 1, 0), True), - 'bottom': (Vector(0, 1, 0), False) + "front": (Vector(0, 0, 1), True), + "back": (Vector(0, 0, 1), False), + "left": (Vector(1, 0, 0), False), + "right": (Vector(1, 0, 0), True), + "top": (Vector(0, 1, 0), True), + "bottom": (Vector(0, 1, 0), False), } self.operatorMinMax = { - '>': True, - '<': False, + ">": True, + "<": False, } self.operator = { - '+': DirectionSelector, - '-': lambda v: DirectionSelector(-v), - '#': PerpendicularDirSelector, - '|': ParallelDirSelector} + "+": DirectionSelector, + "-": lambda v: DirectionSelector(-v), + "#": PerpendicularDirSelector, + "|": ParallelDirSelector, + } self.parseResults = parseResults self.mySelector = self._chooseSelector(parseResults) @@ -541,23 +563,25 @@ class _SimpleStringSyntaxSelector(Selector): """ Sets up the underlying filters accordingly """ - if 'only_dir' in pr: + if "only_dir" in pr: vec = self._getVector(pr) return DirectionSelector(vec) - elif 'type_op' in pr: + elif "type_op" in pr: return TypeSelector(pr.cq_type) - elif 'dir_op' in pr: + elif "dir_op" in pr: vec = self._getVector(pr) minmax = self.operatorMinMax[pr.dir_op] - if 'index' in pr: - return DirectionNthSelector(vec, int(''.join(pr.index.asList())), minmax) + if "index" in pr: + return DirectionNthSelector( + vec, int("".join(pr.index.asList())), minmax + ) else: return DirectionMinMaxSelector(vec, minmax) - elif 'other_op' in pr: + elif "other_op" in pr: vec = self._getVector(pr) return self.operator[pr.other_op](vec) @@ -569,7 +593,7 @@ class _SimpleStringSyntaxSelector(Selector): """ Translate parsed vector string into a CQ Vector """ - if 'vector_dir' in pr: + if "vector_dir" in pr: vec = pr.vector_dir return Vector(float(vec.x), float(vec.y), float(vec.z)) else: @@ -590,10 +614,10 @@ def _makeExpressionGrammar(atom): """ # define operators - and_op = Literal('and') - or_op = Literal('or') - delta_op = oneOf(['exc', 'except']) - not_op = Literal('not') + and_op = Literal("and") + or_op = Literal("or") + delta_op = oneOf(["exc", "except"]) + not_op = Literal("not") def atom_callback(res): return _SimpleStringSyntaxSelector(res) @@ -622,11 +646,15 @@ def _makeExpressionGrammar(atom): return InverseSelector(right) # construct the final grammar and set all the callbacks - expr = infixNotation(atom, - [(and_op, 2, opAssoc.LEFT, and_callback), - (or_op, 2, opAssoc.LEFT, or_callback), - (delta_op, 2, opAssoc.LEFT, exc_callback), - (not_op, 1, opAssoc.RIGHT, not_callback)]) + expr = infixNotation( + atom, + [ + (and_op, 2, opAssoc.LEFT, and_callback), + (or_op, 2, opAssoc.LEFT, or_callback), + (delta_op, 2, opAssoc.LEFT, exc_callback), + (not_op, 1, opAssoc.RIGHT, not_callback), + ], + ) return expr @@ -690,8 +718,7 @@ class StringSyntaxSelector(Selector): Feed the input string through the parser and construct an relevant complex selector object """ self.selectorString = selectorString - parse_result = _expression_grammar.parseString(selectorString, - parseAll=True) + parse_result = _expression_grammar.parseString(selectorString, parseAll=True) self.mySelector = parse_result.asList()[0] def filter(self, objectList): diff --git a/doc/conf.py b/doc/conf.py index fb76407e..4e8517aa 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -13,71 +13,77 @@ import sys, os import os.path -#print "working path is %s" % os.getcwd() -#sys.path.append("../cadquery") + +# print "working path is %s" % os.getcwd() +# sys.path.append("../cadquery") import cadquery -#settings._target = None +# settings._target = None # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.autosummary','cadquery.cq_directive'] +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.autosummary", + "cadquery.cq_directive", +] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'CadQuery' -copyright = u'Parametric Products Intellectual Holdings LLC, All Rights Reserved' +project = u"CadQuery" +copyright = u"Parametric Products Intellectual Holdings LLC, All Rights Reserved" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = '1.0' +version = "1.0" # The full version, including alpha/beta/rc tags. -release = '1.0.0' +release = "1.0.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). @@ -85,27 +91,27 @@ add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -#html_theme = 'timlinux-linfiniti-sphinx' -html_theme = 'sphinx_rtd_theme' +# html_theme = 'timlinux-linfiniti-sphinx' +html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = { +# html_theme_options = { # "headerfont": "'Open Sans',Arial,sans-serif", # #"bodyfont:": "'Open Sans',Arial,sans-serif", # #"headerbg" : "{image: url('/img/bg/body.jpg');color:#000000;}", @@ -115,9 +121,9 @@ html_theme = 'sphinx_rtd_theme' ## "headercolor1": "#13171A;", # "headercolor2": "#444;", # "headerlinkcolor" : "#13171A;", -#} +# } -#agogo options +# agogo options """ bodyfont (CSS font family): Font for normal text. headerfont (CSS font family): Font for headings. @@ -133,14 +139,14 @@ html_theme = 'sphinx_rtd_theme' textalign (CSS text-align value): Text alignment for the body, default is justify. """ # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = "CadQuery Documentation" # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. @@ -149,36 +155,36 @@ html_logo = "_static/cqlogo.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False @@ -187,72 +193,66 @@ html_show_sourcelink = False html_show_sphinx = False # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'CadQuerydoc' +htmlhelp_basename = "CadQuerydoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'CadQuery.tex', u'CadQuery Documentation', - u'David Cowden', 'manual'), + ("index", "CadQuery.tex", u"CadQuery Documentation", u"David Cowden", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'cadquery', u'CadQuery Documentation', - [u'David Cowden'], 1) -] +man_pages = [("index", "cadquery", u"CadQuery Documentation", [u"David Cowden"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -261,16 +261,22 @@ man_pages = [ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'CadQuery', u'CadQuery Documentation', - u'David Cowden', 'CadQuery', 'A Fluent CAD api', - 'Miscellaneous'), + ( + "index", + "CadQuery", + u"CadQuery Documentation", + u"David Cowden", + "CadQuery", + "A Fluent CAD api", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/environment.yml b/environment.yml index 108bc9e8..e9310ae1 100644 --- a/environment.yml +++ b/environment.yml @@ -9,6 +9,7 @@ dependencies: - pyparsing - sphinx - sphinx_rtd_theme + - black - codecov - pytest - pytest-cov diff --git a/examples/Ex001_Simple_Block.py b/examples/Ex001_Simple_Block.py index f72445f4..8496d103 100644 --- a/examples/Ex001_Simple_Block.py +++ b/examples/Ex001_Simple_Block.py @@ -1,9 +1,9 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -length = 80.0 # Length of the block -height = 60.0 # Height of the block -thickness = 10.0 # Thickness of the block +length = 80.0 # Length of the block +height = 60.0 # Height of the block +thickness = 10.0 # Thickness of the block # Create a 3D block based on the dimension variables above. # 1. Establishes a workplane that an object can be built on. diff --git a/examples/Ex002_Block_With_Bored_Center_Hole.py b/examples/Ex002_Block_With_Bored_Center_Hole.py index 6b00ffcd..87ba3926 100644 --- a/examples/Ex002_Block_With_Bored_Center_Hole.py +++ b/examples/Ex002_Block_With_Bored_Center_Hole.py @@ -1,10 +1,10 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -length = 80.0 # Length of the block -height = 60.0 # Height of the block -thickness = 10.0 # Thickness of the block -center_hole_dia = 22.0 # Diameter of center hole in block +length = 80.0 # Length of the block +height = 60.0 # Height of the block +thickness = 10.0 # Thickness of the block +center_hole_dia = 22.0 # Diameter of center hole in block # Create a block based on the dimensions above and add a 22mm center hole. # 1. Establishes a workplane that an object can be built on. @@ -13,8 +13,13 @@ center_hole_dia = 22.0 # Diameter of center hole in block # 2. The highest (max) Z face is selected and a new workplane is created on it. # 3. The new workplane is used to drill a hole through the block. # 3a. The hole is automatically centered in the workplane. -result = (cq.Workplane("XY").box(length, height, thickness) - .faces(">Z").workplane().hole(center_hole_dia)) +result = ( + cq.Workplane("XY") + .box(length, height, thickness) + .faces(">Z") + .workplane() + .hole(center_hole_dia) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py b/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py index 071dfd9a..373478c5 100644 --- a/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py +++ b/examples/Ex003_Pillow_Block_With_Counterbored_Holes.py @@ -1,15 +1,15 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -length = 80.0 # Length of the block -width = 60.0 # Width of the block -height = 100.0 # Height of the block -thickness = 10.0 # Thickness of the block -center_hole_dia = 22.0 # Diameter of center hole in block -cbore_hole_diameter = 2.4 # Bolt shank/threads clearance hole diameter -cbore_inset = 12.0 # How far from the edge the cbored holes are set -cbore_diameter = 4.4 # Bolt head pocket hole diameter -cbore_depth = 2.1 # Bolt head pocket hole depth +length = 80.0 # Length of the block +width = 60.0 # Width of the block +height = 100.0 # Height of the block +thickness = 10.0 # Thickness of the block +center_hole_dia = 22.0 # Diameter of center hole in block +cbore_hole_diameter = 2.4 # Bolt shank/threads clearance hole diameter +cbore_inset = 12.0 # How far from the edge the cbored holes are set +cbore_diameter = 4.4 # Bolt head pocket hole diameter +cbore_depth = 2.1 # Bolt head pocket hole depth # Create a 3D block based on the dimensions above and add a 22mm center hold # and 4 counterbored holes for bolts @@ -26,12 +26,20 @@ cbore_depth = 2.1 # Bolt head pocket hole depth # do not show up in the final displayed geometry. # 6. The vertices of the rectangle (corners) are selected, and a counter-bored # hole is placed at each of the vertices (all 4 of them at once). -result = (cq.Workplane("XY").box(length, height, thickness) - .faces(">Z").workplane().hole(center_hole_dia) - .faces(">Z").workplane() +result = ( + cq.Workplane("XY") + .box(length, height, thickness) + .faces(">Z") + .workplane() + .hole(center_hole_dia) + .faces(">Z") + .workplane() .rect(length - cbore_inset, height - cbore_inset, forConstruction=True) - .vertices().cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) - .edges("|Z").fillet(2.0)) + .vertices() + .cboreHole(cbore_hole_diameter, cbore_diameter, cbore_depth) + .edges("|Z") + .fillet(2.0) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex004_Extruded_Cylindrical_Plate.py b/examples/Ex004_Extruded_Cylindrical_Plate.py index c9f308ff..d287887c 100644 --- a/examples/Ex004_Extruded_Cylindrical_Plate.py +++ b/examples/Ex004_Extruded_Cylindrical_Plate.py @@ -1,10 +1,10 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -circle_radius = 50.0 # Radius of the plate -thickness = 13.0 # Thickness of the plate -rectangle_width = 13.0 # Width of rectangular hole in cylindrical plate -rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate +circle_radius = 50.0 # Radius of the plate +thickness = 13.0 # Thickness of the plate +rectangle_width = 13.0 # Width of rectangular hole in cylindrical plate +rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate # Extrude a cylindrical plate with a rectangular hole in the middle of it. # 1. Establishes a workplane that an object can be built on. @@ -21,9 +21,12 @@ rectangle_length = 19.0 # Length of rectangular hole in cylindrical plate # plate with a rectangular hole in the center. # 3a. circle() and rect() could be changed to any other shape to completely # change the resulting plate and/or the hole in it. -result = (cq.Workplane("front").circle(circle_radius) - .rect(rectangle_width, rectangle_length) - .extrude(thickness)) +result = ( + cq.Workplane("front") + .circle(circle_radius) + .rect(rectangle_width, rectangle_length) + .extrude(thickness) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex005_Extruded_Lines_and_Arcs.py b/examples/Ex005_Extruded_Lines_and_Arcs.py index e213f31e..c9fda2af 100644 --- a/examples/Ex005_Extruded_Lines_and_Arcs.py +++ b/examples/Ex005_Extruded_Lines_and_Arcs.py @@ -1,8 +1,8 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -width = 2.0 # Overall width of the plate -thickness = 0.25 # Thickness of the plate +width = 2.0 # Overall width of the plate +thickness = 0.25 # Thickness of the plate # Extrude a plate outline made of lines and an arc # 1. Establishes a workplane that an object can be built on. @@ -34,12 +34,16 @@ thickness = 0.25 # Thickness of the plate # 7a. Without the close(), the 2D sketch will be left open and the extrude # operation will provide unpredictable results. # 8. The 2D sketch is extruded into a solid object of the specified thickness. -result = (cq.Workplane("front").lineTo(width, 0) - .lineTo(width, 1.0) - .threePointArc((1.0, 1.5), (0.0, 1.0)) - .sagittaArc((-0.5, 1.0), 0.2) - .radiusArc((-0.7, -0.2), -1.5) - .close().extrude(thickness)) +result = ( + cq.Workplane("front") + .lineTo(width, 0) + .lineTo(width, 1.0) + .threePointArc((1.0, 1.5), (0.0, 1.0)) + .sagittaArc((-0.5, 1.0), 0.2) + .radiusArc((-0.7, -0.2), -1.5) + .close() + .extrude(thickness) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex006_Moving_the_Current_Working_Point.py b/examples/Ex006_Moving_the_Current_Working_Point.py index 3c26121a..b1337538 100644 --- a/examples/Ex006_Moving_the_Current_Working_Point.py +++ b/examples/Ex006_Moving_the_Current_Working_Point.py @@ -1,8 +1,8 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -circle_radius = 3.0 # The outside radius of the plate -thickness = 0.25 # The thickness of the plate +circle_radius = 3.0 # The outside radius of the plate +thickness = 0.25 # The thickness of the plate # Make a plate with two cutouts in it by moving the workplane center point # 1. Establishes a workplane that an object can be built on. diff --git a/examples/Ex007_Using_Point_Lists.py b/examples/Ex007_Using_Point_Lists.py index d824c750..ad577fcd 100644 --- a/examples/Ex007_Using_Point_Lists.py +++ b/examples/Ex007_Using_Point_Lists.py @@ -1,9 +1,9 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -plate_radius = 2.0 # The radius of the plate that will be extruded +plate_radius = 2.0 # The radius of the plate that will be extruded hole_pattern_radius = 0.25 # Radius of circle where the holes will be placed -thickness = 0.125 # The thickness of the plate that will be extruded +thickness = 0.125 # The thickness of the plate that will be extruded # Make a plate with 4 holes in it at various points in a polar arrangement from # the center of the workplane. diff --git a/examples/Ex008_Polygon_Creation.py b/examples/Ex008_Polygon_Creation.py index 2853c1e0..43f8eae9 100644 --- a/examples/Ex008_Polygon_Creation.py +++ b/examples/Ex008_Polygon_Creation.py @@ -1,11 +1,11 @@ import cadquery as cq # These can be modified rather than hardcoding values for each dimension. -width = 3.0 # The width of the plate -height = 4.0 # The height of the plate -thickness = 0.25 # The thickness of the plate -polygon_sides = 6 # The number of sides that the polygonal holes should have -polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points +width = 3.0 # The width of the plate +height = 4.0 # The height of the plate +thickness = 0.25 # The thickness of the plate +polygon_sides = 6 # The number of sides that the polygonal holes should have +polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points # Create a plate with two polygons cut through it # 1. Establishes a workplane that an object can be built on. @@ -30,10 +30,13 @@ polygon_dia = 1.0 # The diameter of the circle enclosing the polygon points # like cutBlind() assume a positive cut direction, but cutThruAll() assumes # instead that the cut is made from a max direction and cuts downward from # that max through all objects. -result = (cq.Workplane("front").box(width, height, thickness) - .pushPoints([(0, 0.75), (0, -0.75)]) - .polygon(polygon_sides, polygon_dia) - .cutThruAll()) +result = ( + cq.Workplane("front") + .box(width, height, thickness) + .pushPoints([(0, 0.75), (0, -0.75)]) + .polygon(polygon_sides, polygon_dia) + .cutThruAll() +) # Displays the result of this script show_object(result) diff --git a/examples/Ex009_Polylines.py b/examples/Ex009_Polylines.py index 8712bf99..70f83e87 100644 --- a/examples/Ex009_Polylines.py +++ b/examples/Ex009_Polylines.py @@ -6,13 +6,13 @@ import cadquery as cq # Define the points that the polyline will be drawn to/thru pts = [ - (W/2.0, H/2.0), - (W/2.0, (H/2.0 - t)), - (t/2.0, (H/2.0-t)), - (t/2.0, (t - H/2.0)), - (W/2.0, (t - H/2.0)), - (W/2.0, H/-2.0), - (0, H/-2.0) + (W / 2.0, H / 2.0), + (W / 2.0, (H / 2.0 - t)), + (t / 2.0, (H / 2.0 - t)), + (t / 2.0, (t - H / 2.0)), + (W / 2.0, (t - H / 2.0)), + (W / 2.0, H / -2.0), + (0, H / -2.0), ] # We generate half of the I-beam outline and then mirror it to create the full @@ -30,10 +30,7 @@ pts = [ # 3. Only half of the I-beam profile has been drawn so far. That half is # mirrored around the Y-axis to create the complete I-beam profile. # 4. The I-beam profile is extruded to the final length of the beam. -result = (cq.Workplane("front").moveTo(0, H/2.0) - .polyline(pts) - .mirrorY() - .extrude(L)) +result = cq.Workplane("front").moveTo(0, H / 2.0).polyline(pts).mirrorY().extrude(L) # Displays the result of this script show_object(result) diff --git a/examples/Ex010_Defining_an_Edge_with_a_Spline.py b/examples/Ex010_Defining_an_Edge_with_a_Spline.py index 8b4c67cb..57b06328 100644 --- a/examples/Ex010_Defining_an_Edge_with_a_Spline.py +++ b/examples/Ex010_Defining_an_Edge_with_a_Spline.py @@ -13,7 +13,7 @@ sPnts = [ (1.5, 1.0), (1.0, 1.25), (0.5, 1.0), - (0, 1.0) + (0, 1.0), ] # 2. Generate our plate with the spline feature and make sure it is a diff --git a/examples/Ex015_Rotated_Workplanes.py b/examples/Ex015_Rotated_Workplanes.py index 13a9a8e6..602b4cb0 100644 --- a/examples/Ex015_Rotated_Workplanes.py +++ b/examples/Ex015_Rotated_Workplanes.py @@ -13,10 +13,16 @@ import cadquery as cq # 6. Selects the vertices of the for-construction rectangle. # 7. Places holes at the center of each selected vertex. # 7a. Since the workplane is rotated, this results in angled holes in the face. -result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") - .workplane() - .transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0)) - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25)) +result = ( + cq.Workplane("front") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .workplane() + .transformed(offset=(0, -1.5, 1.0), rotate=(60, 0, 0)) + .rect(1.5, 1.5, forConstruction=True) + .vertices() + .hole(0.25) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex016_Using_Construction_Geometry.py b/examples/Ex016_Using_Construction_Geometry.py index aa92a0c4..37e653f8 100644 --- a/examples/Ex016_Using_Construction_Geometry.py +++ b/examples/Ex016_Using_Construction_Geometry.py @@ -12,10 +12,15 @@ import cadquery as cq # other geometry. # 6. Selects the vertices of the for-construction rectangle. # 7. Places holes at the center of each selected vertex. -result = (cq.Workplane("front").box(2, 2, 0.5) - .faces(">Z").workplane() - .rect(1.5, 1.5, forConstruction=True).vertices() - .hole(0.125)) +result = ( + cq.Workplane("front") + .box(2, 2, 0.5) + .faces(">Z") + .workplane() + .rect(1.5, 1.5, forConstruction=True) + .vertices() + .hole(0.125) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex018_Making_Lofts.py b/examples/Ex018_Making_Lofts.py index f5ae39b8..2106d99f 100644 --- a/examples/Ex018_Making_Lofts.py +++ b/examples/Ex018_Making_Lofts.py @@ -11,10 +11,15 @@ import cadquery as cq # 5. Creates a workplane 3 mm above the face the circle was drawn on. # 6. Draws a 2D circle on the new, offset workplane. # 7. Creates a loft between the circle and the rectangle. -result = (cq.Workplane("front").box(4.0, 4.0, 0.25).faces(">Z") - .circle(1.5).workplane(offset=3.0) - .rect(0.75, 0.5) - .loft(combine=True)) +result = ( + cq.Workplane("front") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .circle(1.5) + .workplane(offset=3.0) + .rect(0.75, 0.5) + .loft(combine=True) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex019_Counter_Sunk_Holes.py b/examples/Ex019_Counter_Sunk_Holes.py index 5ca39c62..e6a8d84c 100644 --- a/examples/Ex019_Counter_Sunk_Holes.py +++ b/examples/Ex019_Counter_Sunk_Holes.py @@ -11,9 +11,15 @@ import cadquery as cq # function. # 5a. When the depth of the counter-sink hole is set to None, the hole will be # cut through. -result = (cq.Workplane(cq.Plane.XY()).box(4, 2, 0.5).faces(">Z") - .workplane().rect(3.5, 1.5, forConstruction=True) - .vertices().cskHole(0.125, 0.25, 82.0, depth=None)) +result = ( + cq.Workplane(cq.Plane.XY()) + .box(4, 2, 0.5) + .faces(">Z") + .workplane() + .rect(3.5, 1.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82.0, depth=None) +) # Displays the result of this script show_object(result) diff --git a/examples/Ex021_Splitting_an_Object.py b/examples/Ex021_Splitting_an_Object.py index 5f878bf6..bdd217b4 100644 --- a/examples/Ex021_Splitting_an_Object.py +++ b/examples/Ex021_Splitting_an_Object.py @@ -9,8 +9,7 @@ import cadquery as cq # that new geometry can be built on. # 4. Draws a 2D circle on the new workplane and then uses it to cut a hole # all the way through the box. -c = (cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane() - .circle(0.25).cutThruAll()) +c = cq.Workplane("XY").box(1, 1, 1).faces(">Z").workplane().circle(0.25).cutThruAll() # 5. Selects the face furthest away from the origin in the +Y axis direction. # 6. Creates an offset workplane that is set in the center of the object. diff --git a/examples/Ex022_Revolution.py b/examples/Ex022_Revolution.py index c5f31070..eb4debb8 100644 --- a/examples/Ex022_Revolution.py +++ b/examples/Ex022_Revolution.py @@ -9,13 +9,13 @@ angle_degrees = 360.0 # Revolve a cylinder from a rectangle # Switch comments around in this section to try the revolve operation with different parameters result = cq.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5)) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5)) -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve(angle_degrees) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5)) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5, -5),(-5, 5)) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length).revolve(angle_degrees,(-5,-5),(-5,5), False) # Revolve a donut with square walls -#result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, True).revolve(angle_degrees, (20, 0), (20, 10)) +# result = cadquery.Workplane("XY").rect(rectangle_width, rectangle_length, True).revolve(angle_degrees, (20, 0), (20, 10)) # Displays the result of this script show_object(result) diff --git a/examples/Ex023_Sweep.py b/examples/Ex023_Sweep.py index c2ba5017..c5b01607 100644 --- a/examples/Ex023_Sweep.py +++ b/examples/Ex023_Sweep.py @@ -1,11 +1,7 @@ import cadquery as cq # Points we will use to create spline and polyline paths to sweep over -pts = [ - (0, 1), - (1, 2), - (2, 4) -] +pts = [(0, 1), (1, 2), (2, 4)] # Spline path generated from our list of points (tuples) path = cq.Workplane("XZ").spline(pts) @@ -37,4 +33,4 @@ show_object(defaultSweep) show_object(frenetShell.translate((5, 0, 0))) show_object(defaultRect.translate((10, 0, 0))) show_object(plineSweep.translate((15, 0, 0))) -show_object(arcSweep.translate((20, 0, 0))) \ No newline at end of file +show_object(arcSweep.translate((20, 0, 0))) diff --git a/examples/Ex024_Sweep_With_Multiple_Sections.py b/examples/Ex024_Sweep_With_Multiple_Sections.py index 0c76f93d..5c4cd005 100644 --- a/examples/Ex024_Sweep_With_Multiple_Sections.py +++ b/examples/Ex024_Sweep_With_Multiple_Sections.py @@ -4,37 +4,80 @@ import cadquery as cq path = cq.Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) # Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 -defaultSweep = (cq.Workplane("YZ").workplane(offset=-10.0).circle(2.0). - workplane(offset=10.0).circle(1.0). - workplane(offset=10.0).circle(2.0).sweep(path, multisection=True)) +defaultSweep = ( + cq.Workplane("YZ") + .workplane(offset=-10.0) + .circle(2.0) + .workplane(offset=10.0) + .circle(1.0) + .workplane(offset=10.0) + .circle(2.0) + .sweep(path, multisection=True) +) # We can sweep thrue different shapes -recttocircleSweep = (cq.Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). - workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). - workplane(offset=8.0).rect(2.0, 2.0).sweep(path, multisection=True)) +recttocircleSweep = ( + cq.Workplane("YZ") + .workplane(offset=-10.0) + .rect(2.0, 2.0) + .workplane(offset=8.0) + .circle(1.0) + .workplane(offset=4.0) + .circle(1.0) + .workplane(offset=8.0) + .rect(2.0, 2.0) + .sweep(path, multisection=True) +) -circletorectSweep = (cq.Workplane("YZ").workplane(offset=-10.0).circle(1.0). - workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). - workplane(offset=7.0).circle(1.0).sweep(path, multisection=True)) +circletorectSweep = ( + cq.Workplane("YZ") + .workplane(offset=-10.0) + .circle(1.0) + .workplane(offset=7.0) + .rect(2.0, 2.0) + .workplane(offset=6.0) + .rect(2.0, 2.0) + .workplane(offset=7.0) + .circle(1.0) + .sweep(path, multisection=True) +) # Placement of the Shape is important otherwise could produce unexpected shape -specialSweep = (cq.Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0). - sweep(path, multisection=True)) +specialSweep = ( + cq.Workplane("YZ") + .circle(1.0) + .workplane(offset=10.0) + .rect(2.0, 2.0) + .sweep(path, multisection=True) +) # Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 -path = (cq.Workplane("XZ").moveTo(-5, 4).lineTo(0, 4). - threePointArc((4, 0), (0, -4)).lineTo(-5, -4)) +path = ( + cq.Workplane("XZ") + .moveTo(-5, 4) + .lineTo(0, 4) + .threePointArc((4, 0), (0, -4)) + .lineTo(-5, -4) +) # Placement of different shapes should follow the path # cylinder r=1.5 along first line # then sweep allong arc from r=1.5 to r=1.0 # then cylinder r=1.0 along last line -arcSweep = (cq.Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). - workplane(offset=5).circle(1.5). - moveTo(0, -8).circle(1.0). - workplane(offset=-5).circle(1.0). - sweep(path, multisection=True)) +arcSweep = ( + cq.Workplane("YZ") + .workplane(offset=-5) + .moveTo(0, 4) + .circle(1.5) + .workplane(offset=5) + .circle(1.5) + .moveTo(0, -8) + .circle(1.0) + .workplane(offset=-5) + .circle(1.0) + .sweep(path, multisection=True) +) # Translate the resulting solids so that they do not overlap and display them left to right @@ -43,5 +86,3 @@ show_object(circletorectSweep.translate((0, 5, 0))) show_object(recttocircleSweep.translate((0, 10, 0))) show_object(specialSweep.translate((0, 15, 0))) show_object(arcSweep.translate((0, -5, 0))) - - diff --git a/examples/Ex100_Lego_Brick.py b/examples/Ex100_Lego_Brick.py index e0981255..ca2fae61 100644 --- a/examples/Ex100_Lego_Brick.py +++ b/examples/Ex100_Lego_Brick.py @@ -4,8 +4,8 @@ import cadquery as cq ##### # Inputs ###### -lbumps = 1 # number of bumps long -wbumps = 1 # number of bumps wide +lbumps = 1 # number of bumps long +wbumps = 1 # number of bumps wide thin = True # True for thin, False for thick # @@ -22,8 +22,8 @@ else: t = (pitch - (2 * clearance) - bumpDiam) / 2.0 postDiam = pitch - t # works out to 6.5 -total_length = lbumps*pitch - 2.0*clearance -total_width = wbumps*pitch - 2.0*clearance +total_length = lbumps * pitch - 2.0 * clearance +total_width = wbumps * pitch - 2.0 * clearance # make the base s = cq.Workplane("XY").box(total_length, total_width, height) @@ -32,23 +32,37 @@ s = cq.Workplane("XY").box(total_length, total_width, height) s = s.faces("Z").workplane(). - rarray(pitch, pitch, lbumps, wbumps, True).circle(bumpDiam / 2.0) - .extrude(bumpHeight)) +s = ( + s.faces(">Z") + .workplane() + .rarray(pitch, pitch, lbumps, wbumps, True) + .circle(bumpDiam / 2.0) + .extrude(bumpHeight) +) # add posts on the bottom. posts are different diameter depending on geometry # solid studs for 1 bump, tubes for multiple, none for 1x1 tmp = s.faces(" 1 and wbumps > 1: - tmp = (tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True). - circle(postDiam / 2.0).circle(bumpDiam / 2.0).extrude(height - t)) + tmp = ( + tmp.rarray(pitch, pitch, lbumps - 1, wbumps - 1, center=True) + .circle(postDiam / 2.0) + .circle(bumpDiam / 2.0) + .extrude(height - t) + ) elif lbumps > 1: - tmp = (tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True). - circle(t).extrude(height - t)) + tmp = ( + tmp.rarray(pitch, pitch, lbumps - 1, 1, center=True) + .circle(t) + .extrude(height - t) + ) elif wbumps > 1: - tmp = (tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True). - circle(t).extrude(height - t)) + tmp = ( + tmp.rarray(pitch, pitch, 1, wbumps - 1, center=True) + .circle(t) + .extrude(height - t) + ) else: tmp = s diff --git a/setup.py b/setup.py index 3faff965..48f712e4 100644 --- a/setup.py +++ b/setup.py @@ -15,43 +15,48 @@ import os from setuptools import setup -#if we are building in travis, use the build number as the sub-minor version -version = '0.5-SNAPSHOT' -if 'TRAVIS_TAG' in os.environ.keys(): - version= os.environ['TRAVIS_TAG'] +# if we are building in travis, use the build number as the sub-minor version +version = "0.5-SNAPSHOT" +if "TRAVIS_TAG" in os.environ.keys(): + version = os.environ["TRAVIS_TAG"] setup( - name='cadquery', + name="cadquery", version=version, - url='https://github.com/dcowden/cadquery', - license='Apache Public License 2.0', - author='David Cowden', - author_email='dave.cowden@gmail.com', - description='CadQuery is a parametric scripting language for creating and traversing CAD models', - long_description=open('README.md').read(), - packages=['cadquery','cadquery.contrib','cadquery.occ_impl','cadquery.plugins','tests'], + url="https://github.com/dcowden/cadquery", + license="Apache Public License 2.0", + author="David Cowden", + author_email="dave.cowden@gmail.com", + description="CadQuery is a parametric scripting language for creating and traversing CAD models", + long_description=open("README.md").read(), + packages=[ + "cadquery", + "cadquery.contrib", + "cadquery.occ_impl", + "cadquery.plugins", + "tests", + ], include_package_data=True, zip_safe=False, - platforms='any', - test_suite='tests', - + platforms="any", + test_suite="tests", classifiers=[ - 'Development Status :: 5 - Production/Stable', + "Development Status :: 5 - Production/Stable", #'Development Status :: 6 - Mature', #'Development Status :: 7 - Inactive', - 'Intended Audience :: Developers', - 'Intended Audience :: End Users/Desktop', - 'Intended Audience :: Information Technology', - 'Intended Audience :: Science/Research', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: POSIX', - 'Operating System :: MacOS', - 'Operating System :: Unix', - 'Programming Language :: Python', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Topic :: Internet', - 'Topic :: Scientific/Engineering' - ] + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "Intended Audience :: Information Technology", + "Intended Audience :: Science/Research", + "Intended Audience :: System Administrators", + "License :: OSI Approved :: Apache Software License", + "Operating System :: POSIX", + "Operating System :: MacOS", + "Operating System :: Unix", + "Programming Language :: Python", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Internet", + "Topic :: Scientific/Engineering", + ], ) diff --git a/tests/__init__.py b/tests/__init__.py index 2c0ca15d..b36542e9 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -6,21 +6,23 @@ import os def readFileAsString(fileName): - f = open(fileName, 'r') + f = open(fileName, "r") s = f.read() f.close() return s def writeStringToFile(strToWrite, fileName): - f = open(fileName, 'w') + f = open(fileName, "w") f.write(strToWrite) f.close() def makeUnitSquareWire(): V = Vector - return Wire.makePolygon([V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)]) + return Wire.makePolygon( + [V(0, 0, 0), V(1, 0, 0), V(1, 1, 0), V(0, 1, 0), V(0, 0, 0)] + ) def makeUnitCube(): @@ -38,26 +40,24 @@ def toTuple(v): elif type(v) == Vector: return v.toTuple() else: - raise RuntimeError( - "dont know how to convert type %s to tuple" % str(type(v))) + raise RuntimeError("dont know how to convert type %s to tuple" % str(type(v))) class BaseTest(unittest.TestCase): - def assertTupleAlmostEquals(self, expected, actual, places): for i, j in zip(actual, expected): self.assertAlmostEqual(i, j, places) __all__ = [ - 'TestCadObjects', - 'TestCadQuery', - 'TestCQGI', - 'TestCQSelectors', - 'TestCQSelectors', - 'TestExporters', - 'TestImporters', - 'TestJupyter', - 'TestWorkplanes', - 'TestAssembleEdges', + "TestCadObjects", + "TestCadQuery", + "TestCQGI", + "TestCQSelectors", + "TestCQSelectors", + "TestExporters", + "TestImporters", + "TestJupyter", + "TestWorkplanes", + "TestAssembleEdges", ] diff --git a/tests/test_cad_objects.py b/tests/test_cad_objects.py index 53c432fe..c556678a 100644 --- a/tests/test_cad_objects.py +++ b/tests/test_cad_objects.py @@ -3,9 +3,11 @@ import sys import unittest from tests import BaseTest from OCC.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_DZ, gp_XYZ -from OCC.BRepBuilderAPI import (BRepBuilderAPI_MakeVertex, - BRepBuilderAPI_MakeEdge, - BRepBuilderAPI_MakeFace) +from OCC.BRepBuilderAPI import ( + BRepBuilderAPI_MakeVertex, + BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeFace, +) from OCC.GC import GC_MakeCircle @@ -13,36 +15,34 @@ from cadquery import * class TestCadObjects(BaseTest): - def _make_circle(self): - circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp_DZ()), - 2.) + circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp_DZ()), 2.0) return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge()) def testVectorConstructors(self): v1 = Vector(1, 2, 3) v2 = Vector((1, 2, 3)) v3 = Vector(gp_Vec(1, 2, 3)) - v4 = Vector([1,2,3]) - v5 = Vector(gp_XYZ(1,2,3)) + v4 = Vector([1, 2, 3]) + v5 = Vector(gp_XYZ(1, 2, 3)) for v in [v1, v2, v3, v4, v5]: self.assertTupleAlmostEquals((1, 2, 3), v.toTuple(), 4) - - v6 = Vector((1,2)) - v7 = Vector([1,2]) - v8 = Vector(1,2) - + + v6 = Vector((1, 2)) + v7 = Vector([1, 2]) + v8 = Vector(1, 2) + for v in [v6, v7, v8]: self.assertTupleAlmostEquals((1, 2, 0), v.toTuple(), 4) - + v9 = Vector() self.assertTupleAlmostEquals((0, 0, 0), v9.toTuple(), 4) - - v9.x = 1. - v9.y = 2. - v9.z = 3. + + v9.x = 1.0 + v9.y = 2.0 + v9.z = 3.0 self.assertTupleAlmostEquals((1, 2, 3), (v9.x, v9.y, v9.z), 4) def testVertex(self): @@ -70,20 +70,22 @@ class TestCadObjects(BaseTest): self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3) def testEdgeWrapperMakeCircle(self): - halfCircleEdge = Edge.makeCircle(radius=10, pnt=( - 0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180) + halfCircleEdge = Edge.makeCircle( + radius=10, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180 + ) - #self.assertTupleAlmostEquals((0.0, 5.0, 0.0), halfCircleEdge.CenterOfBoundBox(0.0001).toTuple(),3) + # self.assertTupleAlmostEquals((0.0, 5.0, 0.0), halfCircleEdge.CenterOfBoundBox(0.0001).toTuple(),3) self.assertTupleAlmostEquals( - (10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3) + (10.0, 0.0, 0.0), halfCircleEdge.startPoint().toTuple(), 3 + ) self.assertTupleAlmostEquals( - (-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3) + (-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3 + ) def testFaceWrapperMakePlane(self): mplane = Face.makePlane(10, 10) - self.assertTupleAlmostEquals( - (0.0, 0.0, 1.0), mplane.normalAt().toTuple(), 3) + self.assertTupleAlmostEquals((0.0, 0.0, 1.0), mplane.normalAt().toTuple(), 3) def testCenterOfBoundBox(self): pass @@ -109,12 +111,15 @@ class TestCadObjects(BaseTest): Workplane.cyl = cylinders # Now test. here we want weird workplane to see if the objects are transformed right - s = Workplane("XY").rect( - 2.0, 3.0, forConstruction=True).vertices().cyl(0.25, 0.5) + s = ( + Workplane("XY") + .rect(2.0, 3.0, forConstruction=True) + .vertices() + .cyl(0.25, 0.5) + ) self.assertEqual(4, len(s.val().Solids())) - self.assertTupleAlmostEquals( - (0.0, 0.0, 0.25), s.val().Center().toTuple(), 3) + self.assertTupleAlmostEquals((0.0, 0.0, 0.25), s.val().Center().toTuple(), 3) def testDot(self): v1 = Vector(2, 2, 2) @@ -142,7 +147,7 @@ class TestCadObjects(BaseTest): self.assertEqual(0, abs(Vector(0, 0, 0))) self.assertEqual(1, abs(Vector(1, 0, 0))) - self.assertEqual((1+4+9)**0.5, abs(Vector(1, 2, 3))) + self.assertEqual((1 + 4 + 9) ** 0.5, abs(Vector(1, 2, 3))) def testVectorEquals(self): a = Vector(1, 2, 3) @@ -163,25 +168,31 @@ class TestCadObjects(BaseTest): # test passing Plane object point = Vector(10, 11, 12).projectToPlane(Plane(base, x_dir, normal)) - self.assertTupleAlmostEquals(point.toTuple(), (59/7, 55/7, 51/7), - decimal_places) + self.assertTupleAlmostEquals( + point.toTuple(), (59 / 7, 55 / 7, 51 / 7), decimal_places + ) def testMatrixCreationAndAccess(self): def matrix_vals(m): - return [[m[r,c] for c in range(4)] for r in range(4)] + return [[m[r, c] for c in range(4)] for r in range(4)] + # default constructor creates a 4x4 identity matrix m = Matrix() - identity = [[1., 0., 0., 0.], - [0., 1., 0., 0.], - [0., 0., 1., 0.], - [0., 0., 0., 1.]] + identity = [ + [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ] self.assertEqual(identity, matrix_vals(m)) - vals4x4 = [[1., 0., 0., 1.], - [0., 1., 0., 2.], - [0., 0., 1., 3.], - [0., 0., 0., 1.]] - vals4x4_tuple = tuple(tuple(r) for r in vals4x4) + vals4x4 = [ + [1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 2.0], + [0.0, 0.0, 1.0, 3.0], + [0.0, 0.0, 0.0, 1.0], + ] + vals4x4_tuple = tuple(tuple(r) for r in vals4x4) # test constructor with 16-value input m = Matrix(vals4x4) @@ -197,10 +208,12 @@ class TestCadObjects(BaseTest): self.assertEqual(vals4x4, matrix_vals(m)) # Test 16-value input with invalid values for the last 4 - invalid = [[1., 0., 0., 1.], - [0., 1., 0., 2.], - [0., 0., 1., 3.], - [1., 2., 3., 4.]] + invalid = [ + [1.0, 0.0, 0.0, 1.0], + [0.0, 1.0, 0.0, 2.0], + [0.0, 0.0, 1.0, 3.0], + [1.0, 2.0, 3.0, 4.0], + ] with self.assertRaises(ValueError): Matrix(invalid) @@ -208,11 +221,11 @@ class TestCadObjects(BaseTest): with self.assertRaises(TypeError): Matrix([[1, 2, 3, 4], [1, 2, 3], [1, 2, 3, 4]]) with self.assertRaises(TypeError): - Matrix([1,2,3]) + Matrix([1, 2, 3]) # Invalid sub-type with self.assertRaises(TypeError): - Matrix([[1, 2, 3, 4], 'abc', [1, 2, 3, 4]]) + Matrix([[1, 2, 3, 4], "abc", [1, 2, 3, 4]]) # test out-of-bounds access m = Matrix() @@ -221,8 +234,7 @@ class TestCadObjects(BaseTest): with self.assertRaises(IndexError): m[4, 0] with self.assertRaises(IndexError): - m['ab'] - + m["ab"] def testTranslate(self): e = Edge.makeCircle(2, (1, 2, 3)) @@ -231,54 +243,53 @@ class TestCadObjects(BaseTest): self.assertTupleAlmostEquals((1.0, 2.0, 4.0), e2.Center().toTuple(), 3) def testVertices(self): - e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0), - gp_Pnt(1, 1, 0)).Edge()) + e = Shape.cast(BRepBuilderAPI_MakeEdge(gp_Pnt(0, 0, 0), gp_Pnt(1, 1, 0)).Edge()) self.assertEqual(2, len(e.Vertices())) def testPlaneEqual(self): # default orientation self.assertEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)) + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), ) # moved origin self.assertEqual( - Plane(origin=(2,1,-1), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(2,1,-1), xDir=(1,0,0), normal=(0,0,1)) + Plane(origin=(2, 1, -1), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(2, 1, -1), xDir=(1, 0, 0), normal=(0, 0, 1)), ) # moved x-axis self.assertEqual( - Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)), - Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)) + Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)), ) # moved z-axis self.assertEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)), - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)) + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)), ) - + def testPlaneNotEqual(self): # type difference - for value in [None, 0, 1, 'abc']: + for value in [None, 0, 1, "abc"]: self.assertNotEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - value + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), value ) # origin difference self.assertNotEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(0,0,1), xDir=(1,0,0), normal=(0,0,1)) + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 1), xDir=(1, 0, 0), normal=(0, 0, 1)), ) # x-axis difference self.assertNotEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(0,0,0), xDir=(1,1,0), normal=(0,0,1)) + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 1, 0), normal=(0, 0, 1)), ) # z-axis difference self.assertNotEqual( - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,0,1)), - Plane(origin=(0,0,0), xDir=(1,0,0), normal=(0,1,1)) + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 0, 1)), + Plane(origin=(0, 0, 0), xDir=(1, 0, 0), normal=(0, 1, 1)), ) -if __name__ == '__main__': + +if __name__ == "__main__": unittest.main() diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 01562c6e..46dfc412 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -3,7 +3,7 @@ """ # system modules -import math,os.path,time,tempfile +import math, os.path, time, tempfile from random import choice from random import random from random import randrange @@ -11,7 +11,14 @@ from random import randrange # my modules from cadquery import * from cadquery import exporters -from tests import BaseTest, writeStringToFile, makeUnitCube, readFileAsString, makeUnitSquareWire, makeCube +from tests import ( + BaseTest, + writeStringToFile, + makeUnitCube, + readFileAsString, + makeUnitSquareWire, + makeCube, +) # where unit test output will be saved OUTDIR = tempfile.gettempdir() @@ -45,7 +52,6 @@ writeStringToFile(SUMMARY_TEMPLATE, SUMMARY_FILE) class TestCadQuery(BaseTest): - def tearDown(self): """ Update summary with data from this test. @@ -61,12 +67,15 @@ class TestCadQuery(BaseTest): existingSummary = readFileAsString(SUMMARY_FILE) svgText = readFileAsString(svgFile) svgText = svgText.replace( - '', "") + '', "" + ) # now write data into the file # the content we are replacing it with also includes the marker, so it can be replaced again - existingSummary = existingSummary.replace("", TEST_RESULT_TEMPLATE % ( - dict(svg=svgText, name=self._testMethodName))) + existingSummary = existingSummary.replace( + "", + TEST_RESULT_TEMPLATE % (dict(svg=svgText, name=self._testMethodName)), + ) writeStringToFile(existingSummary, SUMMARY_FILE) @@ -82,25 +91,27 @@ class TestCadQuery(BaseTest): """ Tests to make sure that a CadQuery object is converted correctly to a OCC object. """ - r = Workplane('XY').rect(5, 5).extrude(5) + r = Workplane("XY").rect(5, 5).extrude(5) r = r.toOCC() import OCC.Core as OCC + self.assertEqual(type(r), OCC.TopoDS.TopoDS_Compound) def testToSVG(self): """ Tests to make sure that a CadQuery object is converted correctly to SVG """ - r = Workplane('XY').rect(5, 5).extrude(5) + r = Workplane("XY").rect(5, 5).extrude(5) r_str = r.toSvg() # Make sure that a couple of sections from the SVG output make sense self.assertTrue(r_str.index('path d="M') > 0) - self.assertTrue(r_str.index( - 'line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3"') > 0) + self.assertTrue( + r_str.index('line x1="30" y1="-30" x2="58" y2="-15" stroke-width="3"') > 0 + ) def testCubePlugin(self): """ @@ -126,8 +137,13 @@ class TestCadQuery(BaseTest): Workplane.makeCubes = makeCubes # call it - result = Workplane("XY").box(6.0, 8.0, 0.5).faces( - ">Z").rect(4.0, 4.0, forConstruction=True).vertices() + result = ( + Workplane("XY") + .box(6.0, 8.0, 0.5) + .faces(">Z") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + ) result = result.makeCubes(1.0) result = result.combineSolids() self.saveModel(result) @@ -143,7 +159,6 @@ class TestCadQuery(BaseTest): """ def cylinders(self, radius, height): - def _cyl(pnt): # inner function to build a cylinder return Solid.makeCylinder(radius, height, pnt) @@ -151,11 +166,16 @@ class TestCadQuery(BaseTest): # combine all the cylinders into a single compound r = self.eachpoint(_cyl, True).combineSolids() return r + Workplane.cyl = cylinders # now test. here we want weird workplane to see if the objects are transformed right - s = Workplane(Plane(Vector((0, 0, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)))).rect(2.0, 3.0, forConstruction=True).vertices() \ + s = ( + Workplane(Plane(Vector((0, 0, 0)), Vector((1, -1, 0)), Vector((1, 1, 0)))) + .rect(2.0, 3.0, forConstruction=True) + .vertices() .cyl(0.25, 0.5) + ) self.assertEqual(4, s.solids().size()) self.saveModel(s) @@ -168,22 +188,35 @@ class TestCadQuery(BaseTest): """ def rPoly(self, nSides, diameter): - def _makePolygon(center): # pnt is a vector in local coordinates angle = 2.0 * math.pi / nSides pnts = [] for i in range(nSides + 1): - pnts.append(center + Vector((diameter / 2.0 * math.cos(angle * i)), - (diameter / 2.0 * math.sin(angle * i)), 0)) + pnts.append( + center + + Vector( + (diameter / 2.0 * math.cos(angle * i)), + (diameter / 2.0 * math.sin(angle * i)), + 0, + ) + ) return Wire.makePolygon(pnts) return self.eachpoint(_makePolygon, True) Workplane.rPoly = rPoly - s = Workplane("XY").box(4.0, 4.0, 0.25).faces(">Z").workplane().rect(2.0, 2.0, forConstruction=True).vertices()\ - .rPoly(5, 0.5).cutThruAll() + s = ( + Workplane("XY") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .workplane() + .rect(2.0, 2.0, forConstruction=True) + .vertices() + .rPoly(5, 0.5) + .cutThruAll() + ) # 6 base sides, 4 pentagons, 5 sides each = 26 self.assertEqual(26, s.faces().size()) @@ -195,8 +228,7 @@ class TestCadQuery(BaseTest): """ c = CQ(makeUnitCube()) - s = c.faces(">Z").workplane().pushPoints( - [(-0.3, 0.3), (0.3, 0.3), (0, 0)]) + s = c.faces(">Z").workplane().pushPoints([(-0.3, 0.3), (0.3, 0.3), (0, 0)]) self.assertEqual(3, s.size()) # TODO: is the ability to iterate over points with circle really worth it? # maybe we should just require using all() and a loop for this. the semantics and @@ -209,7 +241,7 @@ class TestCadQuery(BaseTest): def callback_fn(pnt): self.assertEqual((0.0, 0.0), (pnt.x, pnt.y)) - r = Workplane('XY') + r = Workplane("XY") r.objects = [] r.eachpoint(callback_fn) @@ -237,10 +269,8 @@ class TestCadQuery(BaseTest): """Test solid rotation at the CQ object level.""" box = Workplane("XY").box(1, 1, 5) box.rotate((0, 0, 0), (1, 0, 0), 90) - startPoint = box.faces("Z").circle(1.5)\ - .workplane(offset=3.0).rect(0.75, 0.5).loft(combine=True) + s = ( + Workplane("front") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .circle(1.5) + .workplane(offset=3.0) + .rect(0.75, 0.5) + .loft(combine=True) + ) self.saveModel(s) - #self.assertEqual(1,s.solids().size() ) - #self.assertEqual(8,s.faces().size() ) + # self.assertEqual(1,s.solids().size() ) + # self.assertEqual(8,s.faces().size() ) def testRevolveCylinder(self): """ @@ -319,56 +355,81 @@ class TestCadQuery(BaseTest): angle_degrees = 360.0 # Test revolve without any options for making a cylinder - result = Workplane("XY").rect( - rectangle_width, rectangle_length, False).revolve() + result = ( + Workplane("XY").rect(rectangle_width, rectangle_length, False).revolve() + ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) # Test revolve when only setting the angle to revolve through - result = Workplane("XY").rect( - rectangle_width, rectangle_length, False).revolve(angle_degrees) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length, False) + .revolve(angle_degrees) + ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) - result = Workplane("XY").rect( - rectangle_width, rectangle_length, False).revolve(270.0) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length, False) + .revolve(270.0) + ) self.assertEqual(5, result.faces().size()) self.assertEqual(6, result.vertices().size()) self.assertEqual(9, result.edges().size()) # Test when passing revolve the angle and the axis of revolution's start point - result = Workplane("XY").rect( - rectangle_width, rectangle_length).revolve(angle_degrees, (-5, -5)) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(angle_degrees, (-5, -5)) + ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) - result = Workplane("XY").rect( - rectangle_width, rectangle_length).revolve(270.0, (-5, -5)) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(270.0, (-5, -5)) + ) self.assertEqual(5, result.faces().size()) self.assertEqual(6, result.vertices().size()) self.assertEqual(9, result.edges().size()) # Test when passing revolve the angle and both the start and ends of the axis of revolution - result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve( - angle_degrees, (-5, -5), (-5, 5)) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(angle_degrees, (-5, -5), (-5, 5)) + ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) - result = Workplane("XY").rect( - rectangle_width, rectangle_length).revolve(270.0, (-5, -5), (-5, 5)) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(270.0, (-5, -5), (-5, 5)) + ) self.assertEqual(5, result.faces().size()) self.assertEqual(6, result.vertices().size()) self.assertEqual(9, result.edges().size()) # Testing all of the above without combine - result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve( - angle_degrees, (-5, -5), (-5, 5), False) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(angle_degrees, (-5, -5), (-5, 5), False) + ) self.assertEqual(3, result.faces().size()) self.assertEqual(2, result.vertices().size()) self.assertEqual(3, result.edges().size()) - result = Workplane("XY").rect(rectangle_width, rectangle_length).revolve( - 270.0, (-5, -5), (-5, 5), False) + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length) + .revolve(270.0, (-5, -5), (-5, 5), False) + ) self.assertEqual(5, result.faces().size()) self.assertEqual(6, result.vertices().size()) self.assertEqual(9, result.edges().size()) @@ -384,8 +445,11 @@ class TestCadQuery(BaseTest): rectangle_length = 10.0 angle_degrees = 360.0 - result = Workplane("XY").rect(rectangle_width, rectangle_length, True)\ + result = ( + Workplane("XY") + .rect(rectangle_width, rectangle_length, True) .revolve(angle_degrees, (20, 0), (20, 10)) + ) self.assertEqual(4, result.faces().size()) self.assertEqual(4, result.vertices().size()) self.assertEqual(6, result.edges().size()) @@ -404,18 +468,13 @@ class TestCadQuery(BaseTest): """ Tests construction of splines """ - pts = [ - (0, 0), - (0, 1), - (1, 2), - (2, 4) - ] + pts = [(0, 0), (0, 1), (1, 2), (2, 4)] # Spline path - just a smoke test path = Workplane("XZ").spline(pts).val() # Closed spline - path_closed = Workplane("XZ").spline(pts,periodic=True).val() + path_closed = Workplane("XZ").spline(pts, periodic=True).val() self.assertTrue(path_closed.IsClosed()) # attempt to build a valid face @@ -429,24 +488,19 @@ class TestCadQuery(BaseTest): self.assertFalse(f.isValid()) # Spline with explicit tangents - path_const = Workplane("XZ").spline(pts,tangents=((0,1),(1,0))).val() + path_const = Workplane("XZ").spline(pts, tangents=((0, 1), (1, 0))).val() self.assertFalse(path.tangentAt(0) == path_const.tangentAt(0)) self.assertFalse(path.tangentAt(1) == path_const.tangentAt(1)) - + # test include current - path1 = Workplane("XZ").spline(pts[1:],includeCurrent=True).val() - self.assertAlmostEqual(path.Length(),path1.Length()) + path1 = Workplane("XZ").spline(pts[1:], includeCurrent=True).val() + self.assertAlmostEqual(path.Length(), path1.Length()) def testSweep(self): """ Tests the operation of sweeping a wire(s) along a path """ - pts = [ - (0, 0), - (0, 1), - (1, 2), - (2, 4) - ] + pts = [(0, 0), (0, 1), (1, 2), (2, 4)] # Spline path path = Workplane("XZ").spline(pts) @@ -467,8 +521,7 @@ class TestCadQuery(BaseTest): self.assertEqual(3, result.edges().size()) # Test with makeSolid False and isFrenet True - result = Workplane("XY").circle(1.0).sweep( - path, makeSolid=False, isFrenet=True) + result = Workplane("XY").circle(1.0).sweep(path, makeSolid=False, isFrenet=True) self.assertEqual(1, result.faces().size()) self.assertEqual(3, result.edges().size()) @@ -481,7 +534,7 @@ class TestCadQuery(BaseTest): path = Workplane("XZ").polyline(pts) # Test defaults - result = Workplane("XY").circle(0.1).sweep(path,transition='transformed') + result = Workplane("XY").circle(0.1).sweep(path, transition="transformed") self.assertEqual(5, result.faces().size()) self.assertEqual(7, result.edges().size()) @@ -489,24 +542,40 @@ class TestCadQuery(BaseTest): path = Workplane("XZ").polyline(pts) # Test defaults - result = Workplane("XY").circle(0.2).circle(0.1).sweep(path,transition='transformed') + result = ( + Workplane("XY") + .circle(0.2) + .circle(0.1) + .sweep(path, transition="transformed") + ) self.assertEqual(8, result.faces().size()) self.assertEqual(14, result.edges().size()) # Polyline path and different transition settings - for t in ('transformed','right','round'): + for t in ("transformed", "right", "round"): path = Workplane("XZ").polyline(pts) - result = Workplane("XY").circle(0.2).rect(0.2,0.1).rect(0.1,0.2)\ - .sweep(path,transition=t) + result = ( + Workplane("XY") + .circle(0.2) + .rect(0.2, 0.1) + .rect(0.1, 0.2) + .sweep(path, transition=t) + ) self.assertTrue(result.solids().val().isValid()) # Polyline path and multiple inner profiles path = Workplane("XZ").polyline(pts) # Test defaults - result = Workplane("XY").circle(0.2).rect(0.2,0.1).rect(0.1,0.2)\ - .circle(0.1).sweep(path) + result = ( + Workplane("XY") + .circle(0.2) + .rect(0.2, 0.1) + .rect(0.1, 0.2) + .circle(0.1) + .sweep(path) + ) self.assertTrue(result.solids().val().isValid()) # Arc path @@ -526,36 +595,79 @@ class TestCadQuery(BaseTest): path = Workplane("XZ").moveTo(-10, 0).lineTo(10, 0) # Sweep a circle from diameter 2.0 to diameter 1.0 to diameter 2.0 along X axis length 10.0 + 10.0 - defaultSweep = Workplane("YZ").workplane(offset=-10.0).circle(2.0). \ - workplane(offset=10.0).circle(1.0). \ - workplane(offset=10.0).circle(2.0).sweep(path, multisection=True) + defaultSweep = ( + Workplane("YZ") + .workplane(offset=-10.0) + .circle(2.0) + .workplane(offset=10.0) + .circle(1.0) + .workplane(offset=10.0) + .circle(2.0) + .sweep(path, multisection=True) + ) # We can sweep thrue different shapes - recttocircleSweep = Workplane("YZ").workplane(offset=-10.0).rect(2.0, 2.0). \ - workplane(offset=8.0).circle(1.0).workplane(offset=4.0).circle(1.0). \ - workplane(offset=8.0).rect(2.0, 2.0).sweep(path, multisection=True) + recttocircleSweep = ( + Workplane("YZ") + .workplane(offset=-10.0) + .rect(2.0, 2.0) + .workplane(offset=8.0) + .circle(1.0) + .workplane(offset=4.0) + .circle(1.0) + .workplane(offset=8.0) + .rect(2.0, 2.0) + .sweep(path, multisection=True) + ) - circletorectSweep = Workplane("YZ").workplane(offset=-10.0).circle(1.0). \ - workplane(offset=7.0).rect(2.0, 2.0).workplane(offset=6.0).rect(2.0, 2.0). \ - workplane(offset=7.0).circle(1.0).sweep(path, multisection=True) + circletorectSweep = ( + Workplane("YZ") + .workplane(offset=-10.0) + .circle(1.0) + .workplane(offset=7.0) + .rect(2.0, 2.0) + .workplane(offset=6.0) + .rect(2.0, 2.0) + .workplane(offset=7.0) + .circle(1.0) + .sweep(path, multisection=True) + ) # Placement of the Shape is important otherwise could produce unexpected shape - specialSweep = Workplane("YZ").circle(1.0).workplane(offset=10.0).rect(2.0, 2.0). \ - sweep(path, multisection=True) + specialSweep = ( + Workplane("YZ") + .circle(1.0) + .workplane(offset=10.0) + .rect(2.0, 2.0) + .sweep(path, multisection=True) + ) # Switch to an arc for the path : line l=5.0 then half circle r=4.0 then line l=5.0 - path = Workplane("XZ").moveTo(-5, 4).lineTo(0, 4). \ - threePointArc((4, 0), (0, -4)).lineTo(-5, -4) + path = ( + Workplane("XZ") + .moveTo(-5, 4) + .lineTo(0, 4) + .threePointArc((4, 0), (0, -4)) + .lineTo(-5, -4) + ) # Placement of different shapes should follow the path # cylinder r=1.5 along first line # then sweep allong arc from r=1.5 to r=1.0 # then cylinder r=1.0 along last line - arcSweep = Workplane("YZ").workplane(offset=-5).moveTo(0, 4).circle(1.5). \ - workplane(offset=5).circle(1.5). \ - moveTo(0, -8).circle(1.0). \ - workplane(offset=-5).circle(1.0). \ - sweep(path, multisection=True) + arcSweep = ( + Workplane("YZ") + .workplane(offset=-5) + .moveTo(0, 4) + .circle(1.5) + .workplane(offset=5) + .circle(1.5) + .moveTo(0, -8) + .circle(1.0) + .workplane(offset=-5) + .circle(1.0) + .sweep(path, multisection=True) + ) # Test and saveModel self.assertEqual(1, defaultSweep.solids().size()) @@ -569,7 +681,7 @@ class TestCadQuery(BaseTest): """ Tests extrusion while twisting through an angle. """ - profile = Workplane('XY').rect(10, 10) + profile = Workplane("XY").rect(10, 10) r = profile.twistExtrude(10, 45, False) self.assertEqual(6, r.faces().size()) @@ -578,7 +690,7 @@ class TestCadQuery(BaseTest): """ Tests extrusion while twisting through an angle, combining with other solids. """ - profile = Workplane('XY').rect(10, 10) + profile = Workplane("XY").rect(10, 10) r = profile.twistExtrude(10, 45) self.assertEqual(6, r.faces().size()) @@ -586,9 +698,16 @@ class TestCadQuery(BaseTest): def testRectArray(self): NUMX = 3 NUMY = 3 - s = Workplane("XY").box(40, 40, 5, centered=(True, True, True)).faces( - ">Z").workplane().rarray(8.0, 8.0, NUMX, NUMY, True).circle(2.0).extrude(2.0) - #s = Workplane("XY").box(40,40,5,centered=(True,True,True)).faces(">Z").workplane().circle(2.0).extrude(2.0) + s = ( + Workplane("XY") + .box(40, 40, 5, centered=(True, True, True)) + .faces(">Z") + .workplane() + .rarray(8.0, 8.0, NUMX, NUMY, True) + .circle(2.0) + .extrude(2.0) + ) + # s = Workplane("XY").box(40,40,5,centered=(True,True,True)).faces(">Z").workplane().circle(2.0).extrude(2.0) self.saveModel(s) # 6 faces for the box, 2 faces for each cylinder self.assertEqual(6 + NUMX * NUMY * 2, s.faces().size()) @@ -623,8 +742,14 @@ class TestCadQuery(BaseTest): self.assertAlmostEqual(radius, s.objects[0].y) def testNestedCircle(self): - s = Workplane("XY").box(40, 40, 5).pushPoints( - [(10, 0), (0, 10)]).circle(4).circle(2).extrude(4) + s = ( + Workplane("XY") + .box(40, 40, 5) + .pushPoints([(10, 0), (0, 10)]) + .circle(4) + .circle(2) + .extrude(4) + ) self.saveModel(s) self.assertEqual(14, s.faces().size()) @@ -651,8 +776,13 @@ class TestCadQuery(BaseTest): # build the brick s = Workplane("XY").box(total_length, total_width, H) # make the base s = s.faces("Z").workplane().rarray(P, P, lbumps, wbumps, True).circle( - bumpDiam / 2.0).extrude(1.8) # make the bumps on the top + s = ( + s.faces(">Z") + .workplane() + .rarray(P, P, lbumps, wbumps, True) + .circle(bumpDiam / 2.0) + .extrude(1.8) + ) # make the bumps on the top # add posts on the bottom. posts are different diameter depending on geometry # solid studs for 1 bump, tubes for multiple, none for 1x1 @@ -660,32 +790,40 @@ class TestCadQuery(BaseTest): tmp = s.faces(" 1 and wbumps > 1: - tmp = tmp.rarray(P, P, lbumps - 1, wbumps - 1, center=True).circle( - postDiam / 2.0).circle(bumpDiam / 2.0).extrude(H - t) + tmp = ( + tmp.rarray(P, P, lbumps - 1, wbumps - 1, center=True) + .circle(postDiam / 2.0) + .circle(bumpDiam / 2.0) + .extrude(H - t) + ) elif lbumps > 1: - tmp = tmp.rarray(P, P, lbumps - 1, 1, - center=True).circle(t).extrude(H - t) + tmp = tmp.rarray(P, P, lbumps - 1, 1, center=True).circle(t).extrude(H - t) elif wbumps > 1: - tmp = tmp.rarray(P, P, 1, wbumps - 1, - center=True).circle(t).extrude(H - t) + tmp = tmp.rarray(P, P, 1, wbumps - 1, center=True).circle(t).extrude(H - t) self.saveModel(s) def testAngledHoles(self): - s = Workplane("front").box(4.0, 4.0, 0.25).faces(">Z").workplane().transformed(offset=Vector(0, -1.5, 1.0), rotate=Vector(60, 0, 0))\ - .rect(1.5, 1.5, forConstruction=True).vertices().hole(0.25) + s = ( + Workplane("front") + .box(4.0, 4.0, 0.25) + .faces(">Z") + .workplane() + .transformed(offset=Vector(0, -1.5, 1.0), rotate=Vector(60, 0, 0)) + .rect(1.5, 1.5, forConstruction=True) + .vertices() + .hole(0.25) + ) self.saveModel(s) self.assertEqual(10, s.faces().size()) def testTranslateSolid(self): c = CQ(makeUnitCube()) - self.assertAlmostEqual(0.0, c.faces( - "Z').workplane().circle(0.125).extrude( - 0.5, True) # make a boss, not updating the original + r = ( + c.faces(">Z").workplane().circle(0.125).extrude(0.5, True) + ) # make a boss, not updating the original self.assertEqual(8, r.faces().size()) # just the boss faces self.assertEqual(6, c.faces().size()) # original is not modified @@ -737,9 +876,14 @@ class TestCadQuery(BaseTest): A simple square part with a hole in it """ s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).extrude(0.5)\ - .faces(">Z").workplane()\ - .circle(0.25).cutBlind(-1.0) + r = ( + s.rect(2.0, 2.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .circle(0.25) + .cutBlind(-1.0) + ) self.saveModel(r) self.assertEqual(7, r.faces().size()) @@ -749,13 +893,12 @@ class TestCadQuery(BaseTest): Test Creation of workplane from multiple co-planar face selection. """ - s = Workplane('XY').box(1, 1, 1).faces( - '>Z').rect(1, 0.5).cutBlind(-0.2) + s = Workplane("XY").box(1, 1, 1).faces(">Z").rect(1, 0.5).cutBlind(-0.2) - w = s.faces('>Z').workplane() + w = s.faces(">Z").workplane() o = w.objects[0] # origin of the workplane - self.assertAlmostEqual(o.x, 0., 3) - self.assertAlmostEqual(o.y, 0., 3) + self.assertAlmostEqual(o.x, 0.0, 3) + self.assertAlmostEqual(o.y, 0.0, 3) self.assertAlmostEqual(o.z, 0.5, 3) def testTriangularPrism(self): @@ -779,8 +922,13 @@ class TestCadQuery(BaseTest): also tests using a workplane plane other than XY """ s = Workplane(Plane.YZ()) - r = s.rect(2.0, 2.0).rect( - 1.3, 1.3, forConstruction=True).vertices().circle(0.125).extrude(0.5) + r = ( + s.rect(2.0, 2.0) + .rect(1.3, 1.3, forConstruction=True) + .vertices() + .circle(0.125) + .extrude(0.5) + ) self.saveModel(r) # 10 faces-- 6 plus 4 holes, the vertices of the second rect. self.assertEqual(10, r.faces().size()) @@ -798,8 +946,13 @@ class TestCadQuery(BaseTest): # r = s.rect(2.0,2.0).rect(1.3,1.3,forConstruction=True).vertices() # for c in r.all(): # c.circle(0.125).extrude(0.5,True) - r = s.rect(2.0, 2.0).rect( - 1.3, 1.3, forConstruction=True).vertices().circle(0.125).extrude(0.5) + r = ( + s.rect(2.0, 2.0) + .rect(1.3, 1.3, forConstruction=True) + .vertices() + .circle(0.125) + .extrude(0.5) + ) # side hole, blind deep 1.9 t = r.faces(">Y").workplane().circle(0.125).cutBlind(-1.9) @@ -829,43 +982,45 @@ class TestCadQuery(BaseTest): resS = currentS.intersect(toIntersect.val()) self.assertEqual(6, resS.faces().size()) - self.assertAlmostEqual(resS.val().Volume(),0.5) + self.assertAlmostEqual(resS.val().Volume(), 0.5) resS = currentS.intersect(toIntersect) self.assertEqual(6, resS.faces().size()) - self.assertAlmostEqual(resS.val().Volume(),0.5) + self.assertAlmostEqual(resS.val().Volume(), 0.5) def testBoundingBox(self): """ Tests the boudingbox center of a model """ - result0 = (Workplane("XY") - .moveTo(10, 0) - .lineTo(5, 0) - .threePointArc((3.9393, 0.4393), (3.5, 1.5)) - .threePointArc((3.0607, 2.5607), (2, 3)) - .lineTo(1.5, 3) - .threePointArc((0.4393, 3.4393), (0, 4.5)) - .lineTo(0, 13.5) - .threePointArc((0.4393, 14.5607), (1.5, 15)) - .lineTo(28, 15) - .lineTo(28, 13.5) - .lineTo(24, 13.5) - .lineTo(24, 11.5) - .lineTo(27, 11.5) - .lineTo(27, 10) - .lineTo(22, 10) - .lineTo(22, 13.2) - .lineTo(14.5, 13.2) - .lineTo(14.5, 10) - .lineTo(12.5, 10) - .lineTo(12.5, 13.2) - .lineTo(5.5, 13.2) - .lineTo(5.5, 2) - .threePointArc((5.793, 1.293), (6.5, 1)) - .lineTo(10, 1) - .close()) + result0 = ( + Workplane("XY") + .moveTo(10, 0) + .lineTo(5, 0) + .threePointArc((3.9393, 0.4393), (3.5, 1.5)) + .threePointArc((3.0607, 2.5607), (2, 3)) + .lineTo(1.5, 3) + .threePointArc((0.4393, 3.4393), (0, 4.5)) + .lineTo(0, 13.5) + .threePointArc((0.4393, 14.5607), (1.5, 15)) + .lineTo(28, 15) + .lineTo(28, 13.5) + .lineTo(24, 13.5) + .lineTo(24, 11.5) + .lineTo(27, 11.5) + .lineTo(27, 10) + .lineTo(22, 10) + .lineTo(22, 13.2) + .lineTo(14.5, 13.2) + .lineTo(14.5, 10) + .lineTo(12.5, 10) + .lineTo(12.5, 13.2) + .lineTo(5.5, 13.2) + .lineTo(5.5, 2) + .threePointArc((5.793, 1.293), (6.5, 1)) + .lineTo(10, 1) + .close() + ) result = result0.extrude(100) bb_center = result.val().BoundingBox().center self.saveModel(result) @@ -888,9 +1043,13 @@ class TestCadQuery(BaseTest): """ # base block s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).rect( - 1.3, 1.3, forConstruction=True).vertices().circle(0.125).extrude(0.5) - + r = ( + s.rect(2.0, 2.0) + .rect(1.3, 1.3, forConstruction=True) + .vertices() + .circle(0.125) + .extrude(0.5) + ) # thru all without explicit face selection t = r.circle(0.5).cutThruAll() @@ -907,31 +1066,47 @@ class TestCadQuery(BaseTest): """ # base block s = Workplane(Plane.XY()) - r = s.rect(2.0, 2.0).rect( - 1.3, 1.3, forConstruction=True).vertices().circle(0.125).extrude(0.5) + r = ( + s.rect(2.0, 2.0) + .rect(1.3, 1.3, forConstruction=True) + .vertices() + .circle(0.125) + .extrude(0.5) + ) # side hole, up to 0.1 from the last face try: - t = r.faces(">Y").workplane().circle( - 0.125).cutToOffsetFromFace(r.faces().mminDist(Dir.Y), 0.1) + t = ( + r.faces(">Y") + .workplane() + .circle(0.125) + .cutToOffsetFromFace(r.faces().mminDist(Dir.Y), 0.1) + ) # should end up being a blind hole self.assertEqual(10, t.faces().size()) - t.first().val().exportStep('c:/temp/testCutToFace.STEP') + t.first().val().exportStep("c:/temp/testCutToFace.STEP") except: pass # Not Implemented Yet def testWorkplaneOnExistingSolid(self): "Tests extruding on an existing solid" - c = CQ(makeUnitCube()).faces(">Z").workplane().circle( - 0.25).circle(0.125).extrude(0.25) + c = ( + CQ(makeUnitCube()) + .faces(">Z") + .workplane() + .circle(0.25) + .circle(0.125) + .extrude(0.25) + ) self.saveModel(c) self.assertEqual(10, c.faces().size()) def testWorkplaneCenterMove(self): # this workplane is centered at x=0.5,y=0.5, the center of the upper face - s = Workplane("XY").box(1, 1, 1).faces(">Z").workplane( - ).center(-0.5, -0.5) # move the center to the corner + s = ( + Workplane("XY").box(1, 1, 1).faces(">Z").workplane().center(-0.5, -0.5) + ) # move the center to the corner t = s.circle(0.25).extrude(0.2) # make a boss self.assertEqual(9, t.faces().size()) @@ -946,7 +1121,7 @@ class TestCadQuery(BaseTest): # most users dont understand what a wire is, they are just drawing r = s.lineTo(1.0, 0).lineTo(0, 1.0).close().wire().extrude(0.25) - r.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesStep1.STEP')) + r.val().exportStep(os.path.join(OUTDIR, "testBasicLinesStep1.STEP")) # no faces on the original workplane self.assertEqual(0, s.faces().size()) @@ -956,12 +1131,12 @@ class TestCadQuery(BaseTest): # now add a circle through a side face r1 = r.faces("+XY").workplane().circle(0.08).cutThruAll() self.assertEqual(6, r1.faces().size()) - r1.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesXY.STEP')) + r1.val().exportStep(os.path.join(OUTDIR, "testBasicLinesXY.STEP")) # now add a circle through a top r2 = r1.faces("+Z").workplane().circle(0.08).cutThruAll() self.assertEqual(9, r2.faces().size()) - r2.val().exportStep(os.path.join(OUTDIR, 'testBasicLinesZ.STEP')) + r2.val().exportStep(os.path.join(OUTDIR, "testBasicLinesZ.STEP")) self.saveModel(r2) @@ -970,17 +1145,19 @@ class TestCadQuery(BaseTest): Draw things like 2D lines and arcs, should be expanded later to include all 2D constructs """ s = Workplane(Plane.XY()) - r = s.lineTo(1.0, 0.0) \ - .lineTo(1.0, 1.0) \ - .threePointArc((1.0, 1.5), (0.0, 1.0)) \ - .lineTo(0.0, 0.0) \ - .moveTo(1.0, 0.0) \ - .lineTo(2.0, 0.0) \ - .lineTo(2.0, 2.0) \ - .threePointArc((2.0, 2.5), (0.0, 2.0)) \ - .lineTo(-2.0, 2.0) \ - .lineTo(-2.0, 0.0) \ - .close() + r = ( + s.lineTo(1.0, 0.0) + .lineTo(1.0, 1.0) + .threePointArc((1.0, 1.5), (0.0, 1.0)) + .lineTo(0.0, 0.0) + .moveTo(1.0, 0.0) + .lineTo(2.0, 0.0) + .lineTo(2.0, 2.0) + .threePointArc((2.0, 2.5), (0.0, 2.0)) + .lineTo(-2.0, 2.0) + .lineTo(-2.0, 0.0) + .close() + ) self.assertEqual(1, r.wires().size()) @@ -1004,12 +1181,19 @@ class TestCadQuery(BaseTest): self.assertEqual(1, r.wire().size()) self.assertEqual(4, r.edges().size()) - self.assertEqual((1.0, 1.0), - (r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0))) - .first().val().X, - r.vertices( - selectors.NearestToPointSelector((0.0, 0.0, 0.0))) - .first().val().Y)) + self.assertEqual( + (1.0, 1.0), + ( + r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0))) + .first() + .val() + .X, + r.vertices(selectors.NearestToPointSelector((0.0, 0.0, 0.0))) + .first() + .val() + .Y, + ), + ) # Test the sagittaArc and radiusArc functions a1 = Workplane(Plane.YZ()).threePointArc((5, 1), (10, 0)) @@ -1017,13 +1201,13 @@ class TestCadQuery(BaseTest): a3 = Workplane(Plane.YZ()).threePointArc((6, 2), (12, 0)) a4 = Workplane(Plane.YZ()).radiusArc((12, 0), -10) - assert(a1.edges().first().val().geomType() == "CIRCLE") - assert(a2.edges().first().val().geomType() == "CIRCLE") - assert(a3.edges().first().val().geomType() == "CIRCLE") - assert(a4.edges().first().val().geomType() == "CIRCLE") + assert a1.edges().first().val().geomType() == "CIRCLE" + assert a2.edges().first().val().geomType() == "CIRCLE" + assert a3.edges().first().val().geomType() == "CIRCLE" + assert a4.edges().first().val().geomType() == "CIRCLE" - assert(a1.edges().first().val().Length() == a2.edges().first().val().Length()) - assert(a3.edges().first().val().Length() == a4.edges().first().val().Length()) + assert a1.edges().first().val().Length() == a2.edges().first().val().Length() + assert a3.edges().first().val().Length() == a4.edges().first().val().Length() def testPolarLines(self): """ @@ -1032,11 +1216,13 @@ class TestCadQuery(BaseTest): # Test the PolarLine* functions s = Workplane(Plane.XY()) - r = s.polarLine(10, 45) \ - .polarLineTo(10, -45) \ - .polarLine(10, -180) \ - .polarLine(-10, -90) \ + r = ( + s.polarLine(10, 45) + .polarLineTo(10, -45) + .polarLine(10, -180) + .polarLine(-10, -90) .close() + ) # a single wire, 5 edges self.assertEqual(1, r.wires().size()) @@ -1046,12 +1232,12 @@ class TestCadQuery(BaseTest): """ Tests the largestDimension function when no solids are on the stack and when there are """ - r = Workplane('XY').box(1, 1, 1) + r = Workplane("XY").box(1, 1, 1) dim = r.largestDimension() self.assertAlmostEqual(8.7, dim, 1) - r = Workplane('XY') + r = Workplane("XY") dim = r.largestDimension() self.assertEqual(-1, dim) @@ -1067,12 +1253,19 @@ class TestCadQuery(BaseTest): s = Workplane(Plane.XY()) # draw half the profile of the bottle - p = s.center(-L / 2.0, 0).vLine(w / 2.0).threePointArc((L / 2.0, w / 2.0 + t), (L, w / 2.0)).vLine(-w / 2.0).mirrorX()\ + p = ( + s.center(-L / 2.0, 0) + .vLine(w / 2.0) + .threePointArc((L / 2.0, w / 2.0 + t), (L, w / 2.0)) + .vLine(-w / 2.0) + .mirrorX() .extrude(30.0, True) + ) # make the neck p.faces(">Z").workplane().circle(3.0).extrude( - 2.0, True) # .edges().fillet(0.05) + 2.0, True + ) # .edges().fillet(0.05) # make a shell p.faces(">Z").shell(0.3) @@ -1090,7 +1283,7 @@ class TestCadQuery(BaseTest): (1.5, 1.0), (1.0, 1.25), (0.5, 1.0), - (0, 1.0) + (0, 1.0), ] r = s.lineTo(3.0, 0).lineTo(3.0, 1.0).spline(sPnts).close() r = r.extrude(0.5) @@ -1100,8 +1293,13 @@ class TestCadQuery(BaseTest): """ Tests a simple mirroring operation """ - s = Workplane("XY").lineTo(2, 2).threePointArc((3, 1), (2, 0)) \ - .mirrorX().extrude(0.25) + s = ( + Workplane("XY") + .lineTo(2, 2) + .threePointArc((3, 1), (2, 0)) + .mirrorX() + .extrude(0.25) + ) self.assertEqual(6, s.faces().size()) self.saveModel(s) @@ -1123,7 +1321,7 @@ class TestCadQuery(BaseTest): (r / 2, s / 2), (r / 2 - t, s / 2), (r / 2 - t, r / 2 - 1.5 * t), - (t / 2, 0) + (t / 2, 0), ] r = Workplane("XY").polyline(points).mirrorX() @@ -1131,13 +1329,12 @@ class TestCadQuery(BaseTest): self.assertEqual(1, r.wires().size()) self.assertEqual(18, r.edges().size()) - # try the same with includeCurrent=True - r = Workplane("XY").polyline(points[1:],includeCurrent=True).mirrorX() + # try the same with includeCurrent=True + r = Workplane("XY").polyline(points[1:], includeCurrent=True).mirrorX() self.assertEqual(1, r.wires().size()) self.assertEqual(18, r.edges().size()) - def testChainedMirror(self): """ Tests whether or not calling mirrorX().mirrorY() works correctly @@ -1145,23 +1342,22 @@ class TestCadQuery(BaseTest): r = 20 s = 7 t = 1.5 - + points = [ - (0, 0), - (0, t/2), - (r/2-1.5*t, r/2-t), - (s/2, r/2-t), - (s/2, r/2), - (r/2, r/2), - (r/2, s/2), - (r/2-t, s/2), - (r/2-t, r/2-1.5*t), - (t/2, 0) + (0, 0), + (0, t / 2), + (r / 2 - 1.5 * t, r / 2 - t), + (s / 2, r / 2 - t), + (s / 2, r / 2), + (r / 2, r / 2), + (r / 2, s / 2), + (r / 2 - t, s / 2), + (r / 2 - t, r / 2 - 1.5 * t), + (t / 2, 0), ] - - r = Workplane("XY").polyline(points).mirrorX().mirrorY() \ - .extrude(1).faces('>Z') - + + r = Workplane("XY").polyline(points).mirrorX().mirrorY().extrude(1).faces(">Z") + self.assertEquals(1, r.wires().size()) self.assertEquals(32, r.edges().size()) @@ -1191,7 +1387,7 @@ class TestCadQuery(BaseTest): (t / 2.0, (t - H / 2.0)), (W / 2.0, (t - H / 2.0)), (W / 2.0, H / -2.0), - (0, H / -2.0) + (0, H / -2.0), ] r = s.polyline(pts).mirrorY() # these other forms also work res = r.extrude(L) @@ -1210,8 +1406,15 @@ class TestCadQuery(BaseTest): """ Tests filleting edges on a solid """ - c = CQ(makeUnitCube()).faces(">Z").workplane().circle( - 0.25).extrude(0.25, True).edges("|Z").fillet(0.2) + c = ( + CQ(makeUnitCube()) + .faces(">Z") + .workplane() + .circle(0.25) + .extrude(0.25, True) + .edges("|Z") + .fillet(0.2) + ) self.saveModel(c) self.assertEqual(12, c.faces().size()) @@ -1241,8 +1444,7 @@ class TestCadQuery(BaseTest): """ Test chamfer API with a cylinder shape """ - cylinder = Workplane("XY").circle( - 1).extrude(1).faces(">Z").chamfer(0.1) + cylinder = Workplane("XY").circle(1).extrude(1).faces(">Z").chamfer(0.1) self.saveModel(cylinder) self.assertEqual(4, cylinder.faces().size()) @@ -1251,21 +1453,15 @@ class TestCadQuery(BaseTest): Tests making a set of counterbored holes in a face """ c = CQ(makeCube(3.0)) - pnts = [ - (-1.0, -1.0), (0.0, 0.0), (1.0, 1.0) - ] - c = c.faces(">Z").workplane().pushPoints( - pnts).cboreHole(0.1, 0.25, 0.25, 0.75) + pnts = [(-1.0, -1.0), (0.0, 0.0), (1.0, 1.0)] + c = c.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25, 0.75) self.assertEqual(18, c.faces().size()) self.saveModel(c) # Tests the case where the depth of the cboreHole is not specified c2 = CQ(makeCube(3.0)) - pnts = [ - (-1.0, -1.0), (0.0, 0.0), (1.0, 1.0) - ] - c2 = c2.faces(">Z").workplane().pushPoints( - pnts).cboreHole(0.1, 0.25, 0.25) + pnts = [(-1.0, -1.0), (0.0, 0.0), (1.0, 1.0)] + c2 = c2.faces(">Z").workplane().pushPoints(pnts).cboreHole(0.1, 0.25, 0.25) self.assertEqual(15, c2.faces().size()) def testCounterSinks(self): @@ -1273,8 +1469,15 @@ class TestCadQuery(BaseTest): Tests countersinks """ s = Workplane(Plane.XY()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) def testSplitKeepingHalf(self): @@ -1283,8 +1486,7 @@ class TestCadQuery(BaseTest): """ # drill a hole in the side - c = CQ(makeUnitCube()).faces( - ">Z").workplane().circle(0.25).cutThruAll() + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() self.assertEqual(7, c.faces().size()) @@ -1299,13 +1501,11 @@ class TestCadQuery(BaseTest): """ # drill a hole in the side - c = CQ(makeUnitCube()).faces( - ">Z").workplane().circle(0.25).cutThruAll() + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() self.assertEqual(7, c.faces().size()) # now cut it in half sideways - result = c.faces( - ">Y").workplane(-0.5).split(keepTop=True, keepBottom=True) + result = c.faces(">Y").workplane(-0.5).split(keepTop=True, keepBottom=True) # stack will have both halves, original will be unchanged # two solids are on the stack, eac @@ -1318,13 +1518,11 @@ class TestCadQuery(BaseTest): Tests splitting a solid improperly """ # Drill a hole in the side - c = CQ(makeUnitCube()).faces( - ">Z").workplane().circle(0.25).cutThruAll() + c = CQ(makeUnitCube()).faces(">Z").workplane().circle(0.25).cutThruAll() self.assertEqual(7, c.faces().size()) # Now cut it in half sideways - result = c.faces( - ">Y").workplane(-0.5).split(keepTop=False, keepBottom=True) + result = c.faces(">Y").workplane(-0.5).split(keepTop=False, keepBottom=True) # stack will have both halves, original will be unchanged # one solid is on the stack @@ -1370,22 +1568,37 @@ class TestCadQuery(BaseTest): """ Tests creating an array of boxes """ - s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().box( - 0.25, 0.25, 0.25, combine=True) + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .box(0.25, 0.25, 0.25, combine=True) + ) # 1 object, 4 solids because the object is a compound self.assertEqual(4, s.solids().size()) self.assertEqual(1, s.size()) self.saveModel(s) - s = Workplane("XY").rect(4.0, 4.0, forConstruction=True).vertices().box( - 0.25, 0.25, 0.25, combine=False) + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .box(0.25, 0.25, 0.25, combine=False) + ) # 4 objects, 4 solids, because each is a separate solid self.assertEqual(4, s.size()) self.assertEqual(4, s.solids().size()) def testBoxCombine(self): - s = Workplane("XY").box(4, 4, 0.5).faces(">Z").workplane().rect( - 3, 3, forConstruction=True).vertices().box(0.25, 0.25, 0.25, combine=True) + s = ( + Workplane("XY") + .box(4, 4, 0.5) + .faces(">Z") + .workplane() + .rect(3, 3, forConstruction=True) + .vertices() + .box(0.25, 0.25, 0.25, combine=True) + ) self.saveModel(s) self.assertEqual(1, s.solids().size()) # we should have one big solid @@ -1394,27 +1607,36 @@ class TestCadQuery(BaseTest): def testSphereDefaults(self): s = Workplane("XY").sphere(10) - self.saveModel(s) # Until FreeCAD fixes their sphere operation + self.saveModel(s) # Until FreeCAD fixes their sphere operation self.assertEqual(1, s.solids().size()) self.assertEqual(1, s.faces().size()) def testSphereCustom(self): - s = Workplane("XY").sphere(10, angle1=0, angle2=90, - angle3=360, centered=(False, False, False)) + s = Workplane("XY").sphere( + 10, angle1=0, angle2=90, angle3=360, centered=(False, False, False) + ) self.saveModel(s) self.assertEqual(1, s.solids().size()) self.assertEqual(2, s.faces().size()) def testSpherePointList(self): - s = Workplane("XY").rect( - 4.0, 4.0, forConstruction=True).vertices().sphere(0.25, combine=False) + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .sphere(0.25, combine=False) + ) # self.saveModel(s) # Until FreeCAD fixes their sphere operation self.assertEqual(4, s.solids().size()) self.assertEqual(4, s.faces().size()) def testSphereCombine(self): - s = Workplane("XY").rect( - 4.0, 4.0, forConstruction=True).vertices().sphere(2.25, combine=True) + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .sphere(2.25, combine=True) + ) # self.saveModel(s) # Until FreeCAD fixes their sphere operation self.assertEqual(1, s.solids().size()) self.assertEqual(4, s.faces().size()) @@ -1427,62 +1649,111 @@ class TestCadQuery(BaseTest): self.assertEqual(5, s.vertices().size()) def testWedgeCentering(self): - s = Workplane("XY").wedge(10, 10, 10, 5, 5, 5, 5, centered=(False, False, False)) + s = Workplane("XY").wedge( + 10, 10, 10, 5, 5, 5, 5, centered=(False, False, False) + ) # self.saveModel(s) self.assertEqual(1, s.solids().size()) self.assertEqual(5, s.faces().size()) self.assertEqual(5, s.vertices().size()) def testWedgePointList(self): - s = Workplane("XY").rect( - 4.0, 4.0, forConstruction=True).vertices().wedge(10, 10, 10, 5, 5, 5, 5, combine=False) - #self.saveModel(s) + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .wedge(10, 10, 10, 5, 5, 5, 5, combine=False) + ) + # self.saveModel(s) self.assertEqual(4, s.solids().size()) self.assertEqual(20, s.faces().size()) self.assertEqual(20, s.vertices().size()) def testWedgeCombined(self): - s = Workplane("XY").rect( - 4.0, 4.0, forConstruction=True).vertices().wedge(10, 10, 10, 5, 5, 5, 5, combine=True) + s = ( + Workplane("XY") + .rect(4.0, 4.0, forConstruction=True) + .vertices() + .wedge(10, 10, 10, 5, 5, 5, 5, combine=True) + ) # self.saveModel(s) self.assertEqual(1, s.solids().size()) self.assertEqual(12, s.faces().size()) self.assertEqual(16, s.vertices().size()) def testQuickStartXY(self): - s = Workplane(Plane.XY()).box(2, 4, 0.5).faces(">Z").workplane().rect(1.5, 3.5, forConstruction=True)\ - .vertices().cskHole(0.125, 0.25, 82, depth=None) + s = ( + Workplane(Plane.XY()) + .box(2, 4, 0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.assertEqual(1, s.solids().size()) self.assertEqual(14, s.faces().size()) self.saveModel(s) def testQuickStartYZ(self): - s = Workplane(Plane.YZ()).box(2, 4, 0.5).faces(">X").workplane().rect(1.5, 3.5, forConstruction=True)\ - .vertices().cskHole(0.125, 0.25, 82, depth=None) + s = ( + Workplane(Plane.YZ()) + .box(2, 4, 0.5) + .faces(">X") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.assertEqual(1, s.solids().size()) self.assertEqual(14, s.faces().size()) self.saveModel(s) def testQuickStartXZ(self): - s = Workplane(Plane.XZ()).box(2, 4, 0.5).faces(">Y").workplane().rect(1.5, 3.5, forConstruction=True)\ - .vertices().cskHole(0.125, 0.25, 82, depth=None) + s = ( + Workplane(Plane.XZ()) + .box(2, 4, 0.5) + .faces(">Y") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.assertEqual(1, s.solids().size()) self.assertEqual(14, s.faces().size()) self.saveModel(s) def testDoubleTwistedLoft(self): - s = Workplane("XY").polygon(8, 20.0).workplane(offset=4.0).transformed( - rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() - s2 = Workplane("XY").polygon(8, 20.0).workplane( - offset=-4.0).transformed(rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() + s = ( + Workplane("XY") + .polygon(8, 20.0) + .workplane(offset=4.0) + .transformed(rotate=Vector(0, 0, 15.0)) + .polygon(8, 20) + .loft() + ) + s2 = ( + Workplane("XY") + .polygon(8, 20.0) + .workplane(offset=-4.0) + .transformed(rotate=Vector(0, 0, 15.0)) + .polygon(8, 20) + .loft() + ) # self.assertEquals(10,s.faces().size()) # self.assertEquals(1,s.solids().size()) s3 = s.combineSolids(s2) self.saveModel(s3) def testTwistedLoft(self): - s = Workplane("XY").polygon(8, 20.0).workplane(offset=4.0).transformed( - rotate=Vector(0, 0, 15.0)).polygon(8, 20).loft() + s = ( + Workplane("XY") + .polygon(8, 20.0) + .workplane(offset=4.0) + .transformed(rotate=Vector(0, 0, 15.0)) + .polygon(8, 20) + .loft() + ) self.assertEqual(10, s.faces().size()) self.assertEqual(1, s.solids().size()) self.saveModel(s) @@ -1507,13 +1778,12 @@ class TestCadQuery(BaseTest): toUnion = s.rect(1.0, 1.0).extrude(1.0) resS = currentS.union(toUnion) - - self.assertEqual(11,resS.faces().size()) + + self.assertEqual(11, resS.faces().size()) def testCombine(self): s = Workplane(Plane.XY()) - objects1 = s.rect(2.0, 2.0).extrude(0.5).faces( - '>Z').rect(1.0, 1.0).extrude(0.5) + objects1 = s.rect(2.0, 2.0).extrude(0.5).faces(">Z").rect(1.0, 1.0).extrude(0.5) objects1.combine() @@ -1544,33 +1814,64 @@ class TestCadQuery(BaseTest): # make a cube with a splitter edge on one of the faces # autosimplify should remove the splitter - s = Workplane("XY").moveTo(0, 0).line(5, 0).line(5, 0).line(0, 10).\ - line(-10, 0).close().extrude(10) + s = ( + Workplane("XY") + .moveTo(0, 0) + .line(5, 0) + .line(5, 0) + .line(0, 10) + .line(-10, 0) + .close() + .extrude(10) + ) self.assertEqual(6, s.faces().size()) # test removal of splitter caused by union operation - s = Workplane("XY").box(10, 10, 10).union( - Workplane("XY").box(20, 10, 10)) + s = Workplane("XY").box(10, 10, 10).union(Workplane("XY").box(20, 10, 10)) self.assertEqual(6, s.faces().size()) # test removal of splitter caused by extrude+combine operation - s = Workplane("XY").box(10, 10, 10).faces(">Y").\ - workplane().rect(5, 10, 5).extrude(20) + s = ( + Workplane("XY") + .box(10, 10, 10) + .faces(">Y") + .workplane() + .rect(5, 10, 5) + .extrude(20) + ) self.assertEqual(10, s.faces().size()) # test removal of splitter caused by double hole operation - s = Workplane("XY").box(10, 10, 10).faces(">Z").workplane().\ - hole(3, 5).faces(">Z").workplane().hole(3, 10) + s = ( + Workplane("XY") + .box(10, 10, 10) + .faces(">Z") + .workplane() + .hole(3, 5) + .faces(">Z") + .workplane() + .hole(3, 10) + ) self.assertEqual(7, s.faces().size()) # test removal of splitter caused by cutThruAll - s = Workplane("XY").box(10, 10, 10).faces(">Y").workplane().\ - rect(10, 5).cutBlind(-5).faces(">Z").workplane().\ - center(0, 2.5).rect(5, 5).cutThruAll() + s = ( + Workplane("XY") + .box(10, 10, 10) + .faces(">Y") + .workplane() + .rect(10, 5) + .cutBlind(-5) + .faces(">Z") + .workplane() + .center(0, 2.5) + .rect(5, 5) + .cutThruAll() + ) self.assertEqual(18, s.faces().size()) @@ -1584,16 +1885,33 @@ class TestCadQuery(BaseTest): Test the case when clean is disabled. """ # test disabling autoSimplify - s = Workplane("XY").moveTo(0, 0).line(5, 0).line(5, 0).line(0, 10).\ - line(-10, 0).close().extrude(10, clean=False) + s = ( + Workplane("XY") + .moveTo(0, 0) + .line(5, 0) + .line(5, 0) + .line(0, 10) + .line(-10, 0) + .close() + .extrude(10, clean=False) + ) self.assertEqual(7, s.faces().size()) - s = Workplane("XY").box(10, 10, 10).\ - union(Workplane("XY").box(20, 10, 10), clean=False) + s = ( + Workplane("XY") + .box(10, 10, 10) + .union(Workplane("XY").box(20, 10, 10), clean=False) + ) self.assertEqual(14, s.faces().size()) - s = Workplane("XY").box(10, 10, 10).faces(">Y").\ - workplane().rect(5, 10, 5).extrude(20, clean=False) + s = ( + Workplane("XY") + .box(10, 10, 10) + .faces(">Y") + .workplane() + .rect(5, 10, 5) + .extrude(20, clean=False) + ) self.assertEqual(12, s.faces().size()) @@ -1601,8 +1919,17 @@ class TestCadQuery(BaseTest): """ Test running of `clean()` method explicitly. """ - s = Workplane("XY").moveTo(0, 0).line(5, 0).line(5, 0).line(0, 10).\ - line(-10, 0).close().extrude(10, clean=False).clean() + s = ( + Workplane("XY") + .moveTo(0, 0) + .line(5, 0) + .line(5, 0) + .line(0, 10) + .line(-10, 0) + .close() + .extrude(10, clean=False) + .clean() + ) self.assertEqual(6, s.faces().size()) def testPlanes(self): @@ -1611,62 +1938,132 @@ class TestCadQuery(BaseTest): """ # ZX plane s = Workplane(Plane.ZX()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # YX plane s = Workplane(Plane.YX()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # YX plane s = Workplane(Plane.YX()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # ZY plane s = Workplane(Plane.ZY()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # front plane s = Workplane(Plane.front()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # back plane s = Workplane(Plane.back()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # left plane s = Workplane(Plane.left()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # right plane s = Workplane(Plane.right()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # top plane s = Workplane(Plane.top()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) # bottom plane s = Workplane(Plane.bottom()) - result = s.rect(2.0, 4.0).extrude(0.5).faces(">Z").workplane()\ - .rect(1.5, 3.5, forConstruction=True).vertices().cskHole(0.125, 0.25, 82, depth=None) + result = ( + s.rect(2.0, 4.0) + .extrude(0.5) + .faces(">Z") + .workplane() + .rect(1.5, 3.5, forConstruction=True) + .vertices() + .cskHole(0.125, 0.25, 82, depth=None) + ) self.saveModel(result) def testIsInside(self): @@ -1715,8 +2112,14 @@ class TestCadQuery(BaseTest): h = 10.0 t = 1.0 s1 = Workplane("XY").circle(bd).workplane(offset=h).circle(td).loft() - s2 = Workplane("XY").workplane(offset=t).circle( - bd - (2.0 * t)).workplane(offset=(h - t)).circle(td - (2.0 * t)).loft() + s2 = ( + Workplane("XY") + .workplane(offset=t) + .circle(bd - (2.0 * t)) + .workplane(offset=(h - t)) + .circle(td - (2.0 * t)) + .loft() + ) s3 = s1.cut(s2) self.saveModel(s3) @@ -1756,57 +2159,90 @@ class TestCadQuery(BaseTest): p_lipHeight = 1.0 # outer shell - oshell = Workplane("XY").rect(p_outerWidth, p_outerLength).extrude( - p_outerHeight + p_lipHeight) + oshell = ( + Workplane("XY") + .rect(p_outerWidth, p_outerLength) + .extrude(p_outerHeight + p_lipHeight) + ) # weird geometry happens if we make the fillets in the wrong order if p_sideRadius > p_topAndBottomRadius: - oshell = oshell.edges("|Z").fillet(p_sideRadius)\ - .edges("#Z").fillet(p_topAndBottomRadius) + oshell = ( + oshell.edges("|Z") + .fillet(p_sideRadius) + .edges("#Z") + .fillet(p_topAndBottomRadius) + ) else: - oshell = oshell.edges("#Z").fillet(p_topAndBottomRadius)\ - .edges("|Z").fillet(p_sideRadius) + oshell = ( + oshell.edges("#Z") + .fillet(p_topAndBottomRadius) + .edges("|Z") + .fillet(p_sideRadius) + ) # inner shell - ishell = oshell.faces("Z").workplane(-p_thickness)\ - .rect(POSTWIDTH, POSTLENGTH, forConstruction=True)\ - .vertices()\ - .circle(p_screwpostOD / 2.0)\ - .circle(p_screwpostID / 2.0)\ + box = ( + box.faces(">Z") + .workplane(-p_thickness) + .rect(POSTWIDTH, POSTLENGTH, forConstruction=True) + .vertices() + .circle(p_screwpostOD / 2.0) + .circle(p_screwpostID / 2.0) .extrude((-1.0) * (p_outerHeight + p_lipHeight - p_thickness), True) + ) # split lid into top and bottom parts - (lid, bottom) = box.faces(">Z").workplane(-p_thickness - - p_lipHeight).split(keepTop=True, keepBottom=True).all() # splits into two solids + (lid, bottom) = ( + box.faces(">Z") + .workplane(-p_thickness - p_lipHeight) + .split(keepTop=True, keepBottom=True) + .all() + ) # splits into two solids # translate the lid, and subtract the bottom from it to produce the lid inset lowerLid = lid.translate((0, 0, -p_lipHeight)) cutlip = lowerLid.cut(bottom).translate( - (p_outerWidth + p_thickness, 0, p_thickness - p_outerHeight + p_lipHeight)) + (p_outerWidth + p_thickness, 0, p_thickness - p_outerHeight + p_lipHeight) + ) # compute centers for counterbore/countersink or counterbore - topOfLidCenters = cutlip.faces(">Z").workplane().rect( - POSTWIDTH, POSTLENGTH, forConstruction=True).vertices() + topOfLidCenters = ( + cutlip.faces(">Z") + .workplane() + .rect(POSTWIDTH, POSTLENGTH, forConstruction=True) + .vertices() + ) # add holes of the desired type if p_boreDiameter > 0 and p_boreDepth > 0: topOfLid = topOfLidCenters.cboreHole( - p_screwpostID, p_boreDiameter, p_boreDepth, (2.0) * p_thickness) + p_screwpostID, p_boreDiameter, p_boreDepth, (2.0) * p_thickness + ) elif p_countersinkDiameter > 0 and p_countersinkAngle > 0: topOfLid = topOfLidCenters.cskHole( - p_screwpostID, p_countersinkDiameter, p_countersinkAngle, (2.0) * p_thickness) + p_screwpostID, + p_countersinkDiameter, + p_countersinkAngle, + (2.0) * p_thickness, + ) else: topOfLid = topOfLidCenters.hole(p_screwpostID, (2.0) * p_thickness) @@ -1823,9 +2259,9 @@ class TestCadQuery(BaseTest): """ Test extrude """ - r = 1. - h = 1. - decimal_places = 9. + r = 1.0 + h = 1.0 + decimal_places = 9.0 # extrude in one direction s = Workplane("XY").circle(r).extrude(h, both=False) @@ -1836,9 +2272,7 @@ class TestCadQuery(BaseTest): # calculate the distance between the top and the bottom face delta = top_face.val().Center().sub(bottom_face.val().Center()) - self.assertTupleAlmostEquals(delta.toTuple(), - (0., 0., h), - decimal_places) + self.assertTupleAlmostEquals(delta.toTuple(), (0.0, 0.0, h), decimal_places) # extrude symmetrically s = Workplane("XY").circle(r).extrude(h, both=True) @@ -1849,14 +2283,14 @@ class TestCadQuery(BaseTest): # calculate the distance between the top and the bottom face delta = top_face.val().Center().sub(bottom_face.val().Center()) - self.assertTupleAlmostEquals(delta.toTuple(), - (0., 0., 2. * h), - decimal_places) + self.assertTupleAlmostEquals( + delta.toTuple(), (0.0, 0.0, 2.0 * h), decimal_places + ) def testTaperedExtrudeCutBlind(self): - h = 1. - r = 1. + h = 1.0 + r = 1.0 t = 5 # extrude with a positive taper @@ -1882,10 +2316,17 @@ class TestCadQuery(BaseTest): self.assertTrue(delta > 0) # cut a tapered hole - s = Workplane("XY").rect(2*r,2*r).extrude(2*h).faces('>Z').workplane()\ - .rect(r,r).cutBlind(-h, taper=t) + s = ( + Workplane("XY") + .rect(2 * r, 2 * r) + .extrude(2 * h) + .faces(">Z") + .workplane() + .rect(r, r) + .cutBlind(-h, taper=t) + ) - middle_face = s.faces('>Z[-2]') + middle_face = s.faces(">Z[-2]") self.assertTrue(middle_face.val().Area() < 1) @@ -1896,7 +2337,13 @@ class TestCadQuery(BaseTest): # Close when endPoint and startPoint coincide. # Create a double half-circle - b = Workplane(Plane.XY()).sagittaArc((10, 0), 2).sagittaArc((0, 0), 2).close().extrude(2) + b = ( + Workplane(Plane.XY()) + .sagittaArc((10, 0), 2) + .sagittaArc((0, 0), 2) + .close() + .extrude(2) + ) # The b shape shall have twice the volume of the a shape. self.assertAlmostEqual(a.val().Volume() * 2.0, b.val().Volume()) @@ -1906,46 +2353,80 @@ class TestCadQuery(BaseTest): length = 10.0 width = 5.0 - obj1 = Workplane('XY', origin=(0, 0, -thickness / 2)) \ - .moveTo(length / 2, 0).threePointArc((0, width / 2), (-length / 2, 0)) \ - .threePointArc((0, -width / 2), (length / 2, 0)) \ - .close().extrude(thickness) + obj1 = ( + Workplane("XY", origin=(0, 0, -thickness / 2)) + .moveTo(length / 2, 0) + .threePointArc((0, width / 2), (-length / 2, 0)) + .threePointArc((0, -width / 2), (length / 2, 0)) + .close() + .extrude(thickness) + ) - os_x = 8.0 # Offset in X + os_x = 8.0 # Offset in X os_y = -19.5 # Offset in Y - obj2 = Workplane('YZ', origin=(os_x, os_y, -thickness / 2)) \ - .moveTo(os_x + length / 2, os_y).sagittaArc((os_x -length / 2, os_y), width / 2) \ - .sagittaArc((os_x + length / 2, os_y), width / 2) \ - .close().extrude(thickness) + obj2 = ( + Workplane("YZ", origin=(os_x, os_y, -thickness / 2)) + .moveTo(os_x + length / 2, os_y) + .sagittaArc((os_x - length / 2, os_y), width / 2) + .sagittaArc((os_x + length / 2, os_y), width / 2) + .close() + .extrude(thickness) + ) # The obj1 shape shall have the same volume as the obj2 shape. self.assertAlmostEqual(obj1.val().Volume(), obj2.val().Volume()) def testText(self): - box = Workplane("XY" ).box(4, 4, 0.5) + box = Workplane("XY").box(4, 4, 0.5) - obj1 = box.faces('>Z').workplane()\ - .text('CQ 2.0',0.5,-.05,cut=True,halign='left',valign='bottom', font='Sans') + obj1 = ( + box.faces(">Z") + .workplane() + .text( + "CQ 2.0", + 0.5, + -0.05, + cut=True, + halign="left", + valign="bottom", + font="Sans", + ) + ) - #combined object should have smaller volume - self.assertGreater(box.val().Volume(),obj1.val().Volume()) + # combined object should have smaller volume + self.assertGreater(box.val().Volume(), obj1.val().Volume()) - obj2 = box.faces('>Z').workplane()\ - .text('CQ 2.0',0.5,.05,cut=False,combine=True, font='Sans') + obj2 = ( + box.faces(">Z") + .workplane() + .text("CQ 2.0", 0.5, 0.05, cut=False, combine=True, font="Sans") + ) - #combined object should have bigger volume - self.assertLess(box.val().Volume(),obj2.val().Volume()) + # combined object should have bigger volume + self.assertLess(box.val().Volume(), obj2.val().Volume()) - #verify that the number of top faces is correct (NB: this is font specific) - self.assertEqual(len(obj2.faces('>Z').vals()),5) + # verify that the number of top faces is correct (NB: this is font specific) + self.assertEqual(len(obj2.faces(">Z").vals()), 5) - obj3 = box.faces('>Z').workplane()\ - .text('CQ 2.0',0.5,.05,cut=False,combine=False,halign='right',valign='top', font='Sans') + obj3 = ( + box.faces(">Z") + .workplane() + .text( + "CQ 2.0", + 0.5, + 0.05, + cut=False, + combine=False, + halign="right", + valign="top", + font="Sans", + ) + ) - #verify that the number of solids is correct - self.assertEqual(len(obj3.solids().vals()),5) + # verify that the number of solids is correct + self.assertEqual(len(obj3.solids().vals()), 5) def testParametricCurve(self): @@ -1954,83 +2435,87 @@ class TestCadQuery(BaseTest): k = 4 r = 1 - func = lambda t: ( r*(k+1)*cos(t) - r* cos((k+1)*t), - r*(k+1)*sin(t) - r* sin((k+1)*t)) + func = lambda t: ( + r * (k + 1) * cos(t) - r * cos((k + 1) * t), + r * (k + 1) * sin(t) - r * sin((k + 1) * t), + ) - res_open = Workplane('XY').parametricCurve(func).extrude(3) + res_open = Workplane("XY").parametricCurve(func).extrude(3) - #open profile generates an invalid solid + # open profile generates an invalid solid self.assertFalse(res_open.solids().val().isValid()) - res_closed = Workplane('XY').parametricCurve(func,start=0,stop=2*pi)\ - .extrude(3) + res_closed = ( + Workplane("XY").parametricCurve(func, start=0, stop=2 * pi).extrude(3) + ) - #closed profile will generate a valid solid with 3 faces + # closed profile will generate a valid solid with 3 faces self.assertTrue(res_closed.solids().val().isValid()) - self.assertEqual(len(res_closed.faces().vals()),3) - + self.assertEqual(len(res_closed.faces().vals()), 3) + def testMakeShellSolid(self): - c0 = math.sqrt(2)/4 - vertices = [[c0, -c0, c0], [c0, c0, -c0], [-c0, c0, c0], [-c0, -c0, -c0]] + c0 = math.sqrt(2) / 4 + vertices = [[c0, -c0, c0], [c0, c0, -c0], [-c0, c0, c0], [-c0, -c0, -c0]] faces_ixs = [[0, 1, 2, 0], [1, 0, 3, 1], [2, 3, 0, 2], [3, 2, 1, 3]] - + faces = [] for ixs in faces_ixs: lines = [] - for v1,v2 in zip(ixs,ixs[1:]): - lines.append(Edge.makeLine(Vector(*vertices[v1]), - Vector(*vertices[v2]))) + for v1, v2 in zip(ixs, ixs[1:]): + lines.append( + Edge.makeLine(Vector(*vertices[v1]), Vector(*vertices[v2])) + ) wire = Wire.combine(lines) faces.append(Face.makeFromWires(wire)) - + shell = Shell.makeShell(faces) solid = Solid.makeSolid(shell) - + self.assertTrue(shell.isValid()) self.assertTrue(solid.isValid()) - - self.assertEqual(len(solid.Vertices()),4) - self.assertEqual(len(solid.Faces()),4) + + self.assertEqual(len(solid.Vertices()), 4) + self.assertEqual(len(solid.Faces()), 4) def testIsInsideSolid(self): # test solid - model = Workplane('XY').box(10,10,10) - solid = model.val() # get first object on stack + model = Workplane("XY").box(10, 10, 10) + solid = model.val() # get first object on stack - self.assertTrue(solid.isInside((0,0,0))) - self.assertFalse(solid.isInside((10,10,10))) - self.assertTrue(solid.isInside((Vector(3,3,3)))) - self.assertFalse(solid.isInside((Vector(30.0,30.0,30.0)))) + self.assertTrue(solid.isInside((0, 0, 0))) + self.assertFalse(solid.isInside((10, 10, 10))) + self.assertTrue(solid.isInside((Vector(3, 3, 3)))) + self.assertFalse(solid.isInside((Vector(30.0, 30.0, 30.0)))) - self.assertTrue(solid.isInside((0,0,4.99), tolerance=0.1)) - self.assertTrue(solid.isInside((0,0,5))) # check point on surface - self.assertTrue(solid.isInside((0,0,5.01), tolerance=0.1)) - self.assertFalse(solid.isInside((0,0,5.1), tolerance=0.1)) + self.assertTrue(solid.isInside((0, 0, 4.99), tolerance=0.1)) + self.assertTrue(solid.isInside((0, 0, 5))) # check point on surface + self.assertTrue(solid.isInside((0, 0, 5.01), tolerance=0.1)) + self.assertFalse(solid.isInside((0, 0, 5.1), tolerance=0.1)) # test compound solid - model = Workplane('XY').box(10,10,10) - model = model.moveTo(50,50).box(10,10,10) + model = Workplane("XY").box(10, 10, 10) + model = model.moveTo(50, 50).box(10, 10, 10) solid = model.val() - self.assertTrue(solid.isInside((0,0,0))) - self.assertTrue(solid.isInside((50,50,0))) - self.assertFalse(solid.isInside((50,56,0))) + self.assertTrue(solid.isInside((0, 0, 0))) + self.assertTrue(solid.isInside((50, 50, 0))) + self.assertFalse(solid.isInside((50, 56, 0))) # make sure raises on non solid - model = Workplane('XY').rect(10,10) + model = Workplane("XY").rect(10, 10) solid = model.val() with self.assertRaises(AttributeError): - solid.isInside((0,0,0)) + solid.isInside((0, 0, 0)) # test solid with an internal void - void = Workplane('XY').box(10,10,10) - model = Workplane('XY').box(100,100,100).cut(void) + void = Workplane("XY").box(10, 10, 10) + model = Workplane("XY").box(100, 100, 100).cut(void) solid = model.val() - self.assertFalse(solid.isInside((0,0,0))) - self.assertTrue(solid.isInside((40,40,40))) - self.assertFalse(solid.isInside((55,55,55))) + self.assertFalse(solid.isInside((0, 0, 0))) + self.assertTrue(solid.isInside((40, 40, 40))) + self.assertFalse(solid.isInside((55, 55, 55))) def testWorkplaneCenterOptions(self): """ @@ -2038,141 +2523,222 @@ class TestCadQuery(BaseTest): """ decimal_places = 9 - pts = [(0,0),(90,0),(90,30),(30,30),(30,60),(0.0,60)] + pts = [(0, 0), (90, 0), (90, 30), (30, 30), (30, 60), (0.0, 60)] r = Workplane("XY").polyline(pts).close().extrude(10.0) - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin') \ - .plane.origin.toTuple() + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin") + .plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) - origin = r.faces(">Z").workplane(centerOption='CenterOfMass') \ - .plane.origin.toTuple() + origin = ( + r.faces(">Z").workplane(centerOption="CenterOfMass").plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (37.5, 22.5, 10.0), decimal_places) - origin = r.faces(">Z").workplane(centerOption='CenterOfBoundBox') \ - .plane.origin.toTuple() + origin = ( + r.faces(">Z") + .workplane(centerOption="CenterOfBoundBox") + .plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (45.0, 30.0, 10.0), decimal_places) - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin',origin=(30,10,20)) \ - .plane.origin.toTuple() + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin", origin=(30, 10, 20)) + .plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places) - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin',origin=Vector(30,10,20)) \ - .plane.origin.toTuple() + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin", origin=Vector(30, 10, 20)) + .plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (30.0, 10.0, 10.0), decimal_places) with self.assertRaises(ValueError): - origin = r.faces(">Z").workplane(centerOption='undefined') + origin = r.faces(">Z").workplane(centerOption="undefined") # test case where plane origin is shifted with center call - r = r.faces(">Z").workplane(centerOption='ProjectedOrigin').center(30,0) \ - .hole(90) + r = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin") + .center(30, 0) + .hole(90) + ) - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin') \ - .plane.origin.toTuple() + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin") + .plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (30.0, 0.0, 10.0), decimal_places) - origin = r.faces(">Z").workplane(centerOption='ProjectedOrigin', origin=(0,0,0)) \ - .plane.origin.toTuple() + origin = ( + r.faces(">Z") + .workplane(centerOption="ProjectedOrigin", origin=(0, 0, 0)) + .plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) # make sure projection works in all directions r = Workplane("YZ").polyline(pts).close().extrude(10.0) - origin = r.faces(">X").workplane(centerOption='ProjectedOrigin') \ - .plane.origin.toTuple() + origin = ( + r.faces(">X") + .workplane(centerOption="ProjectedOrigin") + .plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (10.0, 0.0, 0.0), decimal_places) - origin = r.faces(">X").workplane(centerOption='CenterOfMass') \ - .plane.origin.toTuple() + origin = ( + r.faces(">X").workplane(centerOption="CenterOfMass").plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (10.0, 37.5, 22.5), decimal_places) - origin = r.faces(">X").workplane(centerOption='CenterOfBoundBox') \ - .plane.origin.toTuple() + origin = ( + r.faces(">X") + .workplane(centerOption="CenterOfBoundBox") + .plane.origin.toTuple() + ) self.assertTupleAlmostEquals(origin, (10.0, 45.0, 30.0), decimal_places) r = Workplane("XZ").polyline(pts).close().extrude(10.0) - origin = r.faces("Z").workplane().slot2D(4,1,0).cutThruAll() + box = Workplane("XY").box(5, 5, 1) + result = box.faces(">Z").workplane().slot2D(4, 1, 0).cutThruAll() self.assertAlmostEqual(result.val().Volume(), 21.214601837, decimal_places) - result = box.faces(">Z").workplane().slot2D(4,1,0).cutBlind(-0.5) + result = box.faces(">Z").workplane().slot2D(4, 1, 0).cutBlind(-0.5) self.assertAlmostEqual(result.val().Volume(), 23.107300918, decimal_places) # Test to see if slot is rotated correctly - result = Workplane("XY").slot2D(4,1,45).extrude(1) + result = Workplane("XY").slot2D(4, 1, 45).extrude(1) point = result.faces(">Z").edges(">X").first().val().startPoint().toTuple() - self.assertTupleAlmostEquals(point, (0.707106781, 1.414213562, 1.0), decimal_places) + self.assertTupleAlmostEquals( + point, (0.707106781, 1.414213562, 1.0), decimal_places + ) def test_assembleEdges(self): # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides # Passes an open wire to assembleEdges so that IsDone is true but Error returns 2 to test the warning functionality. - edge_points = [[-7.,-7.,0.], [-3.,-10.,3.], [7.,-7.,0.], [7.,7.,0.], [-7.,7.,0.]] - edge_wire = Workplane('XY').polyline([(-7.,-7.), (7.,-7.), (7.,7.), (-7.,7.)]) - edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(0, 45, 0)).spline([(-7.,0.), (3,-3), (7.,0.)])) + edge_points = [ + [-7.0, -7.0, 0.0], + [-3.0, -10.0, 3.0], + [7.0, -7.0, 0.0], + [7.0, 7.0, 0.0], + [-7.0, 7.0, 0.0], + ] + edge_wire = Workplane("XY").polyline( + [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] + ) + edge_wire = edge_wire.add( + Workplane("YZ") + .workplane() + .transformed(offset=Vector(0, 0, -7), rotate=Vector(0, 45, 0)) + .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) + ) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) - + # Embossed star, need to change optional parameters to obtain nice looking result. - r1=3. - r2=10. - fn=6 - edge_points = [[r1*math.cos(i * math.pi/fn), r1*math.sin(i * math.pi/fn)] if i%2==0 else [r2*math.cos(i * math.pi/fn), r2*math.sin(i * math.pi/fn)] for i in range(2*fn+1)] - edge_wire = Workplane('XY').polyline(edge_points) - edge_wire = [o.vals()[0] for o in edge_wire.all()] - edge_wire = Wire.assembleEdges(edge_wire) - - # Points on hexagonal pattern coordinates, use of pushpoints. - r1 = 1. + r1 = 3.0 + r2 = 10.0 fn = 6 - edge_points = [[r1*math.cos(i * 2*math.pi/fn), r1*math.sin(i * 2*math.pi/fn)] for i in range(fn+1)] - surface_points = [[0.25,0,0.75], [-0.25,0,0.75], [0,0.25,0.75], [0,-0.25,0.75], [0,0,2]] - edge_wire = Workplane('XY').polyline(edge_points) + edge_points = [ + [r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)] + if i % 2 == 0 + else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)] + for i in range(2 * fn + 1) + ] + edge_wire = Workplane("XY").polyline(edge_points) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) - + + # Points on hexagonal pattern coordinates, use of pushpoints. + r1 = 1.0 + fn = 6 + edge_points = [ + [r1 * math.cos(i * 2 * math.pi / fn), r1 * math.sin(i * 2 * math.pi / fn)] + for i in range(fn + 1) + ] + surface_points = [ + [0.25, 0, 0.75], + [-0.25, 0, 0.75], + [0, 0.25, 0.75], + [0, -0.25, 0.75], + [0, 0, 2], + ] + edge_wire = Workplane("XY").polyline(edge_points) + edge_wire = [o.vals()[0] for o in edge_wire.all()] + edge_wire = Wire.assembleEdges(edge_wire) + # Gyroïd, all edges are splines on different workplanes. - edge_points = [[[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]]] - plane_list = ['XZ', 'XY', 'YZ', 'XZ', 'YZ', 'XY'] - offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] - edge_wire = Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) - for i in range(len(edge_points)-1): - edge_wire = edge_wire.add(Workplane(plane_list[i+1]).workplane(offset=-offset_list[i+1]).spline(edge_points[i+1])) + edge_points = [ + [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + ] + plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] + offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] + edge_wire = ( + Workplane(plane_list[0]) + .workplane(offset=-offset_list[0]) + .spline(edge_points[0]) + ) + for i in range(len(edge_points) - 1): + edge_wire = edge_wire.add( + Workplane(plane_list[i + 1]) + .workplane(offset=-offset_list[i + 1]) + .spline(edge_points[i + 1]) + ) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) diff --git a/tests/test_cqgi.py b/tests/test_cqgi.py index a7218ae6..43d38d77 100644 --- a/tests/test_cqgi.py +++ b/tests/test_cqgi.py @@ -42,8 +42,9 @@ class TestCQGI(BaseTest): model = cqgi.CQModel(TESTSCRIPT) metadata = model.metadata - self.assertEqual(set(metadata.parameters.keys()), { - 'height', 'width', 'a', 'b', 'foo'}) + self.assertEqual( + set(metadata.parameters.keys()), {"height", "width", "a", "b", "foo"} + ) def test_build_with_debug(self): model = cqgi.CQModel(TEST_DEBUG_SCRIPT) @@ -51,7 +52,7 @@ class TestCQGI(BaseTest): debugItems = result.debugObjects self.assertTrue(len(debugItems) == 2) self.assertTrue(debugItems[0].shape == "bar") - self.assertTrue(debugItems[0].options == {"color": 'yellow'}) + self.assertTrue(debugItems[0].options == {"color": "yellow"}) self.assertTrue(debugItems[1].shape == 2.0) self.assertTrue(debugItems[1].options == {}) @@ -65,7 +66,7 @@ class TestCQGI(BaseTest): def test_build_with_different_params(self): model = cqgi.CQModel(TESTSCRIPT) - result = model.build({'height': 3.0}) + result = model.build({"height": 3.0}) self.assertTrue(result.results[0].shape == "3.0|3.0|bar|1.0") def test_describe_parameters(self): @@ -76,9 +77,9 @@ class TestCQGI(BaseTest): """ ) model = cqgi.CQModel(script) - a_param = model.metadata.parameters['a'] + a_param = model.metadata.parameters["a"] self.assertTrue(a_param.default_value == 2.0) - self.assertTrue(a_param.desc == 'FirstLetter') + self.assertTrue(a_param.desc == "FirstLetter") self.assertTrue(a_param.varType == cqgi.NumberParameterType) def test_describe_parameter_invalid_doesnt_fail_script(self): @@ -89,8 +90,8 @@ class TestCQGI(BaseTest): """ ) model = cqgi.CQModel(script) - a_param = model.metadata.parameters['a'] - self.assertTrue(a_param.name == 'a') + a_param = model.metadata.parameters["a"] + self.assertTrue(a_param.name == "a") def test_build_with_exception(self): badscript = textwrap.dedent( @@ -115,7 +116,7 @@ class TestCQGI(BaseTest): with self.assertRaises(Exception) as context: model = cqgi.CQModel(badscript) - self.assertTrue('invalid syntax' in context.exception.args) + self.assertTrue("invalid syntax" in context.exception.args) def test_that_two_results_are_returned(self): script = textwrap.dedent( @@ -140,7 +141,7 @@ class TestCQGI(BaseTest): show_object(h) """ ) - result = cqgi.parse(script).build({'h': 33.33}) + result = cqgi.parse(script).build({"h": 33.33}) self.assertEqual(result.results[0].shape, "33.33") def test_that_assigning_string_to_number_fails(self): @@ -150,9 +151,8 @@ class TestCQGI(BaseTest): show_object(h) """ ) - result = cqgi.parse(script).build({'h': "a string"}) - self.assertTrue(isinstance(result.exception, - cqgi.InvalidParameterError)) + result = cqgi.parse(script).build({"h": "a string"}) + self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError)) def test_that_assigning_unknown_var_fails(self): script = textwrap.dedent( @@ -162,9 +162,8 @@ class TestCQGI(BaseTest): """ ) - result = cqgi.parse(script).build({'w': "var is not there"}) - self.assertTrue(isinstance(result.exception, - cqgi.InvalidParameterError)) + result = cqgi.parse(script).build({"w": "var is not there"}) + self.assertTrue(isinstance(result.exception, cqgi.InvalidParameterError)) def test_that_cq_objects_are_visible(self): script = textwrap.dedent( @@ -198,10 +197,10 @@ class TestCQGI(BaseTest): """ ) - result = cqgi.parse(script).build({'h': False}) + result = cqgi.parse(script).build({"h": False}) self.assertTrue(result.success) - self.assertEqual(result.first_result.shape, '*False*') + self.assertEqual(result.first_result.shape, "*False*") def test_that_only_top_level_vars_are_detected(self): script = textwrap.dedent( diff --git a/tests/test_exporters.py b/tests/test_exporters.py index db631bea..a91eab98 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -12,7 +12,6 @@ from tests import BaseTest class TestExporters(BaseTest): - def _exportBox(self, eType, stringsToFind): """ Exports a test object, and then looks for @@ -21,31 +20,32 @@ class TestExporters(BaseTest): """ p = Workplane("XY").box(1, 2, 3) - if eType == exporters.ExportTypes.AMF: + if eType == exporters.ExportTypes.AMF: s = io.BytesIO() else: s = io.StringIO() - + exporters.exportShape(p, eType, s, 0.1) - result = '{}'.format(s.getvalue()) + result = "{}".format(s.getvalue()) for q in stringsToFind: self.assertTrue(result.find(q) > -1) return result def testSTL(self): - self._exportBox(exporters.ExportTypes.STL, ['facet normal']) + self._exportBox(exporters.ExportTypes.STL, ["facet normal"]) def testSVG(self): - self._exportBox(exporters.ExportTypes.SVG, ['']) + self._exportBox(exporters.ExportTypes.AMF, [""]) def testSTEP(self): - self._exportBox(exporters.ExportTypes.STEP, ['FILE_SCHEMA']) + self._exportBox(exporters.ExportTypes.STEP, ["FILE_SCHEMA"]) def testTJS(self): - self._exportBox(exporters.ExportTypes.TJS, [ - 'vertices', 'formatVersion', 'faces']) + self._exportBox( + exporters.ExportTypes.TJS, ["vertices", "formatVersion", "faces"] + ) diff --git a/tests/test_importers.py b/tests/test_importers.py index 16be51dc..018fcd9f 100644 --- a/tests/test_importers.py +++ b/tests/test_importers.py @@ -36,12 +36,18 @@ class TestImporters(BaseTest): self.assertTrue(importedShape.val().ShapeType() == "Solid") # Check the number of faces and vertices per face to make sure we have a box shape - self.assertTrue(importedShape.faces("+X").size() == - 1 and importedShape.faces("+X").vertices().size() == 4) - self.assertTrue(importedShape.faces("+Y").size() == - 1 and importedShape.faces("+Y").vertices().size() == 4) - self.assertTrue(importedShape.faces("+Z").size() == - 1 and importedShape.faces("+Z").vertices().size() == 4) + self.assertTrue( + importedShape.faces("+X").size() == 1 + and importedShape.faces("+X").vertices().size() == 4 + ) + self.assertTrue( + importedShape.faces("+Y").size() == 1 + and importedShape.faces("+Y").vertices().size() == 4 + ) + self.assertTrue( + importedShape.faces("+Z").size() == 1 + and importedShape.faces("+Z").vertices().size() == 4 + ) def testSTEP(self): """ @@ -55,7 +61,8 @@ class TestImporters(BaseTest): not segfault. """ tmpfile = OUTDIR + "/badSTEP.step" - with open(tmpfile, 'w') as f: f.write("invalid STEP file") + with open(tmpfile, "w") as f: + f.write("invalid STEP file") with self.assertRaises(ValueError): importers.importShape(importers.ImportTypes.STEP, tmpfile) @@ -69,6 +76,8 @@ class TestImporters(BaseTest): objs = importers.importShape(importers.ImportTypes.STEP, filename) self.assertEqual(2, len(objs.all())) -if __name__ == '__main__': + +if __name__ == "__main__": import unittest + unittest.main() diff --git a/tests/test_jupyter.py b/tests/test_jupyter.py index 7f4b92c2..ad7db4db 100644 --- a/tests/test_jupyter.py +++ b/tests/test_jupyter.py @@ -2,9 +2,10 @@ from tests import BaseTest import cadquery + class TestJupyter(BaseTest): def test_repr_html(self): - cube = cadquery.Workplane('XY').box(1, 1, 1) + cube = cadquery.Workplane("XY").box(1, 1, 1) shape = cube.val() self.assertIsInstance(shape, cadquery.occ_impl.shapes.Solid) diff --git a/tests/test_selectors.py b/tests/test_selectors.py index d557375e..049a5a4b 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -1,4 +1,4 @@ -__author__ = 'dcowden' +__author__ = "dcowden" """ Tests for CadQuery Selectors @@ -20,22 +20,19 @@ 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) + self.assertTupleAlmostEquals((0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3) # move origin and confirm center moves 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) + self.assertTupleAlmostEquals((-2.0, -2.0, 0.0), s.plane.origin.toTuple(), 3) def testVertices(self): t = makeUnitSquareWire() # square box @@ -43,8 +40,7 @@ class TestCQSelectors(BaseTest): self.assertEqual(4, c.vertices().size()) self.assertEqual(4, c.edges().size()) - self.assertEqual(0, c.vertices().edges().size() - ) # no edges on any vertices + 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 @@ -71,8 +67,7 @@ class TestCQSelectors(BaseTest): def testFirst(self): c = CQ(makeUnitCube()) self.assertEqual(type(c.vertices().first().val()), Vertex) - self.assertEqual( - type(c.vertices().first().first().first().val()), Vertex) + self.assertEqual(type(c.vertices().first().first().first().val()), Vertex) def testCompounds(self): c = CQ(makeUnitSquareWire()) @@ -99,11 +94,11 @@ class TestCQSelectors(BaseTest): 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()) + 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()) @@ -131,10 +126,12 @@ class TestCQSelectors(BaseTest): # faces parallel to Z axis self.assertEqual(2, c.faces("|Z").size()) # TODO: provide short names for ParallelDirSelector - self.assertEqual(2, c.faces(selectors.ParallelDirSelector( - Vector((0, 0, 1)))).size()) # same thing as above - self.assertEqual(2, c.faces(selectors.ParallelDirSelector( - Vector((0, 0, -1)))).size()) # same thing as above + self.assertEqual( + 2, c.faces(selectors.ParallelDirSelector(Vector((0, 0, 1)))).size() + ) # same thing as above + 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()) @@ -178,97 +175,96 @@ class TestCQSelectors(BaseTest): self.assertEqual(4, len(el)) def testNthDistance(self): - c = Workplane('XY').pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1) + c = Workplane("XY").pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1) # 2nd face val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), 1)).val() self.assertAlmostEqual(val.Center().x, -1.5) # 2nd face with inversed selection vector - val = c.faces(selectors.DirectionNthSelector( - Vector(-1, 0, 0), 1)).val() + val = c.faces(selectors.DirectionNthSelector(Vector(-1, 0, 0), 1)).val() self.assertAlmostEqual(val.Center().x, 1.5) # 2nd last face - val = c.faces(selectors.DirectionNthSelector( - Vector(1, 0, 0), -2)).val() + val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), -2)).val() self.assertAlmostEqual(val.Center().x, 1.5) # Last face - val = c.faces(selectors.DirectionNthSelector( - Vector(1, 0, 0), -1)).val() + val = c.faces(selectors.DirectionNthSelector(Vector(1, 0, 0), -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) + self.assertAlmostEqual(val.normalAt().cross(Vector(1, 0, 0)).Length, 0.0) # repeat the test using string based selector # 2nd face - val = c.faces('>(1,0,0)[1]').val() + val = c.faces(">(1,0,0)[1]").val() self.assertAlmostEqual(val.Center().x, -1.5) - val = c.faces('>X[1]').val() + 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() + val = c.faces(">(-1,0,0)[1]").val() self.assertAlmostEqual(val.Center().x, 1.5) - val = c.faces('X[-2]').val() + val = c.faces(">X[-2]").val() self.assertAlmostEqual(val.Center().x, 1.5) # Last face - val = c.faces('>X[-1]').val() + 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) + 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')\ + 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() + vals = c.faces(">Z[1]").vals() self.assertEqual(len(vals), 2) - val = c.faces('>Z[1]').val() + 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]').val() - val2 = c.faces('>Z').val() - self.assertTupleAlmostEquals(val1.Center().toTuple(), - val2.Center().toTuple(), - 3) + val1 = c.faces(">Z[-1]").val() + val2 = c.faces(">Z").val() + self.assertTupleAlmostEquals( + val1.Center().toTuple(), val2.Center().toTuple(), 3 + ) def testNearestTo(self): c = CQ(makeUnitCube()) @@ -302,7 +298,7 @@ class TestCQSelectors(BaseTest): ((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)) + ((0.9, -0.1, 0.9), (1.1, 0.1, 1.1), (1.0, 0.0, 1.0)), ] for d in test_data_vertices: @@ -318,11 +314,13 @@ class TestCQSelectors(BaseTest): 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() + 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() + 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 @@ -331,7 +329,7 @@ class TestCQSelectors(BaseTest): ((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)) + ((0.4, 0.9, 0.9), (0.6, 1.1, 1.1,), (0.5, 1.0, 1.0)), ] for d in test_data_edges: @@ -347,11 +345,9 @@ class TestCQSelectors(BaseTest): 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() + 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() + 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 @@ -360,7 +356,7 @@ class TestCQSelectors(BaseTest): ((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)) + ((0.4, 0.4, -0.1), (0.6, 0.6, 0.1), (0.5, 0.5, 0.0)), ] for d in test_data_faces: @@ -376,22 +372,23 @@ class TestCQSelectors(BaseTest): 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() + 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() + 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() + 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() + 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() + 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 testAndSelector(self): @@ -400,12 +397,13 @@ class TestCQSelectors(BaseTest): S = selectors.StringSyntaxSelector BS = selectors.BoxSelector - el = c.edges(selectors.AndSelector( - S('|X'), BS((-2, -2, 0.1), (2, 2, 2)))).vals() + 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() + el = c.edges(S("|X") & BS((-2, -2, 0.1), (2, 2, 2))).vals() self.assertEqual(2, len(el)) # test using extended string syntax @@ -455,27 +453,27 @@ class TestCQSelectors(BaseTest): S = selectors.StringSyntaxSelector - fl = c.faces(selectors.InverseSelector(S('>Z'))).vals() + fl = c.faces(selectors.InverseSelector(S(">Z"))).vals() self.assertEqual(5, len(fl)) - el = c.faces('>Z').edges(selectors.InverseSelector(S('>X'))).vals() + el = c.faces(">Z").edges(selectors.InverseSelector(S(">X"))).vals() self.assertEqual(3, len(el)) # test invert operator - fl = c.faces(-S('>Z')).vals() + fl = c.faces(-S(">Z")).vals() self.assertEqual(5, len(fl)) - el = c.faces('>Z').edges(-S('>X')).vals() + el = c.faces(">Z").edges(-S(">X")).vals() self.assertEqual(3, len(el)) # test using extended string syntax - fl = c.faces('not >Z').vals() + fl = c.faces("not >Z").vals() self.assertEqual(5, len(fl)) - el = c.faces('>Z').edges('not >X').vals() + 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 (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 )'] + expressions = [ + "+X ", + "-Y", + "|(1,0,0)", + "#(1.,1.4114,-0.532)", + "%Plane", + ">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) diff --git a/tests/test_workplanes.py b/tests/test_workplanes.py index 414a16f1..9548e7ed 100644 --- a/tests/test_workplanes.py +++ b/tests/test_workplanes.py @@ -16,65 +16,70 @@ zInvAxis_ = Vector(0, 0, -1) class TestWorkplanes(BaseTest): - def testYZPlaneOrigins(self): # xy plane-- with origin at x=0.25 base = Vector(0.25, 0, 0) p = Plane(base, Vector(0, 1, 0), Vector(1, 0, 0)) # origin is always (0,0,0) in local coordinates - self.assertTupleAlmostEquals( - (0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) + self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) - #(0,0,0) is always the original base in global coordinates + # (0,0,0) is always the original base in global coordinates self.assertTupleAlmostEquals( - base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2) + base.toTuple(), p.toWorldCoords((0, 0)).toTuple(), 2 + ) def testXYPlaneOrigins(self): base = Vector(0, 0, 0.25) p = Plane(base, Vector(1, 0, 0), Vector(0, 0, 1)) # origin is always (0,0,0) in local coordinates - self.assertTupleAlmostEquals( - (0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) + self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) - #(0,0,0) is always the original base in global coordinates + # (0,0,0) is always the original base in global coordinates self.assertTupleAlmostEquals( - toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2) + toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2 + ) def testXZPlaneOrigins(self): base = Vector(0, 0.25, 0) p = Plane(base, Vector(0, 0, 1), Vector(0, 1, 0)) - #(0,0,0) is always the original base in global coordinates + # (0,0,0) is always the original base in global coordinates self.assertTupleAlmostEquals( - toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2) + toTuple(base), p.toWorldCoords((0, 0)).toTuple(), 2 + ) # origin is always (0,0,0) in local coordinates - self.assertTupleAlmostEquals( - (0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) + self.assertTupleAlmostEquals((0, 0, 0), p.toLocalCoords(p.origin).toTuple(), 2) def testPlaneBasics(self): p = Plane.XY() # local to world self.assertTupleAlmostEquals( - (1.0, 1.0, 0), p.toWorldCoords((1, 1)).toTuple(), 2) + (1.0, 1.0, 0), p.toWorldCoords((1, 1)).toTuple(), 2 + ) self.assertTupleAlmostEquals( - (-1.0, -1.0, 0), p.toWorldCoords((-1, -1)).toTuple(), 2) + (-1.0, -1.0, 0), p.toWorldCoords((-1, -1)).toTuple(), 2 + ) # world to local self.assertTupleAlmostEquals( - (-1.0, -1.0), p.toLocalCoords(Vector(-1, -1, 0)).toTuple(), 2) + (-1.0, -1.0), p.toLocalCoords(Vector(-1, -1, 0)).toTuple(), 2 + ) self.assertTupleAlmostEquals( - (1.0, 1.0), p.toLocalCoords(Vector(1, 1, 0)).toTuple(), 2) + (1.0, 1.0), p.toLocalCoords(Vector(1, 1, 0)).toTuple(), 2 + ) p = Plane.YZ() self.assertTupleAlmostEquals( - (0, 1.0, 1.0), p.toWorldCoords((1, 1)).toTuple(), 2) + (0, 1.0, 1.0), p.toWorldCoords((1, 1)).toTuple(), 2 + ) # world to local self.assertTupleAlmostEquals( - (1.0, 1.0), p.toLocalCoords(Vector(0, 1, 1)).toTuple(), 2) + (1.0, 1.0), p.toLocalCoords(Vector(0, 1, 1)).toTuple(), 2 + ) p = Plane.XZ() r = p.toWorldCoords((1, 1)).toTuple() @@ -82,62 +87,68 @@ class TestWorkplanes(BaseTest): # world to local self.assertTupleAlmostEquals( - (1.0, 1.0), p.toLocalCoords(Vector(1, 0, 1)).toTuple(), 2) + (1.0, 1.0), p.toLocalCoords(Vector(1, 0, 1)).toTuple(), 2 + ) def testOffsetPlanes(self): "Tests that a plane offset from the origin works ok too" p = Plane.XY(origin=(10.0, 10.0, 0)) self.assertTupleAlmostEquals( - (11.0, 11.0, 0.0), p.toWorldCoords((1.0, 1.0)).toTuple(), 2) - self.assertTupleAlmostEquals((2.0, 2.0), p.toLocalCoords( - Vector(12.0, 12.0, 0)).toTuple(), 2) + (11.0, 11.0, 0.0), p.toWorldCoords((1.0, 1.0)).toTuple(), 2 + ) + self.assertTupleAlmostEquals( + (2.0, 2.0), p.toLocalCoords(Vector(12.0, 12.0, 0)).toTuple(), 2 + ) # TODO test these offsets in the other dimensions too p = Plane.YZ(origin=(0, 2, 2)) self.assertTupleAlmostEquals( - (0.0, 5.0, 5.0), p.toWorldCoords((3.0, 3.0)).toTuple(), 2) - self.assertTupleAlmostEquals((10, 10.0, 0.0), p.toLocalCoords( - Vector(0.0, 12.0, 12.0)).toTuple(), 2) + (0.0, 5.0, 5.0), p.toWorldCoords((3.0, 3.0)).toTuple(), 2 + ) + self.assertTupleAlmostEquals( + (10, 10.0, 0.0), p.toLocalCoords(Vector(0.0, 12.0, 12.0)).toTuple(), 2 + ) p = Plane.XZ(origin=(2, 0, 2)) r = p.toWorldCoords((1.0, 1.0)).toTuple() self.assertTupleAlmostEquals((3.0, 0.0, 3.0), r, 2) - self.assertTupleAlmostEquals((10.0, 10.0), p.toLocalCoords( - Vector(12.0, 0.0, 12.0)).toTuple(), 2) + self.assertTupleAlmostEquals( + (10.0, 10.0), p.toLocalCoords(Vector(12.0, 0.0, 12.0)).toTuple(), 2 + ) def testXYPlaneBasics(self): - p = Plane.named('XY') + p = Plane.named("XY") self.assertTupleAlmostEquals(p.zDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4) def testYZPlaneBasics(self): - p = Plane.named('YZ') + p = Plane.named("YZ") self.assertTupleAlmostEquals(p.zDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4) def testZXPlaneBasics(self): - p = Plane.named('ZX') + p = Plane.named("ZX") self.assertTupleAlmostEquals(p.zDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4) def testXZPlaneBasics(self): - p = Plane.named('XZ') + p = Plane.named("XZ") self.assertTupleAlmostEquals(p.zDir.toTuple(), yInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), xAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), zAxis_.toTuple(), 4) def testYXPlaneBasics(self): - p = Plane.named('YX') + p = Plane.named("YX") self.assertTupleAlmostEquals(p.zDir.toTuple(), zInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), yAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), xAxis_.toTuple(), 4) def testZYPlaneBasics(self): - p = Plane.named('ZY') + p = Plane.named("ZY") self.assertTupleAlmostEquals(p.zDir.toTuple(), xInvAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4) self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4) From 113a48a952454dd232d7f739fbdc1bb478a82c7e Mon Sep 17 00:00:00 2001 From: Jeremy Wright Date: Tue, 21 Jan 2020 07:18:55 -0500 Subject: [PATCH 48/70] Updated the __version__ variable to fix CQ-editor's update mechanism. --- cadquery/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/__init__.py b/cadquery/__init__.py index 652ec5e6..c789890a 100644 --- a/cadquery/__init__.py +++ b/cadquery/__init__.py @@ -62,4 +62,4 @@ __all__ = [ "plugins", ] -__version__ = "2.0.0dev" +__version__ = "2.0RC1" From d1ced40c4e4c61847856bf5beb63c46b76a3bb52 Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Fri, 20 Dec 2019 20:54:59 +1030 Subject: [PATCH 49/70] Added tags --- cadquery/cq.py | 62 +++++++++++++++++++++++++++++++++++++++-- tests/test_cadquery.py | 57 +++++++++++++++++++++++++++++++++++++ tests/test_selectors.py | 2 +- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 61496f2f..1e1cd9ec 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -18,6 +18,7 @@ """ import math +from copy import copy from . import ( Vector, Plane, @@ -71,6 +72,7 @@ class CQ(object): self.objects = [] self.ctx = CQContext() self.parent = None + self._tag = None if obj: # guarded because sometimes None for internal use self.objects.append(obj) @@ -94,6 +96,17 @@ class CQ(object): r.objects = list(objlist) return r + def tag(self, name): + """ + Tags the current CQ object for later reference. + + :param name: the name to tag this object with + :type name: string + :returns: self, a cq object with tag applied + """ + self._tag = name + return self + def _collectProperty(self, propName): """ Collects all of the values for propName, @@ -266,6 +279,22 @@ class CQ(object): """ return self.objects[0] + def getTagged(self, name): + """ + Search the parent chain for a an object with tag == name. + + :param name: the tag to search for + :type name: string + :returns: the first CQ object in the parent chain with tag == name + :raises: ValueError if no object tagged name in the chain + """ + if self._tag == name: + return self + if self.parent is None: + raise ValueError("No CQ object named {} in chain".format(name)) + else: + return self.parent.getTagged(name) + def toOCC(self): """ Directly returns the wrapped FreeCAD object to cut down on the amount of boiler plate code @@ -430,6 +459,31 @@ class CQ(object): # a new workplane has the center of the workplane on the stack return s + def copyWorkplane(self, obj): + """ + Copies the workplane from obj. + + :param obj: an object to copy the workplane from + :type obj: a CQ object + :returns: a CQ object with obj's workplane + """ + out = Workplane(obj.plane) + out.parent = self + out.ctx = self.ctx + return out + + def copyWorkplaneFromTagged(self, name): + """ + Copies the workplane from a tagged parent. + + :param name: tag to search for + :type name: string + :returns: a CQ object with name's workplane + """ + tagged = self.getTagged(name) + out = self.copyWorkplane(tagged) + return out + def first(self): """ Return the first item on the stack @@ -1007,6 +1061,7 @@ class Workplane(CQ): self.objects = [self.plane.origin] self.parent = None self.ctx = CQContext() + self._tag = None def transformed(self, rotate=(0, 0, 0), offset=(0, 0, 0)): """ @@ -1048,7 +1103,7 @@ class Workplane(CQ): # copy the current state to the new object ns = Workplane("XY") - ns.plane = self.plane + ns.plane = copy(self.plane) ns.parent = self ns.objects = list(objlist) ns.ctx = self.ctx @@ -1215,8 +1270,9 @@ class Workplane(CQ): The result is a cube with a round boss on the corner """ "Shift local coordinates to the specified location, according to current coordinates" - self.plane.setOrigin2d(x, y) - n = self.newObject([self.plane.origin]) + new_origin = self.plane.toWorldCoords((x, y)) + n = self.newObject([new_origin]) + n.plane.setOrigin2d(x, y) return n def lineTo(self, x, y, forConstruction=False): diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 46dfc412..651be8e9 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2742,3 +2742,60 @@ class TestCadQuery(BaseTest): ) edge_wire = [o.vals()[0] for o in edge_wire.all()] edge_wire = Wire.assembleEdges(edge_wire) + + def testTag(self): + + # test tagging + Workplane("XY").pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1, combine=False) + result = (Workplane("XY").pushPoints([(-2, 0), (2, 0)]) + .box(1, 1, 1, combine=False).tag("2 solids") + .union(Workplane("XY").box(6, 1, 1))) + self.assertEqual(len(result.objects), 1) + result = result.getTagged("2 solids") + self.assertEqual(len(result.objects), 2) + + # tags can be used to create geometry for construction + part = ( # create a base solid + Workplane("XY").box(1, 1, 1).tag("base") + # do some stuff "for construction" + .faces(">Y").workplane().box(1, 3, 1) + .faces(">Z").workplane().box(2, 1, 1) + .faces(">X").workplane() + # create an edge, which CadQuery adds to the modelling context + .circle(0.5) + # go back to base object, but modelling context is preserved + .getTagged("base") + .faces(">Z").workplane().circle(0.5) + # loft between the top of the base object and the wire at the end of the "for construction" code + .loft() + ) + # assert face is made at the end of the construction geometry + self.assertTupleAlmostEquals(part.faces(">X").val().Center().toTuple(), + (1.0, 0.5, 1.5), + 9) + # assert face points in the x-direction, like the construction geometry + self.assertTupleAlmostEquals(part.faces(">X").val().normalAt().toTuple(), + (1.0, 0, 0), + 9) + + def testCopyWorkplane(self): + + obj0 = Workplane("XY").box(1, 1, 10).faces(">Z").workplane() + obj1 = Workplane("XY").copyWorkplane(obj0).box(1, 1, 1) + self.assertTupleAlmostEquals((0, 0, 5), + obj1.val().Center().toTuple(), + 9) + + def testCopyWorkplaneFromTagged(self): + + # create a flat, wide base. Extrude one object 4 units high, another + # object ontop of it 6 units high. Go back to base plane. Extrude an + # object 11 units high. Assert that top face is 11 units high. + result = (Workplane("XY").box(10, 10, 1, centered=(True, True, False)) + .faces(">Z").workplane().tag("base").center(3, 0).rect(2, 2) + .extrude(4).faces(">Z").workplane().circle(1).extrude(6) + .copyWorkplaneFromTagged("base").center(-3, 0).circle(1) + .extrude(11)) + self.assertTupleAlmostEquals(result.faces(">Z").val().Center().toTuple(), + (-3, 0, 12), + 9) diff --git a/tests/test_selectors.py b/tests/test_selectors.py index 049a5a4b..4bddb8ba 100644 --- a/tests/test_selectors.py +++ b/tests/test_selectors.py @@ -28,7 +28,7 @@ class TestCQSelectors(BaseTest): self.assertTupleAlmostEquals((0.0, 0.0, 0.0), s.plane.origin.toTuple(), 3) # move origin and confirm center moves - s.center(-2.0, -2.0) + s = s.center(-2.0, -2.0) # current point should be 0,0, but From b38f01711cafd3a734786b53685803180821194d Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Sun, 22 Dec 2019 08:22:26 +1030 Subject: [PATCH 50/70] Added the ability to select from a tagged object --- cadquery/cq.py | 51 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 1e1cd9ec..a11429b3 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -576,20 +576,25 @@ class CQ(object): return self._findType(Face, searchStack, searchParents) - def _selectObjects(self, objType, selector=None): + def _selectObjects(self, objType, selector=None, tag=None): """ Filters objects of the selected type with the specified selector,and returns results :param objType: the type of object we are searching for :type objType: string: (Vertex|Edge|Wire|Solid|Shell|Compound|CompSolid) + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object with the selected objects on the stack. **Implementation Note**: This is the base implementation of the vertices,edges,faces, solids,shells, and other similar selector methods. It is a useful extension point for plugin developers to make other selector methods. """ + cq_obj = self + if tag: + cq_obj = self.getTagged(tag) # A single list of all faces from all objects on the stack - toReturn = self._collectProperty(objType) + toReturn = cq_obj._collectProperty(objType) if selector is not None: if isinstance(selector, str) or isinstance(selector, str): @@ -600,7 +605,7 @@ class CQ(object): return self.newObject(toReturn) - def vertices(self, selector=None): + def vertices(self, selector=None, tag=None): """ Select the vertices of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the vertices of all objects are collected and a list of @@ -608,6 +613,8 @@ class CQ(object): :param selector: :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains the *distinct* vertices of *all* objects on the current stack, after being filtered by the selector, if provided @@ -629,9 +636,9 @@ class CQ(object): :py:class:`StringSyntaxSelector` """ - return self._selectObjects("Vertices", selector) + return self._selectObjects("Vertices", selector, tag) - def faces(self, selector=None): + def faces(self, selector=None, tag=None): """ Select the faces of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the faces of all objects are collected and a list of all the @@ -639,6 +646,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* faces of *all* objects on the current stack, filtered by the provided selector. @@ -661,9 +670,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects("Faces", selector) + return self._selectObjects("Faces", selector, tag) - def edges(self, selector=None): + def edges(self, selector=None, tag=None): """ Select the edges of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the edges of all objects are collected and a list of all the @@ -671,6 +680,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* edges of *all* objects on the current stack, filtered by the provided selector. @@ -692,9 +703,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects("Edges", selector) + return self._selectObjects("Edges", selector, tag) - def wires(self, selector=None): + def wires(self, selector=None, tag=None): """ Select the wires of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the wires of all objects are collected and a list of all the @@ -702,6 +713,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* wires of *all* objects on the current stack, filtered by the provided selector. @@ -715,9 +728,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects("Wires", selector) + return self._selectObjects("Wires", selector, tag) - def solids(self, selector=None): + def solids(self, selector=None, tag=None): """ Select the solids of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the solids of all objects are collected and a list of all the @@ -725,6 +738,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. @@ -741,9 +756,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects("Solids", selector) + return self._selectObjects("Solids", selector, tag) - def shells(self, selector=None): + def shells(self, selector=None, tag=None): """ Select the shells of objects on the stack, optionally filtering the selection. If there are multiple objects on the stack, the shells of all objects are collected and a list of all the @@ -751,6 +766,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. @@ -761,9 +778,9 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects("Shells", selector) + return self._selectObjects("Shells", selector, tag) - def compounds(self, selector=None): + def compounds(self, selector=None, tag=None): """ Select compounds on the stack, optionally filtering the selection. If there are multiple objects on the stack, they are collected and a list of all the distinct compounds @@ -771,6 +788,8 @@ class CQ(object): :param selector: A selector :type selector: None, a Selector object, or a string selector expression. + :param tag: if set, search the tagged CQ object instead of self + :type tag: string :return: a CQ object who's stack contains all of the *distinct* solids of *all* objects on the current stack, filtered by the provided selector. @@ -779,7 +798,7 @@ class CQ(object): See more about selectors HERE """ - return self._selectObjects("Compounds", selector) + return self._selectObjects("Compounds", selector, tag) def toSvg(self, opts=None): """ From 488a6144a59cc631a1fa8ccf2ab589477aa210dd Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Sun, 22 Dec 2019 13:54:59 +1030 Subject: [PATCH 51/70] Added tests for tags in selectors --- tests/test_cadquery.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 651be8e9..b4c2ba9e 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2799,3 +2799,32 @@ class TestCadQuery(BaseTest): self.assertTupleAlmostEquals(result.faces(">Z").val().Center().toTuple(), (-3, 0, 12), 9) + + def testTagSelectors(self): + + result0 = Workplane("XY").box(1, 1, 1).tag("box").sphere(1) + # result is currently a sphere + self.assertEqual(1, result0.faces().size()) + # a box has 8 vertices + self.assertEqual(8, result0.vertices(tag="box").size()) + # 6 faces + self.assertEqual(6, result0.faces(tag="box").size()) + # 12 edges + self.assertEqual(12, result0.edges(tag="box").size()) + # 6 wires + self.assertEqual(6, result0.wires(tag="box").size()) + + # create two solids, tag them, join to one solid + result1 = (Workplane("XY").pushPoints([(1, 0), (-1, 0)]).box(1, 1, 1) + .tag("boxes").sphere(1)) + self.assertEqual(1, result1.solids().size()) + self.assertEqual(2, result1.solids(tag="boxes").size()) + self.assertEqual(1, result1.shells().size()) + self.assertEqual(2, result1.shells(tag="boxes").size()) + + # create 4 individual objects, tag it, then combine to one compound + result2 = (Workplane("XY").rect(4, 4).vertices() + .box(1, 1, 1, combine=False).tag("4 objs")) + result2 = result2.newObject([Compound.makeCompound(result2.objects)]) + self.assertEqual(1, result2.compounds().size()) + self.assertEqual(0, result2.compounds(tag="4 objs").size()) From 6d98e29b151379a0878420f481a23e4e97128378 Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Tue, 24 Dec 2019 10:11:42 +1030 Subject: [PATCH 52/70] Simplify selector code --- cadquery/cq.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index a11429b3..a5c3ea90 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -590,9 +590,7 @@ class CQ(object): solids,shells, and other similar selector methods. It is a useful extension point for plugin developers to make other selector methods. """ - cq_obj = self - if tag: - cq_obj = self.getTagged(tag) + cq_obj = self.getTagged(tag) if tag else self # A single list of all faces from all objects on the stack toReturn = cq_obj._collectProperty(objType) From b54f03abf5775358cb0ed942175f2dde360299e4 Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Wed, 25 Dec 2019 21:34:05 +1030 Subject: [PATCH 53/70] Renamed copyWorkplaneFromTagged to workplaneFromTagged --- cadquery/cq.py | 2 +- tests/test_cadquery.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index a5c3ea90..aa2225d3 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -472,7 +472,7 @@ class CQ(object): out.ctx = self.ctx return out - def copyWorkplaneFromTagged(self, name): + def workplaneFromTagged(self, name): """ Copies the workplane from a tagged parent. diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index b4c2ba9e..0520fb41 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2786,7 +2786,7 @@ class TestCadQuery(BaseTest): obj1.val().Center().toTuple(), 9) - def testCopyWorkplaneFromTagged(self): + def testWorkplaneFromTagged(self): # create a flat, wide base. Extrude one object 4 units high, another # object ontop of it 6 units high. Go back to base plane. Extrude an @@ -2794,7 +2794,7 @@ class TestCadQuery(BaseTest): result = (Workplane("XY").box(10, 10, 1, centered=(True, True, False)) .faces(">Z").workplane().tag("base").center(3, 0).rect(2, 2) .extrude(4).faces(">Z").workplane().circle(1).extrude(6) - .copyWorkplaneFromTagged("base").center(-3, 0).circle(1) + .workplaneFromTagged("base").center(-3, 0).circle(1) .extrude(11)) self.assertTupleAlmostEquals(result.faces(">Z").val().Center().toTuple(), (-3, 0, 12), From 89e75470d0459025edca607d6b03f653c8b94afb Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Sat, 28 Dec 2019 20:46:10 +1030 Subject: [PATCH 54/70] Added examples to docs --- doc/examples.rst | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ doc/primer.rst | 2 ++ 2 files changed, 77 insertions(+) diff --git a/doc/examples.rst b/doc/examples.rst index 0f3662e0..559f5b3b 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -460,6 +460,32 @@ This example uses an offset workplane to make a compound object, which is perfec * :py:meth:`Workplane.box` * :py:meth:`Workplane` +Copying Workplanes +-------------------------- + +An existing CQ object can copy a workplane from another CQ object. + +.. cq_plot:: + + result = (cq.Workplane("front").circle(1).extrude(10) # make a cylinder + # We want to make a second cylinder perpendicular to the first, + # but we have no face to base the workplane off + .copyWorkplane( + # create a temporary object with the required workplane + cq.Workplane("right", origin=(-5, 0, 0)) + ).circle(1).extrude(10)) + show_object(result) + +.. topic:: API References + + .. hlist: + :columns: 2 + + * :py:meth:`CQ.copyWorkplane` **!** + * :py:meth:`Workplane.circle` + * :py:meth:`Workplane.extrude` + * :py:meth:`Workplane` + Rotated Workplanes -------------------------- @@ -604,6 +630,55 @@ Here we fillet all of the edges of a simple plate. * :py:meth:`Workplane.edges` * :py:meth:`Workplane` +Tagging objects +---------------- + +The :py:meth:`CQ.tag` method can be used to tag a particular object in the chain with a string. At a later point in the chain, the method :py:meth:`CQ.getTagged` can be used to return the previously tagged object (note that CQ objects share the modelling context throughout a chain, so going back to a previously tagged object will not change what is in the modelling context eg. unextruded edges). + +The method :py:meth:`CQ.getTagged` is awkward to use in chained calls (see :ref:`chaining`), the main utility of tags is when they are combined with other functions. The :py:meth:`CQ.workplaneFromTagged` method applies :py:meth:`CQ.copyWorkplane` to a tagged object. For example, when extruding two different solids from a surface, after the first solid is extruded it can become difficult to reselect the original surface with CadQuery's other selectors. + +.. cq_plot:: + + result = (cq.Workplane("XY") + # create and tag the base workplane + .box(10, 10, 10).faces(">Z").workplane().tag("baseplane") + # extrude a cylinder + .center(-3, 0).circle(1).extrude(3) + # to reselect the base workplane, simply + .workplaneFromTagged("baseplane") + # extrude a second cylinder + .center(3, 0).circle(1).extrude(2)) + show_object(result) + + +Tags can also be used with most selectors, including :py:meth:`CQ.vertices`, :py:meth:`CQ.faces`, :py:meth:`CQ.edges`, :py:meth:`CQ.wires`, :py:meth:`CQ.shells`, :py:meth:`CQ.solids` and :py:meth:`CQ.compounds`. + +.. cq_plot:: + + result = (cq.Workplane("XY") + # create a triangular prism and tag it + .polygon(3, 5).extrude(4).tag("prism") + # create a sphere that obscures the prism + .sphere(10) + # create features based on the prism's faces + .faces("X", tag="prism").faces(">Y").workplane().circle(1).cutThruAll()) + show_object(result) + +.. topic:: Api References + + .. hlist:: + :columns: 2 + + * :py:meth:`CQ.tag` **!** + * :py:meth:`CQ.getTagged` **!** + * :py:meth:`CQ.workplaneFromTagged` **!** + * :py:meth:`Workplane.extrude` + * :py:meth:`Workplane.cutThruAll` + * :py:meth:`Workplane.circle` + * :py:meth:`Workplane.faces` + * :py:meth:`Workplane` + A Parametric Bearing Pillow Block ------------------------------------ diff --git a/doc/primer.rst b/doc/primer.rst index f036de3f..a8ee7e8b 100644 --- a/doc/primer.rst +++ b/doc/primer.rst @@ -111,6 +111,8 @@ backwards in the stack to get the face as well:: You can browse stack access methods here: :ref:`stackMethods`. +.. _chaining: + Chaining --------------------------- From fa3277e6a0cdcbe85670a86b566433e82ff107be Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Sun, 19 Jan 2020 08:41:43 +1030 Subject: [PATCH 55/70] Renamed getTagged to _getTagged --- cadquery/cq.py | 8 ++++---- tests/test_cadquery.py | 26 +------------------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index aa2225d3..818c7734 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -279,7 +279,7 @@ class CQ(object): """ return self.objects[0] - def getTagged(self, name): + def _getTagged(self, name): """ Search the parent chain for a an object with tag == name. @@ -293,7 +293,7 @@ class CQ(object): if self.parent is None: raise ValueError("No CQ object named {} in chain".format(name)) else: - return self.parent.getTagged(name) + return self.parent._getTagged(name) def toOCC(self): """ @@ -480,7 +480,7 @@ class CQ(object): :type name: string :returns: a CQ object with name's workplane """ - tagged = self.getTagged(name) + tagged = self._getTagged(name) out = self.copyWorkplane(tagged) return out @@ -590,7 +590,7 @@ class CQ(object): solids,shells, and other similar selector methods. It is a useful extension point for plugin developers to make other selector methods. """ - cq_obj = self.getTagged(tag) if tag else self + cq_obj = self._getTagged(tag) if tag else self # A single list of all faces from all objects on the stack toReturn = cq_obj._collectProperty(objType) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 0520fb41..64e8e36a 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2751,33 +2751,9 @@ class TestCadQuery(BaseTest): .box(1, 1, 1, combine=False).tag("2 solids") .union(Workplane("XY").box(6, 1, 1))) self.assertEqual(len(result.objects), 1) - result = result.getTagged("2 solids") + result = result._getTagged("2 solids") self.assertEqual(len(result.objects), 2) - # tags can be used to create geometry for construction - part = ( # create a base solid - Workplane("XY").box(1, 1, 1).tag("base") - # do some stuff "for construction" - .faces(">Y").workplane().box(1, 3, 1) - .faces(">Z").workplane().box(2, 1, 1) - .faces(">X").workplane() - # create an edge, which CadQuery adds to the modelling context - .circle(0.5) - # go back to base object, but modelling context is preserved - .getTagged("base") - .faces(">Z").workplane().circle(0.5) - # loft between the top of the base object and the wire at the end of the "for construction" code - .loft() - ) - # assert face is made at the end of the construction geometry - self.assertTupleAlmostEquals(part.faces(">X").val().Center().toTuple(), - (1.0, 0.5, 1.5), - 9) - # assert face points in the x-direction, like the construction geometry - self.assertTupleAlmostEquals(part.faces(">X").val().normalAt().toTuple(), - (1.0, 0, 0), - 9) - def testCopyWorkplane(self): obj0 = Workplane("XY").box(1, 1, 10).faces(">Z").workplane() From 911c08f453e2edf666655d29d7224fb3f1b3d2f1 Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Thu, 23 Jan 2020 20:26:17 +1030 Subject: [PATCH 56/70] removed getTagged from docs --- doc/examples.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/examples.rst b/doc/examples.rst index 559f5b3b..a25975e4 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -633,9 +633,9 @@ Here we fillet all of the edges of a simple plate. Tagging objects ---------------- -The :py:meth:`CQ.tag` method can be used to tag a particular object in the chain with a string. At a later point in the chain, the method :py:meth:`CQ.getTagged` can be used to return the previously tagged object (note that CQ objects share the modelling context throughout a chain, so going back to a previously tagged object will not change what is in the modelling context eg. unextruded edges). +The :py:meth:`CQ.tag` method can be used to tag a particular object in the chain with a string, so that it can be refered to later in the chain. -The method :py:meth:`CQ.getTagged` is awkward to use in chained calls (see :ref:`chaining`), the main utility of tags is when they are combined with other functions. The :py:meth:`CQ.workplaneFromTagged` method applies :py:meth:`CQ.copyWorkplane` to a tagged object. For example, when extruding two different solids from a surface, after the first solid is extruded it can become difficult to reselect the original surface with CadQuery's other selectors. +The :py:meth:`CQ.workplaneFromTagged` method applies :py:meth:`CQ.copyWorkplane` to a tagged object. For example, when extruding two different solids from a surface, after the first solid is extruded it can become difficult to reselect the original surface with CadQuery's other selectors. .. cq_plot:: @@ -664,7 +664,7 @@ Tags can also be used with most selectors, including :py:meth:`CQ.vertices`, :py .faces("X", tag="prism").faces(">Y").workplane().circle(1).cutThruAll()) show_object(result) - + .. topic:: Api References .. hlist:: From 04c8b80084dcd0c21fd1fbedd3499a42363ed79e Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Thu, 23 Jan 2020 20:43:34 +1030 Subject: [PATCH 57/70] Added tags to primer/concepts docs --- doc/primer.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/doc/primer.rst b/doc/primer.rst index a8ee7e8b..88d2c7f5 100644 --- a/doc/primer.rst +++ b/doc/primer.rst @@ -119,6 +119,11 @@ Chaining All CadQuery methods return another CadQuery object, so that you can chain the methods together fluently. Use the core CQ methods to get at the objects that were created. +Each time a new CadQuery object is produced during these chained calls, it has a ``parent`` attribute that points +to the CadQuery object that created it. Several CadQuery methods search this parent chain, for example when searching +for the context solid. You can also give a CadQuery object a tag, and further down your chain of CadQuery calls you +can refer back to this particular object using it's tag. + The Context Solid --------------------------- From b2c1094b8262158930be9377594d0f87d1ac21a4 Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Thu, 23 Jan 2020 21:16:44 +1030 Subject: [PATCH 58/70] removed CQ.firstPoint from CQ.__init__ --- cadquery/cq.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 61496f2f..af074c0d 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1002,7 +1002,6 @@ class Workplane(CQ): self.obj = obj self.plane = tmpPlane - self.firstPoint = None # Changed so that workplane has the center as the first item on the stack self.objects = [self.plane.origin] self.parent = None From 95fb436081312b670b5ec618f51d1db23f722e98 Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Thu, 23 Jan 2020 21:25:32 +1030 Subject: [PATCH 59/70] test_cadquery formatting --- tests/test_cadquery.py | 62 +++++++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 64e8e36a..9f885156 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2746,10 +2746,13 @@ class TestCadQuery(BaseTest): def testTag(self): # test tagging - Workplane("XY").pushPoints([(-2, 0), (2, 0)]).box(1, 1, 1, combine=False) - result = (Workplane("XY").pushPoints([(-2, 0), (2, 0)]) - .box(1, 1, 1, combine=False).tag("2 solids") - .union(Workplane("XY").box(6, 1, 1))) + result = ( + Workplane("XY") + .pushPoints([(-2, 0), (2, 0)]) + .box(1, 1, 1, combine=False) + .tag("2 solids") + .union(Workplane("XY") .box(6, 1, 1)) + ) self.assertEqual(len(result.objects), 1) result = result._getTagged("2 solids") self.assertEqual(len(result.objects), 2) @@ -2758,23 +2761,34 @@ class TestCadQuery(BaseTest): obj0 = Workplane("XY").box(1, 1, 10).faces(">Z").workplane() obj1 = Workplane("XY").copyWorkplane(obj0).box(1, 1, 1) - self.assertTupleAlmostEquals((0, 0, 5), - obj1.val().Center().toTuple(), - 9) + self.assertTupleAlmostEquals((0, 0, 5), obj1.val().Center().toTuple(), 9) def testWorkplaneFromTagged(self): # create a flat, wide base. Extrude one object 4 units high, another # object ontop of it 6 units high. Go back to base plane. Extrude an # object 11 units high. Assert that top face is 11 units high. - result = (Workplane("XY").box(10, 10, 1, centered=(True, True, False)) - .faces(">Z").workplane().tag("base").center(3, 0).rect(2, 2) - .extrude(4).faces(">Z").workplane().circle(1).extrude(6) - .workplaneFromTagged("base").center(-3, 0).circle(1) - .extrude(11)) - self.assertTupleAlmostEquals(result.faces(">Z").val().Center().toTuple(), - (-3, 0, 12), - 9) + result = ( + Workplane("XY") + .box(10, 10, 1, centered=(True, True, False)) + .faces(">Z") + .workplane() + .tag("base") + .center(3, 0) + .rect(2, 2) + .extrude(4) + .faces(">Z") + .workplane() + .circle(1) + .extrude(6) + .workplaneFromTagged("base") + .center(-3, 0) + .circle(1) + .extrude(11) + ) + self.assertTupleAlmostEquals( + result.faces(">Z").val().Center().toTuple(), (-3, 0, 12), 9 + ) def testTagSelectors(self): @@ -2791,16 +2805,26 @@ class TestCadQuery(BaseTest): self.assertEqual(6, result0.wires(tag="box").size()) # create two solids, tag them, join to one solid - result1 = (Workplane("XY").pushPoints([(1, 0), (-1, 0)]).box(1, 1, 1) - .tag("boxes").sphere(1)) + result1 = ( + Workplane("XY") + .pushPoints([(1, 0), (-1, 0)]) + .box(1, 1, 1) + .tag("boxes") + .sphere(1) + ) self.assertEqual(1, result1.solids().size()) self.assertEqual(2, result1.solids(tag="boxes").size()) self.assertEqual(1, result1.shells().size()) self.assertEqual(2, result1.shells(tag="boxes").size()) # create 4 individual objects, tag it, then combine to one compound - result2 = (Workplane("XY").rect(4, 4).vertices() - .box(1, 1, 1, combine=False).tag("4 objs")) + result2 = ( + Workplane("XY") + .rect(4, 4) + .vertices() + .box(1, 1, 1, combine=False) + .tag("4 objs") + ) result2 = result2.newObject([Compound.makeCompound(result2.objects)]) self.assertEqual(1, result2.compounds().size()) self.assertEqual(0, result2.compounds(tag="4 objs").size()) From 09da93a01422fd34046206d3c3ec59722daeddc9 Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Thu, 23 Jan 2020 22:22:53 +1030 Subject: [PATCH 60/70] formatting --- tests/test_cadquery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 9f885156..2057e58f 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2751,7 +2751,7 @@ class TestCadQuery(BaseTest): .pushPoints([(-2, 0), (2, 0)]) .box(1, 1, 1, combine=False) .tag("2 solids") - .union(Workplane("XY") .box(6, 1, 1)) + .union(Workplane("XY").box(6, 1, 1)) ) self.assertEqual(len(result.objects), 1) result = result._getTagged("2 solids") From 3d9041e98bdd9312118c4e2abfc70317ee892f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20S=C3=A1nchez=20de=20Le=C3=B3n=20Peque?= Date: Mon, 24 Feb 2020 20:45:36 +0100 Subject: [PATCH 61/70] Fix plane rotation method (again...) (#243) --- cadquery/occ_impl/geom.py | 24 ++++++++---- tests/test_cadquery.py | 81 +++++++++++++++++++++++++++++++-------- 2 files changed, 82 insertions(+), 23 deletions(-) diff --git a/cadquery/occ_impl/geom.py b/cadquery/occ_impl/geom.py index 812f6dc2..ff30c826 100644 --- a/cadquery/occ_impl/geom.py +++ b/cadquery/occ_impl/geom.py @@ -634,19 +634,29 @@ class Plane(object): :param rotate: Vector [xDegrees, yDegrees, zDegrees] :return: a copy of this plane rotated as requested. """ - rotate = Vector(self.toWorldCoords(rotate)) + # NB: this is not a geometric Vector + rotate = Vector(rotate) # Convert to radians. rotate = rotate.multiply(math.pi / 180.0) # Compute rotation matrix. - m = Matrix() - m.rotateX(rotate.x) - m.rotateY(rotate.y) - m.rotateZ(rotate.z) + T1 = gp_Trsf() + T1.SetRotation( + gp_Ax1(gp_Pnt(*(0, 0, 0)), gp_Dir(*self.xDir.toTuple())), rotate.x + ) + T2 = gp_Trsf() + T2.SetRotation( + gp_Ax1(gp_Pnt(*(0, 0, 0)), gp_Dir(*self.yDir.toTuple())), rotate.y + ) + T3 = gp_Trsf() + T3.SetRotation( + gp_Ax1(gp_Pnt(*(0, 0, 0)), gp_Dir(*self.zDir.toTuple())), rotate.z + ) + T = Matrix(gp_GTrsf(T1 * T2 * T3)) # Compute the new plane. - newXdir = self.xDir.transform(m) - newZdir = self.zDir.transform(m) + newXdir = self.xDir.transform(T) + newZdir = self.zDir.transform(T) return Plane(self.origin, newXdir, newZdir) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 2057e58f..88a17e84 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -8,6 +8,8 @@ from random import choice from random import random from random import randrange +from pytest import approx + # my modules from cadquery import * from cadquery import exporters @@ -283,27 +285,74 @@ class TestCadQuery(BaseTest): """ Rotation of a plane in the Z direction should never alter its normal. - This test creates random planes, with the normal in a random direction - among positive and negative X, Y and Z. The plane is defined with this - normal and another random perpendicular vector (the X-direction of the - plane). The plane is finally rotated a random angle in the Z-direction - to verify that the resulting plane maintains the same normal. + This test creates random planes. The plane is rotated a random angle in + the Z-direction to verify that the resulting plane maintains the same + normal. + + The test also checks that the random origin is unaltered after + rotation. """ for _ in range(100): - normal_sign = choice((-1, 1)) - normal_dir = randrange(3) angle = (random() - 0.5) * 720 + xdir = Vector(random(), random(), random()).normalized() + rdir = Vector(random(), random(), random()).normalized() + zdir = xdir.cross(rdir).normalized() + origin = (random(), random(), random()) + plane = Plane(origin=origin, xDir=xdir, normal=zdir) + rotated = plane.rotated((0, 0, angle)) + assert rotated.zDir.toTuple() == approx(zdir.toTuple()) + assert rotated.origin.toTuple() == approx(origin) - normal = [0, 0, 0] - normal[normal_dir] = normal_sign - xdir = [random(), random(), random()] - xdir[normal_dir] = 0 + def testPlaneRotateConcat(self): + """ + Test the result of a well-known concatenated rotation example. + """ + xdir = (1, 0, 0) + normal = (0, 0, 1) + k = 2.0 ** 0.5 / 2.0 + origin = (2, -1, 1) + plane = Plane(origin=origin, xDir=xdir, normal=normal) + plane = plane.rotated((0, 0, 45)) + assert plane.xDir.toTuple() == approx((k, k, 0)) + assert plane.yDir.toTuple() == approx((-k, k, 0)) + assert plane.zDir.toTuple() == approx((0, 0, 1)) + plane = plane.rotated((0, 45, 0)) + assert plane.xDir.toTuple() == approx((0.5, 0.5, -k)) + assert plane.yDir.toTuple() == approx((-k, k, 0)) + assert plane.zDir.toTuple() == approx((0.5, 0.5, k)) + assert plane.origin.toTuple() == origin - plane = Plane(origin=(0, 0, 0), xDir=xdir, normal=normal) - rotated = plane.rotated((0, 0, angle)).zDir.toTuple() - self.assertAlmostEqual(rotated[0], normal[0]) - self.assertAlmostEqual(rotated[1], normal[1]) - self.assertAlmostEqual(rotated[2], normal[2]) + def testPlaneRotateConcatRandom(self): + """ + Rotation of a plane in a given direction should never alter that + direction. + + This test creates a plane and rotates it a random angle in a given + direction. After the rotation, the direction of the resulting plane + in the rotation-direction should be constant. + + The test also checks that the origin is unaltered after all rotations. + """ + origin = (2, -1, 1) + plane = Plane(origin=origin, xDir=(1, 0, 0), normal=(0, 0, 1)) + for _ in range(100): + before = { + 0: plane.xDir.toTuple(), + 1: plane.yDir.toTuple(), + 2: plane.zDir.toTuple(), + } + angle = (random() - 0.5) * 720 + direction = randrange(3) + rotation = [0, 0, 0] + rotation[direction] = angle + plane = plane.rotated(rotation) + after = { + 0: plane.xDir.toTuple(), + 1: plane.yDir.toTuple(), + 2: plane.zDir.toTuple(), + } + assert before[direction] == approx(after[direction]) + assert plane.origin.toTuple() == origin def testLoft(self): """ From cd66589e4e9a020d9dc9a446df100b026db0e44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Thu, 5 Mar 2020 21:43:16 +0100 Subject: [PATCH 62/70] Implementation of BRepOffsetAPI_MakeFilling (#253) --- cadquery/cq.py | 84 ++++++++++++++++ cadquery/occ_impl/shapes.py | 183 ++++++++++++++++++++++++++++++++++ examples/Ex101_InterpPlate.py | 175 ++++++++++++++++++++++++++++++++ tests/__init__.py | 1 - tests/test_cadquery.py | 176 ++++++++++++++++++++++++++++++++ 5 files changed, 618 insertions(+), 1 deletion(-) create mode 100644 examples/Ex101_InterpPlate.py diff --git a/cadquery/cq.py b/cadquery/cq.py index 25dc1d3e..4fee00e6 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -2862,6 +2862,90 @@ class Workplane(CQ): return Compound.makeCompound(toFuse) + def interpPlate( + self, + surf_edges, + surf_pts=[], + thickness=0, + combine=False, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a plate surface that is 'thickness' thick, enclosed by 'surf_edge_pts' points, and going through 'surf_pts' points. Using pushpoints directly with interpPlate and combine=True, can be very ressources intensive depending on the complexity of the shape. In this case set combine=False. + + :param surf_edges + :type 1 surf_edges: list of [x,y,z] float ordered coordinates + :type 2 surf_edges: list of ordered or unordered CadQuery wires + :param surf_pts = [] (uses only edges if []) + :type surf_pts: list of [x,y,z] float coordinates + :param thickness = 0 (returns 2D surface if 0) + :type thickness: float (may be negative or positive depending on thicknening direction) + :param combine: should the results be combined with other solids on the stack + (and each other)? + :type combine: true to combine shapes, false otherwise. + :param boolean clean: call :py:meth:`clean` afterwards to have a clean shape + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + # If thickness is 0, only a 2D surface will be returned. + if thickness == 0: + combine = False + + # Creates interpolated plate + def _makeplate(pnt): + return Solid.interpPlate( + surf_edges, + surf_pts, + thickness, + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ).translate(pnt) + + plates = self.eachpoint(_makeplate, True) + + # if combination is not desired, just return the created boxes + if not combine: + return plates + else: + return self.union(plates, clean=clean) + def box( self, length, diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index caa83f69..72c4e7b9 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -127,6 +127,14 @@ from OCC.Core.BRepFeat import BRepFeat_MakeDPrism from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier +from OCC.Core.GeomAbs import GeomAbs_C0 +from OCC.Extend.TopologyUtils import TopologyExplorer, WireExplorer +from OCC.Core.GeomAbs import GeomAbs_Intersection +from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_MakeFilling +from OCC.Core.BRepOffset import BRepOffset_MakeOffset, BRepOffset_Skin +from OCC.Core.ShapeFix import ShapeFix_Wire + +import warnings from math import pi, sqrt from functools import reduce import warnings @@ -991,6 +999,73 @@ class Face(Shape): return [w for w in self.Wires() if not w.isSame(outer)] + @classmethod + def makeNSidedSurface( + cls, + edges, + points, + continuity=GeomAbs_C0, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a surface enclosed by a closed polygon defined by 'edges' and going through 'points'. + :param points + :type points: list of gp_Pnt + :param edges + :type edges: list of TopologyExplorer().edges() + :param continuity=GeomAbs_C0 + :type continuity: OCC.Core.GeomAbs continuity condition + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + n_sided = BRepOffsetAPI_MakeFilling( + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ) + for edg in edges: + n_sided.Add(edg, continuity) + for pt in points: + n_sided.Add(pt) + n_sided.Build() + face = n_sided.Shape() + return cls.cast(face).fix() + @classmethod def makePlane(cls, length, width, basePnt=(0, 0, 0), dir=(0, 0, 1)): basePnt = Vector(basePnt) @@ -1165,6 +1240,114 @@ class Solid(Shape, Mixin3D): a single solid """ + @classmethod + def interpPlate( + cls, + surf_edges, + surf_pts, + thickness, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ): + """ + Returns a plate surface that is 'thickness' thick, enclosed by 'surf_edge_pts' points, and going through 'surf_pts' points. + + :param surf_edges + :type 1 surf_edges: list of [x,y,z] float ordered coordinates + :type 2 surf_edges: list of ordered or unordered CadQuery wires + :param surf_pts = [] (uses only edges if []) + :type surf_pts: list of [x,y,z] float coordinates + :param thickness = 0 (returns 2D surface if 0) + :type thickness: float (may be negative or positive depending on thicknening direction) + :param Degree = 3 (OCCT default) + :type Degree: Integer >= 2 + :param NbPtsOnCur = 15 (OCCT default) + :type: NbPtsOnCur Integer >= 15 + :param NbIter = 2 (OCCT default) + :type: NbIterInteger >= 2 + :param Anisotropie = False (OCCT default) + :type Anisotropie: Boolean + :param: Tol2d = 0.00001 (OCCT default) + :type Tol2d: float > 0 + :param Tol3d = 0.0001 (OCCT default) + :type Tol3dReal: float > 0 + :param TolAng = 0.01 (OCCT default) + :type TolAngReal: float > 0 + :param TolCurv = 0.1 (OCCT default) + :type TolCurvReal: float > 0 + :param MaxDeg = 8 (OCCT default) + :type MaxDegInteger: Integer >= 2 (?) + :param MaxSegments = 9 (OCCT default) + :type MaxSegments: Integer >= 2 (?) + """ + + # POINTS CONSTRAINTS: list of (x,y,z) points, optional. + pts_array = [gp_Pnt(*pt) for pt in surf_pts] + + # EDGE CONSTRAINTS + # If a list of wires is provided, make a closed wire + if not isinstance(surf_edges, list): + surf_edges = [o.vals()[0] for o in surf_edges.all()] + surf_edges = Wire.assembleEdges(surf_edges) + w = surf_edges.wrapped + + # If a list of (x,y,z) points provided, build closed polygon + if isinstance(surf_edges, list): + e_array = [Vector(*e) for e in surf_edges] + wire_builder = BRepBuilderAPI_MakePolygon() + for e in e_array: # Create polygon from edges + wire_builder.Add(e.toPnt()) + wire_builder.Close() + w = wire_builder.Wire() + + edges = [i for i in TopologyExplorer(w).edges()] + + # MAKE SURFACE + continuity = GeomAbs_C0 # Fixed, changing to anything else crashes. + face = Face.makeNSidedSurface( + edges, + pts_array, + continuity, + degree, + nbPtsOnCur, + nbIter, + anisotropy, + tol2d, + tol3d, + tolAng, + tolCurv, + maxDeg, + maxSegments, + ) + + # THICKEN SURFACE + if ( + abs(thickness) > 0 + ): # abs() because negative values are allowed to set direction of thickening + solid = BRepOffset_MakeOffset() + solid.Initialize( + face.wrapped, + thickness, + 1.0e-5, + BRepOffset_Skin, + False, + False, + GeomAbs_Intersection, + True, + ) # The last True is important to make solid + solid.MakeOffsetShape() + return cls(solid.Shape()) + else: # Return 2D surface only + return face + @classmethod def isSolid(cls, obj): """ diff --git a/examples/Ex101_InterpPlate.py b/examples/Ex101_InterpPlate.py new file mode 100644 index 00000000..aa7db4cd --- /dev/null +++ b/examples/Ex101_InterpPlate.py @@ -0,0 +1,175 @@ +from math import sin, cos, pi, sqrt +import cadquery as cq + +# TEST_1 +# example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. +thickness = 0 +edge_points = [[0.0, 0.0, 0.0], [0.0, 10.0, 0.0], [0.0, 10.0, 10.0], [0.0, 0.0, 10.0]] +surface_points = [[5.0, 5.0, 5.0]] +plate_0 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness) +print("plate_0.val().Volume() = ", plate_0.val().Volume()) +plate_0 = plate_0.translate((0, 6 * 12, 0)) +show_object(plate_0) + +# EXAMPLE 1 +# Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides +thickness = 0.1 +edge_points = [ + [-7.0, -7.0, 0.0], + [-3.0, -10.0, 3.0], + [7.0, -7.0, 0.0], + [7.0, 7.0, 0.0], + [-7.0, 7.0, 0.0], +] +edge_wire = cq.Workplane("XY").polyline( + [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] +) +# edge_wire = edge_wire.add(cq.Workplane("YZ").workplane().transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)])) +# In CadQuery Sept-2019 it worked with rotate=cq.Vector(0, 45, 0). In CadQuery Dec-2019 rotate=cq.Vector(45, 0, 0) only closes the wire. +edge_wire = edge_wire.add( + cq.Workplane("YZ") + .workplane() + .transformed(offset=cq.Vector(0, 0, -7), rotate=cq.Vector(45, 0, 0)) + .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) +) +surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]] +plate_1 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness) +# plate_1 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness) # list of (x,y,z) points instead of wires for edges +print("plate_1.val().Volume() = ", plate_1.val().Volume()) +show_object(plate_1) + +# EXAMPLE 2 +# Embossed star, need to change optional parameters to obtain nice looking result. +r1 = 3.0 +r2 = 10.0 +fn = 6 +thickness = 0.1 +edge_points = [ + [r1 * cos(i * pi / fn), r1 * sin(i * pi / fn)] + if i % 2 == 0 + else [r2 * cos(i * pi / fn), r2 * sin(i * pi / fn)] + for i in range(2 * fn + 1) +] +edge_wire = cq.Workplane("XY").polyline(edge_points) +r2 = 4.5 +surface_points = [ + [r2 * cos(i * pi / fn), r2 * sin(i * pi / fn), 1.0] for i in range(2 * fn) +] + [[0.0, 0.0, -2.0]] +plate_2 = cq.Workplane("XY").interpPlate( + edge_wire, + surface_points, + thickness, + combine=True, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=49, +) +# plate_2 = cq.Workplane("XY").interpPlate(edge_points, surface_points, thickness, combine=True, clean=True, Degree=3, NbPtsOnCur=15, NbIter=2, Anisotropie=False, Tol2d=0.00001, Tol3d=0.0001, TolAng=0.01, TolCurv=0.1, MaxDeg=8, MaxSegments=49) # list of (x,y,z) points instead of wires for edges +print("plate_2.val().Volume() = ", plate_2.val().Volume()) +plate_2 = plate_2.translate((0, 2 * 12, 0)) +show_object(plate_2) + +# EXAMPLE 3 +# Points on hexagonal pattern coordinates, use of pushpoints. +r1 = 1.0 +N = 3 +ca = cos(30.0 * pi / 180.0) +sa = sin(30.0 * pi / 180.0) +# EVEN ROWS +pts = [ + (-3.0, -3.0), + (-1.267949, -3.0), + (0.464102, -3.0), + (2.196152, -3.0), + (-3.0, 0.0), + (-1.267949, 0.0), + (0.464102, 0.0), + (2.196152, 0.0), + (-2.133974, -1.5), + (-0.401923, -1.5), + (1.330127, -1.5), + (3.062178, -1.5), + (-2.133975, 1.5), + (-0.401924, 1.5), + (1.330127, 1.5), + (3.062178, 1.5), +] +# Spike surface +thickness = 0.1 +fn = 6 +edge_points = [ + [ + r1 * cos(i * 2 * pi / fn + 30 * pi / 180), + r1 * sin(i * 2 * pi / fn + 30 * pi / 180), + ] + for i in range(fn + 1) +] +surface_points = [ + [ + r1 / 4 * cos(i * 2 * pi / fn + 30 * pi / 180), + r1 / 4 * sin(i * 2 * pi / fn + 30 * pi / 180), + 0.75, + ] + for i in range(fn + 1) +] + [[0, 0, 2]] +edge_wire = cq.Workplane("XY").polyline(edge_points) +plate_3 = ( + cq.Workplane("XY") + .pushPoints(pts) + .interpPlate( + edge_wire, + surface_points, + thickness, + combine=False, + clean=False, + degree=2, + nbPtsOnCur=20, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ) +) +print("plate_3.val().Volume() = ", plate_3.val().Volume()) +plate_3 = plate_3.translate((0, 4 * 11, 0)) +show_object(plate_3) + +# EXAMPLE 4 +# Gyroïd, all edges are splines on different workplanes. +thickness = 0.1 +edge_points = [ + [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], +] +plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] +offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] +edge_wire = ( + cq.Workplane(plane_list[0]).workplane(offset=-offset_list[0]).spline(edge_points[0]) +) +for i in range(len(edge_points) - 1): + edge_wire = edge_wire.add( + cq.Workplane(plane_list[i + 1]) + .workplane(offset=-offset_list[i + 1]) + .spline(edge_points[i + 1]) + ) +surface_points = [[0, 0, 0]] +plate_4 = cq.Workplane("XY").interpPlate(edge_wire, surface_points, thickness) +print("plate_4.val().Volume() = ", plate_4.val().Volume()) +plate_4 = plate_4.translate((0, 5 * 12, 0)) +show_object(plate_4) diff --git a/tests/__init__.py b/tests/__init__.py index b36542e9..42ac9093 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -59,5 +59,4 @@ __all__ = [ "TestImporters", "TestJupyter", "TestWorkplanes", - "TestAssembleEdges", ] diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 88a17e84..f50f8982 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -2877,3 +2877,179 @@ class TestCadQuery(BaseTest): result2 = result2.newObject([Compound.makeCompound(result2.objects)]) self.assertEqual(1, result2.compounds().size()) self.assertEqual(0, result2.compounds(tag="4 objs").size()) + + def test_interpPlate(self): + """ + Tests the interpPlate() functionnalites + Numerical values of Areas and Volumes were obtained with the Area() and Volume() functions on a Linux machine under Debian 10 with python 3.7. + """ + + # example from PythonOCC core_geometry_geomplate.py, use of thickness = 0 returns 2D surface. + thickness = 0 + edge_points = [ + [0.0, 0.0, 0.0], + [0.0, 10.0, 0.0], + [0.0, 10.0, 10.0], + [0.0, 0.0, 10.0], + ] + surface_points = [[5.0, 5.0, 5.0]] + plate_0 = Workplane("XY").interpPlate(edge_points, surface_points, thickness) + self.assertTrue(plate_0.val().isValid()) + self.assertAlmostEqual(plate_0.val().Area(), 141.218823892, 1) + + # Plate with 5 sides and 2 bumps, one side is not co-planar with the other sides + thickness = 0.1 + edge_points = [ + [-7.0, -7.0, 0.0], + [-3.0, -10.0, 3.0], + [7.0, -7.0, 0.0], + [7.0, 7.0, 0.0], + [-7.0, 7.0, 0.0], + ] + edge_wire = Workplane("XY").polyline( + [(-7.0, -7.0), (7.0, -7.0), (7.0, 7.0), (-7.0, 7.0)] + ) + # edge_wire = edge_wire.add(Workplane('YZ').workplane().transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)).polyline([(-7.,0.), (3,-3), (7.,0.)])) + # In CadQuery Sept-2019 it worked with rotate=Vector(0, 45, 0). In CadQuery Dec-2019 rotate=Vector(45, 0, 0) only closes the wire. + edge_wire = edge_wire.add( + Workplane("YZ") + .workplane() + .transformed(offset=Vector(0, 0, -7), rotate=Vector(45, 0, 0)) + .spline([(-7.0, 0.0), (3, -3), (7.0, 0.0)]) + ) + surface_points = [[-3.0, -3.0, -3.0], [3.0, 3.0, 3.0]] + plate_1 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness) + self.assertTrue(plate_1.val().isValid()) + self.assertAlmostEqual(plate_1.val().Volume(), 26.124970206, 3) + + # Embossed star, need to change optional parameters to obtain nice looking result. + r1 = 3.0 + r2 = 10.0 + fn = 6 + thickness = 0.1 + edge_points = [ + [r1 * math.cos(i * math.pi / fn), r1 * math.sin(i * math.pi / fn)] + if i % 2 == 0 + else [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn)] + for i in range(2 * fn + 1) + ] + edge_wire = Workplane("XY").polyline(edge_points) + r2 = 4.5 + surface_points = [ + [r2 * math.cos(i * math.pi / fn), r2 * math.sin(i * math.pi / fn), 1.0] + for i in range(2 * fn) + ] + [[0.0, 0.0, -2.0]] + plate_2 = Workplane("XY").interpPlate( + edge_wire, + surface_points, + thickness, + combine=True, + clean=True, + degree=3, + nbPtsOnCur=15, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=49, + ) + self.assertTrue(plate_2.val().isValid()) + self.assertAlmostEqual(plate_2.val().Volume(), 10.956054314, 0) + + # Points on hexagonal pattern coordinates, use of pushpoints. + r1 = 1.0 + N = 3 + ca = math.cos(30.0 * math.pi / 180.0) + sa = math.sin(30.0 * math.pi / 180.0) + # EVEN ROWS + pts = [ + (-3.0, -3.0), + (-1.267949, -3.0), + (0.464102, -3.0), + (2.196152, -3.0), + (-3.0, 0.0), + (-1.267949, 0.0), + (0.464102, 0.0), + (2.196152, 0.0), + (-2.133974, -1.5), + (-0.401923, -1.5), + (1.330127, -1.5), + (3.062178, -1.5), + (-2.133975, 1.5), + (-0.401924, 1.5), + (1.330127, 1.5), + (3.062178, 1.5), + ] + # Spike surface + thickness = 0.1 + fn = 6 + edge_points = [ + [ + r1 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180), + r1 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180), + ] + for i in range(fn + 1) + ] + surface_points = [ + [ + r1 / 4 * math.cos(i * 2 * math.pi / fn + 30 * math.pi / 180), + r1 / 4 * math.sin(i * 2 * math.pi / fn + 30 * math.pi / 180), + 0.75, + ] + for i in range(fn + 1) + ] + [[0, 0, 2]] + edge_wire = Workplane("XY").polyline(edge_points) + plate_3 = ( + Workplane("XY") + .pushPoints(pts) + .interpPlate( + edge_wire, + surface_points, + thickness, + combine=False, + clean=False, + degree=2, + nbPtsOnCur=20, + nbIter=2, + anisotropy=False, + tol2d=0.00001, + tol3d=0.0001, + tolAng=0.01, + tolCurv=0.1, + maxDeg=8, + maxSegments=9, + ) + ) + self.assertTrue(plate_3.val().isValid()) + self.assertAlmostEqual(plate_3.val().Volume(), 0.45893954685189414, 1) + + # Gyroïd, all edges are splines on different workplanes. + thickness = 0.1 + edge_points = [ + [[3.54, 3.54], [1.77, 0.0], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [0.0, -1.77], [3.54, -3.54]], + [[-3.54, -3.54], [-1.77, 0.0], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + [[3.54, 3.54], [0.0, 1.77], [-3.54, 3.54]], + ] + plane_list = ["XZ", "XY", "YZ", "XZ", "YZ", "XY"] + offset_list = [-3.54, 3.54, 3.54, 3.54, -3.54, -3.54] + edge_wire = ( + Workplane(plane_list[0]) + .workplane(offset=-offset_list[0]) + .spline(edge_points[0]) + ) + for i in range(len(edge_points) - 1): + edge_wire = edge_wire.add( + Workplane(plane_list[i + 1]) + .workplane(offset=-offset_list[i + 1]) + .spline(edge_points[i + 1]) + ) + surface_points = [[0, 0, 0]] + plate_4 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness) + self.assertTrue(plate_4.val().isValid()) + self.assertAlmostEqual(plate_4.val().Volume(), 7.760559490, 3) From b9d3a1c18704edba7361256622acd767456240f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Fri, 6 Mar 2020 22:18:31 +0100 Subject: [PATCH 63/70] Spline tangnet fix for offset workplane (#293) Fixes #292 --- cadquery/cq.py | 5 ++++- tests/test_cadquery.py | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 4fee00e6..2332a5b5 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1535,7 +1535,10 @@ class Workplane(CQ): if tangents: t1, t2 = tangents - tangents = (self.plane.toWorldCoords(t1), self.plane.toWorldCoords(t2)) + tangents = ( + self.plane.toWorldCoords(t1) - self.plane.origin, + self.plane.toWorldCoords(t2) - self.plane.origin, + ) e = Edge.makeSpline(allPoints, tangents=tangents, periodic=periodic) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index f50f8982..b07f9121 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -545,6 +545,13 @@ class TestCadQuery(BaseTest): path1 = Workplane("XZ").spline(pts[1:], includeCurrent=True).val() self.assertAlmostEqual(path.Length(), path1.Length()) + # test tangents and offset plane + pts = [(0, 0), (-1, 1), (-2, 0), (-1, 0)] + tangents = [(0, 1), (1, 0)] + + path2 = Workplane("XY", (0, 0, 10)).spline(pts, tangents=tangents) + self.assertAlmostEqual(path2.val().tangentAt(0).z, 0) + def testSweep(self): """ Tests the operation of sweeping a wire(s) along a path From b89bc44f86e7743bcf01cb021ab04fcc3bc5557a Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Sat, 22 Feb 2020 20:00:45 +1030 Subject: [PATCH 64/70] Added Workplane.tangentArcToPoint, occ_impl.shapes.Edge.makeTangentArc --- cadquery/cq.py | 63 +++++++++++++++++++++++++-- cadquery/occ_impl/shapes.py | 15 +++++++ doc/apireference.rst | 1 + tests/test_cad_objects.py | 18 ++++++++ tests/test_cadquery.py | 85 +++++++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 4 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index 4fee00e6..c7de5511 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1160,6 +1160,28 @@ class Workplane(CQ): else: return p + def _findFromEdge(self, useLocalCoords=False): + """ + Finds the previous edge for an operation that needs it, similar to + method _findFromPoint. Examples include tangentArcEndpoint. + + :param useLocalCoords: selects whether the point is returned + in local coordinates or global coordinates. + :return: an Edge + """ + obj = self.objects[-1] + + if not isinstance(obj, Edge): + raise RuntimeError( + "Previous Edge requested, but the previous object was of " + + f"type {type(obj)}, not an Edge." + ) + + if useLocalCoords: + obj = self.plane.toLocalCoords(obj) + + return obj + def rarray(self, xSpacing, ySpacing, xCount, yCount, center=True): """ Creates an array of points and pushes them onto the stack. @@ -1660,6 +1682,37 @@ class Workplane(CQ): else: return self.sagittaArc(endPoint, -sag, forConstruction) + def tangentArcEndpoint(self, endpoint, forConstruction=False, relative=True): + """ + Draw an arc as a tangent from the end of the current edge to endpoint. + + :param endpoint: point for the arc to end at + :type endpoint: 2-tuple or Vector + :param relative: True if endpoint is specified relative to the current point, False if endpoint is in workplane coordinates + :type relative: Bool + :return: a Workplane object with an arc on the stack + + Requires the the current first object on the stack is an Edge, as would + be the case after a lineTo operation or similar. + """ + + if not isinstance(endpoint, Vector): + endpoint = Vector(endpoint) + if relative: + endpoint = endpoint + self._findFromPoint(useLocalCoords=True) + endpoint = self.plane.toWorldCoords(endpoint) + + previousEdge = self._findFromEdge() + + arc = Edge.makeTangentArc( + previousEdge.endPoint(), previousEdge.tangentAt(1), endpoint + ) + + if not forConstruction: + self._addPendingEdge(arc) + + return self.newObject([arc]) + def rotateAndCopy(self, matrix): """ Makes a copy of all edges on the stack, rotates them according to the @@ -2094,11 +2147,13 @@ class Workplane(CQ): :return: a CQ object with a completed wire on the stack, if possible. - After 2-d drafting with lineTo,threePointArc, and polyline, it is necessary - to convert the edges produced by these into one or more wires. + After 2-d drafting with methods such as lineTo, threePointArc, + tangentArcEndpoint and polyline, it is necessary to convert the edges + produced by these into one or more wires. - When a set of edges is closed, cadQuery assumes it is safe to build the group of edges - into a wire. This example builds a simple triangular prism:: + When a set of edges is closed, cadQuery assumes it is safe to build + the group of edges into a wire. This example builds a simple triangular + prism:: s = Workplane().lineTo(1,0).lineTo(1,1).close().extrude(0.2) """ diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 72c4e7b9..02ed4190 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -790,6 +790,21 @@ class Edge(Shape, Mixin1D): return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) + @classmethod + def makeTangentArc(cls, v1, v2, v3): + """ + Makes a tangent arc from point v1, in the direction of v2 and ends at + v3. + :param cls: + :param v1: start vector + :param v2: tangent vector + :param v3: end vector + :return: an edge + """ + circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2._wrapped, v3.toPnt()).Value() + + return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) + @classmethod def makeLine(cls, v1, v2): """ diff --git a/doc/apireference.rst b/doc/apireference.rst index e83e7b93..84152d48 100644 --- a/doc/apireference.rst +++ b/doc/apireference.rst @@ -54,6 +54,7 @@ All 2-d operations require a **Workplane** object to be created. Workplane.threePointArc Workplane.sagittaArc Workplane.radiusArc + Workplane.tangentArcEndpoint Workplane.rotateAndCopy Workplane.mirrorY Workplane.mirrorX diff --git a/tests/test_cad_objects.py b/tests/test_cad_objects.py index c556678a..b43c59de 100644 --- a/tests/test_cad_objects.py +++ b/tests/test_cad_objects.py @@ -82,6 +82,24 @@ class TestCadObjects(BaseTest): (-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3 ) + def testEdgeWrapperMakeTangentArc(self): + tangent_arc = Edge.makeTangentArc( + Vector(1, 1), # starts at 1, 1 + Vector(0, 1), # tangent at start of arc is in the +y direction + Vector(2, 1), # arc curves 180 degrees and ends at 2, 1 + ) + self.assertTupleAlmostEquals((1, 1, 0), tangent_arc.startPoint().toTuple(), 3) + self.assertTupleAlmostEquals((2, 1, 0), tangent_arc.endPoint().toTuple(), 3) + self.assertTupleAlmostEquals( + (0, 1, 0), tangent_arc.tangentAt(locationParam=0).toTuple(), 3 + ) + self.assertTupleAlmostEquals( + (1, 0, 0), tangent_arc.tangentAt(locationParam=0.5).toTuple(), 3 + ) + self.assertTupleAlmostEquals( + (0, -1, 0), tangent_arc.tangentAt(locationParam=1).toTuple(), 3 + ) + def testFaceWrapperMakePlane(self): mplane = Face.makePlane(10, 10) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index f50f8982..c39ba951 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -3053,3 +3053,88 @@ class TestCadQuery(BaseTest): plate_4 = Workplane("XY").interpPlate(edge_wire, surface_points, thickness) self.assertTrue(plate_4.val().isValid()) self.assertAlmostEqual(plate_4.val().Volume(), 7.760559490, 3) + + def testTangentArcToPoint(self): + + # create a simple shape with tangents of straight edges and see if it has the correct area + s0 = ( + Workplane("XY") + .hLine(1) + .tangentArcEndpoint((1, 1), relative=False) + .hLineTo(0) + .tangentArcEndpoint((0, 0), relative=False) + .close() + .extrude(1) + ) + area0 = s0.faces(">Z").val().Area() + self.assertAlmostEqual(area0, (1 + math.pi * 0.5 ** 2), 4) + + # test relative coords + s1 = ( + Workplane("XY") + .hLine(1) + .tangentArcEndpoint((0, 1), relative=True) + .hLineTo(0) + .tangentArcEndpoint((0, -1), relative=True) + .close() + .extrude(1) + ) + self.assertTupleAlmostEquals( + s1.val().Center().toTuple(), s0.val().Center().toTuple(), 4 + ) + self.assertAlmostEqual(s1.val().Volume(), s0.val().Volume(), 4) + + # consecutive tangent arcs + s1 = ( + Workplane("XY") + .vLine(2) + .tangentArcEndpoint((1, 0)) + .tangentArcEndpoint((1, 0)) + .tangentArcEndpoint((1, 0)) + .vLine(-2) + .close() + .extrude(1) + ) + self.assertAlmostEqual( + s1.faces(">Z").val().Area(), 2 * 3 + 0.5 * math.pi * 0.5 ** 2, 4 + ) + + # tangentArc on the end of a spline + # spline will be a simple arc of a circle, then finished off with a + # tangentArcEndpoint + angles = [idx * 1.5 * math.pi / 10 for idx in range(10)] + pts = [(math.sin(a), math.cos(a)) for a in angles] + s2 = ( + Workplane("XY") + .spline(pts) + .tangentArcEndpoint((0, 1), relative=False) + .close() + .extrude(1) + ) + # volume should almost be pi, but not accurately because we need to + # start with a spline + self.assertAlmostEqual(s2.val().Volume(), math.pi, 1) + # assert local coords are mapped to global correctly + arc0 = ( + Workplane("XZ", origin=(1, 1, 1)).hLine(1).tangentArcEndpoint((1, 1)).val() + ) + self.assertTupleAlmostEquals(arc0.endPoint().toTuple(), (3, 1, 2), 4) + + def test_findFromEdge(self): + part = Workplane("XY", origin=(1, 1, 1)).hLine(1) + found_edge = part._findFromEdge(useLocalCoords=False) + self.assertTupleAlmostEquals(found_edge.startPoint().toTuple(), (1, 1, 1), 3) + self.assertTupleAlmostEquals(found_edge.Center().toTuple(), (1.5, 1, 1), 3) + self.assertTupleAlmostEquals(found_edge.endPoint().toTuple(), (2, 1, 1), 3) + found_edge = part._findFromEdge(useLocalCoords=True) + self.assertTupleAlmostEquals(found_edge.endPoint().toTuple(), (1, 0, 0), 3) + # check _findFromEdge can find a spline + pts = [(0, 0), (0, 1), (1, 2), (2, 4)] + spline0 = Workplane("XZ").spline(pts)._findFromEdge() + self.assertTupleAlmostEquals((2, 0, 4), spline0.endPoint().toTuple(), 3) + # check method fails if no edge is present + part2 = Workplane("XY").box(1, 1, 1) + with self.assertRaises(RuntimeError): + part2._findFromEdge() + with self.assertRaises(RuntimeError): + part2._findFromEdge(useLocalCoords=True) From d0f2576a60a5339237448039dd034badcd24324c Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Wed, 26 Feb 2020 15:28:54 +1030 Subject: [PATCH 65/70] doc/examples.rst: fix a typo --- doc/examples.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/examples.rst b/doc/examples.rst index a25975e4..cee51b36 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -806,7 +806,8 @@ A Parametric Enclosure #inner shell ishell = (oshell.faces(" Date: Sun, 15 Mar 2020 14:04:48 +0100 Subject: [PATCH 66/70] Ellipse (#265) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added ellipse * removed unused math imports * added method ellipseArc and adapted method ellipse to circle * introduced sense for ellipse building * adapted ellipse test cases * exclude vscode config folder * use gp_Ax2(p, zdir, xdir) for ellipse building * ran black against the changes * Fix docstring of makeEllipse Co-Authored-By: Adam Urbańczyk * Fix return value in docstring of makeEllips Co-Authored-By: Adam Urbańczyk * Formatting fix * Increase test coverage * Formatting fixes * Add test for makeEllipse * Test fix * Formatting + typo fix Co-authored-by: Bernhard Co-authored-by: Adam Urbańczyk --- .gitignore | 1 + cadquery/cq.py | 95 +++++++++++++++++ cadquery/occ_impl/shapes.py | 99 +++++++++++++++++- tests/__init__.py | 2 +- tests/test_cad_objects.py | 97 ++++++++++++++++- tests/test_cadquery.py | 200 ++++++++++++++++++++++++++++++++++++ 6 files changed, 490 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index e7df8cfc..8a19d8f0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist/* .idea/* cadquery.egg-info target/* +.vscode diff --git a/cadquery/cq.py b/cadquery/cq.py index 2332a5b5..0bc06150 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1570,6 +1570,65 @@ class Workplane(CQ): return self.spline(allPoints, includeCurrent=False, makeWire=True) + def ellipseArc( + self, + x_radius, + y_radius, + angle1=360, + angle2=360, + rotation_angle=0.0, + sense=1, + forConstruction=False, + startAtCurrent=True, + makeWire=False, + ): + """Draw an elliptical arc with x and y radiuses either with start point at current point or + or current point being the center of the arc + + :param x_radius: x radius of the ellipse (along the x-axis of plane the ellipse should lie in) + :param y_radius: y radius of the ellipse (along the y-axis of plane the ellipse should lie in) + :param angle1: start angle of arc + :param angle2: end angle of arc (angle2 == angle1 return closed ellipse = default) + :param rotation_angle: angle to rotate the created ellipse / arc + :param sense: clockwise (-1) or counter clockwise (1) + :param startAtCurrent: True: start point of arc is moved to current point; False: center of + arc is on current point + :param makeWire: convert the resulting arc edge to a wire + """ + + # Start building the ellipse with the current point as center + center = self._findFromPoint(useLocalCoords=False) + e = Edge.makeEllipse( + x_radius, + y_radius, + center, + self.plane.zDir, + self.plane.xDir, + angle1, + angle2, + sense == 1, + ) + + # Rotate if necessary + if rotation_angle != 0.0: + e = e.rotate(center, center.add(self.plane.zDir), rotation_angle) + + # Move the start point of the ellipse onto the last current point + if startAtCurrent: + startPoint = e.startPoint() + e = e.translate(center.sub(startPoint)) + + if makeWire: + rv = Wire.assembleEdges([e]) + if not forConstruction: + self._addPendingWire(rv) + else: + rv = e + if not forConstruction: + self._addPendingEdge(e) + + return self.newObject([rv]) + def threePointArc(self, point1, point2, forConstruction=False): """ Draw an arc from the current point, through point1, and ending at point2 @@ -2023,6 +2082,42 @@ class Workplane(CQ): return self.eachpoint(makeCircleWire, useLocalCoordinates=True) + # ellipse from current point + def ellipse(self, x_radius, y_radius, rotation_angle=0.0, forConstruction=False): + """ + Make an ellipse for each item on the stack. + :param x_radius: x radius of the ellipse (x-axis of plane the ellipse should lie in) + :type x_radius: float > 0 + :param y_radius: y radius of the ellipse (y-axis of plane the ellipse should lie in) + :type y_radius: float > 0 + :param rotation_angle: angle to rotate the ellipse (0 = no rotation = default) + :type rotation_angle: float + :param forConstruction: should the new wires be reference geometry only? + :type forConstruction: true if the wires are for reference, false if they are creating + part geometry + :return: a new CQ object with the created wires on the stack + + *NOTE* Due to a bug in opencascade (https://tracker.dev.opencascade.org/view.php?id=31290) + the center of mass (equals center for next shape) is shifted. To create concentric ellipses + use Workplane("XY") + .center(10, 20).ellipse(100,10) + .center(0, 0).ellipse(50, 5) + """ + + def makeEllipseWire(obj): + elip = Wire.makeEllipse( + x_radius, + y_radius, + obj, + Vector(0, 0, 1), + Vector(1, 0, 0), + rotation_angle=rotation_angle, + ) + elip.forConstruction = forConstruction + return elip + + return self.eachpoint(makeEllipseWire, useLocalCoordinates=True) + def polygon(self, nSides, diameter, forConstruction=False): """ Creates a polygon inscribed in a circle of the specified diameter for each point on diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 72c4e7b9..5a425b17 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -11,6 +11,7 @@ from OCC.Core.gp import ( gp_Ax3, gp_Dir, gp_Circ, + gp_Elips, gp_Trsf, gp_Pln, gp_GTrsf, @@ -76,7 +77,7 @@ from OCC.Core.TopoDS import ( from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Builder -from OCC.Core.GC import GC_MakeArcOfCircle # geometry construction +from OCC.Core.GC import GC_MakeArcOfCircle, GC_MakeArcOfEllipse # geometry construction from OCC.Core.GCE2d import GCE2d_MakeSegment from OCC.Core.GeomAPI import GeomAPI_Interpolate, GeomAPI_ProjectPointOnSurf @@ -751,6 +752,62 @@ class Edge(Shape, Mixin1D): ).Value() return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) + @classmethod + def makeEllipse( + cls, + x_radius, + y_radius, + pnt=Vector(0, 0, 0), + dir=Vector(0, 0, 1), + xdir=Vector(1, 0, 0), + angle1=360.0, + angle2=360.0, + sense=1, + ): + """ + Makes an Ellipse centered at the provided point, having normal in the provided direction + :param cls: + :param x_radius: x radius of the ellipse (along the x-axis of plane the ellipse should lie in) + :param y_radius: y radius of the ellipse (along the y-axis of plane the ellipse should lie in) + :param pnt: vector representing the center of the ellipse + :param dir: vector representing the direction of the plane the ellipse should lie in + :param angle1: start angle of arc + :param angle2: end angle of arc (angle2 == angle1 return closed ellipse = default) + :param sense: clockwise (-1) or counter clockwise (1) + :return: an Edge + """ + + pnt = Vector(pnt).toPnt() + dir = Vector(dir).toDir() + xdir = Vector(xdir).toDir() + + ax1 = gp_Ax1(pnt, dir) + ax2 = gp_Ax2(pnt, dir, xdir) + + if y_radius > x_radius: + # swap x and y radius and rotate by 90° afterwards to create an ellipse with x_radius < y_radius + correction_angle = 90.0 * DEG2RAD + ellipse_gp = gp_Elips(ax2, y_radius, x_radius).Rotated( + ax1, correction_angle + ) + else: + correction_angle = 0.0 + ellipse_gp = gp_Elips(ax2, x_radius, y_radius) + + if angle1 == angle2: # full ellipse case + ellipse = cls(BRepBuilderAPI_MakeEdge(ellipse_gp).Edge()) + else: # arc case + # take correction_angle into account + ellipse_geom = GC_MakeArcOfEllipse( + ellipse_gp, + angle1 * DEG2RAD - correction_angle, + angle2 * DEG2RAD - correction_angle, + sense == 1, + ).Value() + ellipse = cls(BRepBuilderAPI_MakeEdge(ellipse_geom).Edge()) + + return ellipse + @classmethod def makeSpline(cls, listOfVector, tangents=None, periodic=False, tol=1e-6): """ @@ -869,6 +926,46 @@ class Wire(Shape, Mixin1D): w = cls.assembleEdges([circle_edge]) return w + @classmethod + def makeEllipse( + cls, + x_radius, + y_radius, + center, + normal, + xDir, + angle1=360.0, + angle2=360.0, + rotation_angle=0.0, + closed=True, + ): + """ + Makes an Ellipse centered at the provided point, having normal in the provided direction + :param x_radius: floating point major radius of the ellipse (x-axis), must be > 0 + :param y_radius: floating point minor radius of the ellipse (y-axis), must be > 0 + :param center: vector representing the center of the circle + :param normal: vector representing the direction of the plane the circle should lie in + :param angle1: start angle of arc + :param angle2: end angle of arc + :param rotation_angle: angle to rotate the created ellipse / arc + :return: Wire + """ + + ellipse_edge = Edge.makeEllipse( + x_radius, y_radius, center, normal, xDir, angle1, angle2 + ) + + if angle1 != angle2 and closed: + line = Edge.makeLine(ellipse_edge.endPoint(), ellipse_edge.startPoint()) + w = cls.assembleEdges([ellipse_edge, line]) + else: + w = cls.assembleEdges([ellipse_edge]) + + if rotation_angle != 0.0: + w = w.rotate(center, center + normal, rotation_angle) + + return w + @classmethod def makePolygon(cls, listOfVertices, forConstruction=False): # convert list of tuples into Vectors. diff --git a/tests/__init__.py b/tests/__init__.py index 42ac9093..254c400c 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,5 +1,5 @@ from cadquery import * -from OCC.gp import gp_Vec +from OCC.Core.gp import gp_Vec import unittest import sys import os diff --git a/tests/test_cad_objects.py b/tests/test_cad_objects.py index c556678a..53cd3c7c 100644 --- a/tests/test_cad_objects.py +++ b/tests/test_cad_objects.py @@ -1,18 +1,21 @@ # system modules +import math import sys import unittest from tests import BaseTest -from OCC.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_DZ, gp_XYZ +from OCC.gp import gp_Vec, gp_Pnt, gp_Ax2, gp_Circ, gp_Elips, gp_DZ, gp_XYZ from OCC.BRepBuilderAPI import ( BRepBuilderAPI_MakeVertex, BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace, ) -from OCC.GC import GC_MakeCircle +from OCC.Core.GC import GC_MakeCircle from cadquery import * +DEG2RAD = 2 * math.pi / 360 + class TestCadObjects(BaseTest): def _make_circle(self): @@ -20,6 +23,11 @@ class TestCadObjects(BaseTest): circle = gp_Circ(gp_Ax2(gp_Pnt(1, 2, 3), gp_DZ()), 2.0) return Shape.cast(BRepBuilderAPI_MakeEdge(circle).Edge()) + def _make_ellipse(self): + + ellipse = gp_Elips(gp_Ax2(gp_Pnt(1, 2, 3), gp_DZ()), 4.0, 2.0) + return Shape.cast(BRepBuilderAPI_MakeEdge(ellipse).Edge()) + def testVectorConstructors(self): v1 = Vector(1, 2, 3) v2 = Vector((1, 2, 3)) @@ -69,6 +77,13 @@ class TestCadObjects(BaseTest): self.assertTupleAlmostEquals((1.0, 2.0, 3.0), e.Center().toTuple(), 3) + def testEdgeWrapperEllipseCenter(self): + e = self._make_ellipse() + w = Wire.assembleEdges([e]) + self.assertTupleAlmostEquals( + (1.0, 2.0, 3.0), Face.makeFromWires(w).Center().toTuple(), 3 + ) + def testEdgeWrapperMakeCircle(self): halfCircleEdge = Edge.makeCircle( radius=10, pnt=(0, 0, 0), dir=(0, 0, 1), angle1=0, angle2=180 @@ -82,6 +97,84 @@ class TestCadObjects(BaseTest): (-10.0, 0.0, 0.0), halfCircleEdge.endPoint().toTuple(), 3 ) + def testEdgeWrapperMakeEllipse1(self): + # Check x_radius > y_radius + x_radius, y_radius = 20, 10 + angle1, angle2 = -75.0, 90.0 + arcEllipseEdge = Edge.makeEllipse( + x_radius=x_radius, + y_radius=y_radius, + pnt=(0, 0, 0), + dir=(0, 0, 1), + angle1=angle1, + angle2=angle2, + ) + + start = ( + x_radius * math.cos(angle1 * DEG2RAD), + y_radius * math.sin(angle1 * DEG2RAD), + 0.0, + ) + end = ( + x_radius * math.cos(angle2 * DEG2RAD), + y_radius * math.sin(angle2 * DEG2RAD), + 0.0, + ) + self.assertTupleAlmostEquals(start, arcEllipseEdge.startPoint().toTuple(), 3) + self.assertTupleAlmostEquals(end, arcEllipseEdge.endPoint().toTuple(), 3) + + def testEdgeWrapperMakeEllipse2(self): + # Check x_radius < y_radius + x_radius, y_radius = 10, 20 + angle1, angle2 = 0.0, 45.0 + arcEllipseEdge = Edge.makeEllipse( + x_radius=x_radius, + y_radius=y_radius, + pnt=(0, 0, 0), + dir=(0, 0, 1), + angle1=angle1, + angle2=angle2, + ) + + start = ( + x_radius * math.cos(angle1 * DEG2RAD), + y_radius * math.sin(angle1 * DEG2RAD), + 0.0, + ) + end = ( + x_radius * math.cos(angle2 * DEG2RAD), + y_radius * math.sin(angle2 * DEG2RAD), + 0.0, + ) + self.assertTupleAlmostEquals(start, arcEllipseEdge.startPoint().toTuple(), 3) + self.assertTupleAlmostEquals(end, arcEllipseEdge.endPoint().toTuple(), 3) + + def testEdgeWrapperMakeCircleWithEllipse(self): + # Check x_radius == y_radius + x_radius, y_radius = 20, 20 + angle1, angle2 = 15.0, 60.0 + arcEllipseEdge = Edge.makeEllipse( + x_radius=x_radius, + y_radius=y_radius, + pnt=(0, 0, 0), + dir=(0, 0, 1), + angle1=angle1, + angle2=angle2, + ) + + start = ( + x_radius * math.cos(angle1 * DEG2RAD), + y_radius * math.sin(angle1 * DEG2RAD), + 0.0, + ) + end = ( + x_radius * math.cos(angle2 * DEG2RAD), + y_radius * math.sin(angle2 * DEG2RAD), + 0.0, + ) + self.assertTupleAlmostEquals(start, arcEllipseEdge.startPoint().toTuple(), 3) + self.assertTupleAlmostEquals(end, arcEllipseEdge.endPoint().toTuple(), 3) + def testFaceWrapperMakePlane(self): mplane = Face.makePlane(10, 10) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index b07f9121..e41e3f73 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -552,6 +552,199 @@ class TestCadQuery(BaseTest): path2 = Workplane("XY", (0, 0, 10)).spline(pts, tangents=tangents) self.assertAlmostEqual(path2.val().tangentAt(0).z, 0) + def testRotatedEllipse(self): + def rotatePoint(x, y, alpha): + # rotation matrix + a = alpha * DEG2RAD + r = ((math.cos(a), math.sin(a)), (-math.sin(a), math.cos(a))) + return ((x * r[0][0] + y * r[1][0]), (x * r[0][1] + y * r[1][1])) + + def ellipsePoints(r1, r2, a): + return (r1 * math.cos(a * DEG2RAD), r2 * math.sin(a * DEG2RAD)) + + DEG2RAD = math.pi / 180.0 + p0 = (10, 20) + a1, a2 = 30, -60 + r1, r2 = 20, 10 + ra = 25 + + sx_rot, sy_rot = rotatePoint(*ellipsePoints(r1, r2, a1), ra) + ex_rot, ey_rot = rotatePoint(*ellipsePoints(r1, r2, a2), ra) + + # startAtCurrent=False, sense = 1 + ellipseArc1 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra + ) + ) + start = ellipseArc1.vertices().objects[0] + end = ellipseArc1.vertices().objects[1] + + self.assertTupleAlmostEquals( + (start.X, start.Y), (p0[0] + sx_rot, p0[1] + sy_rot), 3 + ) + self.assertTupleAlmostEquals( + (end.X, end.Y), (p0[0] + ex_rot, p0[1] + ey_rot), 3 + ) + + # startAtCurrent=True, sense = 1 + ellipseArc2 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, r2, startAtCurrent=True, angle1=a1, angle2=a2, rotation_angle=ra + ) + ) + start = ellipseArc2.vertices().objects[0] + end = ellipseArc2.vertices().objects[1] + + self.assertTupleAlmostEquals( + (start.X, start.Y), (p0[0] + sx_rot - sx_rot, p0[1] + sy_rot - sy_rot), 3 + ) + self.assertTupleAlmostEquals( + (end.X, end.Y), (p0[0] + ex_rot - sx_rot, p0[1] + ey_rot - sy_rot), 3 + ) + + # startAtCurrent=False, sense = -1 + ellipseArc3 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, + r2, + startAtCurrent=False, + angle1=a1, + angle2=a2, + rotation_angle=ra, + sense=-1, + ) + ) + start = ellipseArc3.vertices().objects[0] + end = ellipseArc3.vertices().objects[1] + + # swap start and end points for coparison due to different sense + self.assertTupleAlmostEquals( + (start.X, start.Y), (p0[0] + ex_rot, p0[1] + ey_rot), 3 + ) + self.assertTupleAlmostEquals( + (end.X, end.Y), (p0[0] + sx_rot, p0[1] + sy_rot), 3 + ) + + # startAtCurrent=True, sense = -1 + ellipseArc4 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, + r2, + startAtCurrent=True, + angle1=a1, + angle2=a2, + rotation_angle=ra, + sense=-1, + makeWire=True, + ) + ) + + self.assertEqual(len(ellipseArc4.ctx.pendingWires), 1) + + start = ellipseArc4.vertices().objects[0] + end = ellipseArc4.vertices().objects[1] + + # swap start and end points for coparison due to different sense + self.assertTupleAlmostEquals( + (start.X, start.Y), (p0[0] + ex_rot - ex_rot, p0[1] + ey_rot - ey_rot), 3 + ) + self.assertTupleAlmostEquals( + (end.X, end.Y), (p0[0] + sx_rot - ex_rot, p0[1] + sy_rot - ey_rot), 3 + ) + + def testEllipseArcsClockwise(self): + ellipseArc = ( + Workplane("XY") + .moveTo(10, 15) + .ellipseArc(5, 4, -10, 190, 45, sense=-1, startAtCurrent=False) + ) + sp = ellipseArc.val().startPoint() + ep = ellipseArc.val().endPoint() + self.assertTupleAlmostEquals( + (sp.x, sp.y), (7.009330014275797, 11.027027582524015), 3 + ) + self.assertTupleAlmostEquals( + (ep.x, ep.y), (13.972972417475985, 17.990669985724203), 3 + ) + + ellipseArc = ( + ellipseArc.ellipseArc(5, 4, -10, 190, 315, sense=-1) + .ellipseArc(5, 4, -10, 190, 225, sense=-1) + .ellipseArc(5, 4, -10, 190, 135, sense=-1) + ) + ep = ellipseArc.val().endPoint() + self.assertTupleAlmostEquals((sp.x, sp.y), (ep.x, ep.y), 3) + + def testEllipseArcsCounterClockwise(self): + ellipseArc = ( + Workplane("XY") + .moveTo(10, 15) + .ellipseArc(5, 4, -10, 190, 45, startAtCurrent=False) + ) + sp = ellipseArc.val().startPoint() + ep = ellipseArc.val().endPoint() + self.assertTupleAlmostEquals( + (sp.x, sp.y), (13.972972417475985, 17.990669985724203), 3 + ) + self.assertTupleAlmostEquals( + (ep.x, ep.y), (7.009330014275797, 11.027027582524015), 3 + ) + + ellipseArc = ( + ellipseArc.ellipseArc(5, 4, -10, 190, 135) + .ellipseArc(5, 4, -10, 190, 225) + .ellipseArc(5, 4, -10, 190, 315) + ) + ep = ellipseArc.val().endPoint() + self.assertTupleAlmostEquals((sp.x, sp.y), (ep.x, ep.y), 3) + + def testEllipseCenterAndMoveTo(self): + # Whether we start from a center() call or a moveTo call, it should be the same ellipse Arc + p0 = (10, 20) + a1, a2 = 30, -60 + r1, r2 = 20, 10 + ra = 25 + + ellipseArc1 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra + ) + ) + sp1 = ellipseArc1.val().startPoint() + ep1 = ellipseArc1.val().endPoint() + + ellipseArc2 = ( + Workplane("XY") + .moveTo(*p0) + .ellipseArc( + r1, r2, startAtCurrent=False, angle1=a1, angle2=a2, rotation_angle=ra + ) + ) + sp2 = ellipseArc2.val().startPoint() + ep2 = ellipseArc2.val().endPoint() + + self.assertTupleAlmostEquals(sp1.toTuple(), sp2.toTuple(), 3) + self.assertTupleAlmostEquals(ep1.toTuple(), ep2.toTuple(), 3) + + def testMakeEllipse(self): + el = Wire.makeEllipse( + 1, 2, Vector(0, 0, 0), Vector(0, 0, 1), Vector(1, 0, 0), 0, 90, 45, True, + ) + + self.assertTrue(el.IsClosed()) + self.assertTrue(el.isValid()) + def testSweep(self): """ Tests the operation of sweeping a wire(s) along a path @@ -809,6 +1002,13 @@ class TestCadQuery(BaseTest): self.saveModel(s) self.assertEqual(14, s.faces().size()) + def testConcentricEllipses(self): + concentricEllipses = ( + Workplane("XY").center(10, 20).ellipse(100, 10).center(0, 0).ellipse(50, 5) + ) + v = concentricEllipses.vertices().objects[0] + self.assertTupleAlmostEquals((v.X, v.Y), (10 + 50, 20), 3) + def testLegoBrick(self): # test making a simple lego brick # which of the below From ed21b9fc9c54fe7a53e243dfc7c3e78eb2fbe416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Thu, 19 Mar 2020 08:43:37 +0100 Subject: [PATCH 67/70] Use wrapped i.s.o. _wrapped --- cadquery/occ_impl/shapes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cadquery/occ_impl/shapes.py b/cadquery/occ_impl/shapes.py index 02ed4190..7f775a21 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -801,7 +801,7 @@ class Edge(Shape, Mixin1D): :param v3: end vector :return: an edge """ - circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2._wrapped, v3.toPnt()).Value() + circle_geom = GC_MakeArcOfCircle(v1.toPnt(), v2.wrapped, v3.toPnt()).Value() return cls(BRepBuilderAPI_MakeEdge(circle_geom).Edge()) From 228ca55786fa4be947280807ed02bd28aab0f2f2 Mon Sep 17 00:00:00 2001 From: Marcus Boyd Date: Thu, 19 Mar 2020 21:21:01 +1030 Subject: [PATCH 68/70] Renamed tangentArcEndpoint to tangentArc --- cadquery/cq.py | 8 ++++---- doc/apireference.rst | 2 +- tests/test_cadquery.py | 22 ++++++++++------------ 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/cadquery/cq.py b/cadquery/cq.py index c7bd321a..21249ded 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1163,7 +1163,7 @@ class Workplane(CQ): def _findFromEdge(self, useLocalCoords=False): """ Finds the previous edge for an operation that needs it, similar to - method _findFromPoint. Examples include tangentArcEndpoint. + method _findFromPoint. Examples include tangentArcPoint. :param useLocalCoords: selects whether the point is returned in local coordinates or global coordinates. @@ -1744,12 +1744,12 @@ class Workplane(CQ): else: return self.sagittaArc(endPoint, -sag, forConstruction) - def tangentArcEndpoint(self, endpoint, forConstruction=False, relative=True): + def tangentArcPoint(self, endpoint, forConstruction=False, relative=True): """ Draw an arc as a tangent from the end of the current edge to endpoint. :param endpoint: point for the arc to end at - :type endpoint: 2-tuple or Vector + :type endpoint: 2-tuple, 3-tuple or Vector :param relative: True if endpoint is specified relative to the current point, False if endpoint is in workplane coordinates :type relative: Bool :return: a Workplane object with an arc on the stack @@ -2246,7 +2246,7 @@ class Workplane(CQ): :return: a CQ object with a completed wire on the stack, if possible. After 2-d drafting with methods such as lineTo, threePointArc, - tangentArcEndpoint and polyline, it is necessary to convert the edges + tangentArcPoint and polyline, it is necessary to convert the edges produced by these into one or more wires. When a set of edges is closed, cadQuery assumes it is safe to build diff --git a/doc/apireference.rst b/doc/apireference.rst index 84152d48..7cdd2928 100644 --- a/doc/apireference.rst +++ b/doc/apireference.rst @@ -54,7 +54,7 @@ All 2-d operations require a **Workplane** object to be created. Workplane.threePointArc Workplane.sagittaArc Workplane.radiusArc - Workplane.tangentArcEndpoint + Workplane.tangentArcPoint Workplane.rotateAndCopy Workplane.mirrorY Workplane.mirrorX diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index c96975bf..2a25a3bc 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -3267,9 +3267,9 @@ class TestCadQuery(BaseTest): s0 = ( Workplane("XY") .hLine(1) - .tangentArcEndpoint((1, 1), relative=False) + .tangentArcPoint((1, 1), relative=False) .hLineTo(0) - .tangentArcEndpoint((0, 0), relative=False) + .tangentArcPoint((0, 0), relative=False) .close() .extrude(1) ) @@ -3280,9 +3280,9 @@ class TestCadQuery(BaseTest): s1 = ( Workplane("XY") .hLine(1) - .tangentArcEndpoint((0, 1), relative=True) + .tangentArcPoint((0, 1), relative=True) .hLineTo(0) - .tangentArcEndpoint((0, -1), relative=True) + .tangentArcPoint((0, -1), relative=True) .close() .extrude(1) ) @@ -3295,9 +3295,9 @@ class TestCadQuery(BaseTest): s1 = ( Workplane("XY") .vLine(2) - .tangentArcEndpoint((1, 0)) - .tangentArcEndpoint((1, 0)) - .tangentArcEndpoint((1, 0)) + .tangentArcPoint((1, 0)) + .tangentArcPoint((1, 0)) + .tangentArcPoint((1, 0)) .vLine(-2) .close() .extrude(1) @@ -3308,13 +3308,13 @@ class TestCadQuery(BaseTest): # tangentArc on the end of a spline # spline will be a simple arc of a circle, then finished off with a - # tangentArcEndpoint + # tangentArcPoint angles = [idx * 1.5 * math.pi / 10 for idx in range(10)] pts = [(math.sin(a), math.cos(a)) for a in angles] s2 = ( Workplane("XY") .spline(pts) - .tangentArcEndpoint((0, 1), relative=False) + .tangentArcPoint((0, 1), relative=False) .close() .extrude(1) ) @@ -3322,9 +3322,7 @@ class TestCadQuery(BaseTest): # start with a spline self.assertAlmostEqual(s2.val().Volume(), math.pi, 1) # assert local coords are mapped to global correctly - arc0 = ( - Workplane("XZ", origin=(1, 1, 1)).hLine(1).tangentArcEndpoint((1, 1)).val() - ) + arc0 = Workplane("XZ", origin=(1, 1, 1)).hLine(1).tangentArcPoint((1, 1)).val() self.assertTupleAlmostEquals(arc0.endPoint().toTuple(), (3, 1, 2), 4) def test_findFromEdge(self): From 71080aff595e3063c411c16d0e739602d72c6dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Thu, 19 Mar 2020 18:36:44 +0100 Subject: [PATCH 69/70] test tangentArcPoint with 3-tuple argument --- tests/test_cadquery.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 2a25a3bc..68524fba 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -3325,6 +3325,16 @@ class TestCadQuery(BaseTest): arc0 = Workplane("XZ", origin=(1, 1, 1)).hLine(1).tangentArcPoint((1, 1)).val() self.assertTupleAlmostEquals(arc0.endPoint().toTuple(), (3, 1, 2), 4) + # tangentArcPoint with 3-tuple argument + w0 = ( + Workplane("XY") + .lineTo(1, 1) + .tangentArcPoint((1, 1, 1)) + .wire() + ) + zmax = w0.val().BoundingBox().zmax + self.assertAlmostEqual(zmax, 1, 1) + def test_findFromEdge(self): part = Workplane("XY", origin=(1, 1, 1)).hLine(1) found_edge = part._findFromEdge(useLocalCoords=False) From e655098834ef3be14ce1eefc778c12b2a9843ef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Urba=C5=84czyk?= Date: Thu, 19 Mar 2020 18:51:15 +0100 Subject: [PATCH 70/70] Improve formatting --- tests/test_cadquery.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_cadquery.py b/tests/test_cadquery.py index 68524fba..9ef4ffa3 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -3326,12 +3326,7 @@ class TestCadQuery(BaseTest): self.assertTupleAlmostEquals(arc0.endPoint().toTuple(), (3, 1, 2), 4) # tangentArcPoint with 3-tuple argument - w0 = ( - Workplane("XY") - .lineTo(1, 1) - .tangentArcPoint((1, 1, 1)) - .wire() - ) + w0 = Workplane("XY").lineTo(1, 1).tangentArcPoint((1, 1, 1)).wire() zmax = w0.val().BoundingBox().zmax self.assertAlmostEqual(zmax, 1, 1)