Centered option (#617)

* Added 2d centered option to rect

* Added single bool center option to box

* Add single bool center option to sphere

* Added single bool center option to wedge

* Added 2d centered option to rarray

* Black fix

* Centered docstrings

Change all docstrings for centered options to be consistent and more
clear

* Docstring fixes

* Fix type hint on wedge method
This commit is contained in:
Marcus Boyd
2021-02-11 03:32:43 +10:30
committed by GitHub
parent 184a985066
commit b6beba7cb1
3 changed files with 248 additions and 98 deletions

View File

@ -1320,7 +1320,7 @@ class Workplane(object):
ySpacing: float,
xCount: int,
yCount: int,
center: bool = True,
center: Union[bool, Tuple[bool, bool]] = True,
) -> "Workplane":
"""
Creates an array of points and pushes them onto the stack.
@ -1331,26 +1331,30 @@ class Workplane(object):
:param ySpacing: spacing between points in the y direction ( must be > 0)
:param xCount: number of points ( > 0 )
:param yCount: number of points ( > 0 )
:param center: if true, the array will be centered at the center of the workplane. if
false, the lower left corner will be at the center of the work plane
:param center: If True, the array will be centered around the workplane center.
If False, the lower corner will be on the reference point and the array will
extend in the positive x and y directions. Can also use a 2-tuple to specify
centering along each axis.
"""
if xSpacing <= 0 or ySpacing <= 0 or xCount < 1 or yCount < 1:
raise ValueError("Spacing and count must be > 0 ")
if isinstance(center, bool):
center = (center, center)
lpoints = [] # coordinates relative to bottom left point
for x in range(xCount):
for y in range(yCount):
lpoints.append((xSpacing * x, ySpacing * y))
lpoints.append(Vector(xSpacing * x, ySpacing * y))
# shift points down and left relative to origin if requested
if center:
xc = xSpacing * (xCount - 1) * 0.5
yc = ySpacing * (yCount - 1) * 0.5
cpoints = []
for p in lpoints:
cpoints.append((p[0] - xc, p[1] - yc))
lpoints = list(cpoints)
offset = Vector()
if center[0]:
offset += Vector(-xSpacing * (xCount - 1) * 0.5, 0)
if center[1]:
offset += Vector(0, -ySpacing * (yCount - 1) * 0.5)
lpoints = [x + offset for x in lpoints]
return self.pushPoints(lpoints)
@ -2214,7 +2218,7 @@ class Workplane(object):
self,
xLen: float,
yLen: float,
centered: bool = True,
centered: Union[bool, Tuple[bool, bool]] = True,
forConstruction: bool = False,
) -> "Workplane":
"""
@ -2224,8 +2228,10 @@ class Workplane(object):
:type xLen: float > 0
:param yLen: length in yDirection ( in workplane coordinates )
:type yLen: float > 0
:param boolean centered: true if the rect is centered on the reference point, false if the
lower-left is on the reference point
:param centered: If True, the rectangle will be centered around the reference
point. If False, the corner of the rectangle will be on the reference point and
it will extend in the positive x and y directions. Can also use a 2-tuple to
specify centering along each axis.
: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
@ -2239,22 +2245,31 @@ class Workplane(object):
Creates 4 circles at the corners of a square centered on the origin.
Future Enhancements:
better way to handle forConstruction
project points not in the workplane plane onto the workplane plane
* project points not in the workplane plane onto the workplane plane
"""
if centered:
p1 = Vector(xLen / -2.0, yLen / -2.0, 0)
p2 = Vector(xLen / 2.0, yLen / -2.0, 0)
p3 = Vector(xLen / 2.0, yLen / 2.0, 0)
p4 = Vector(xLen / -2.0, yLen / 2.0, 0)
else:
p1 = Vector()
p2 = Vector(xLen, 0, 0)
p3 = Vector(xLen, yLen, 0)
p4 = Vector(0, yLen, 0)
if isinstance(centered, bool):
centered = (centered, centered)
w = Wire.makePolygon([p1, p2, p3, p4, p1], forConstruction)
offset = Vector()
if not centered[0]:
offset += Vector(xLen / 2, 0, 0)
if not centered[1]:
offset += Vector(0, yLen / 2, 0)
points = [
Vector(xLen / -2.0, yLen / -2.0, 0),
Vector(xLen / 2.0, yLen / -2.0, 0),
Vector(xLen / 2.0, yLen / 2.0, 0),
Vector(xLen / -2.0, yLen / 2.0, 0),
]
points = [x + offset for x in points]
# close the wire
points.append(points[0])
w = Wire.makePolygon(points, forConstruction)
return self.eachpoint(lambda loc: w.moved(loc), True)
@ -3342,7 +3357,7 @@ class Workplane(object):
length: float,
width: float,
height: float,
centered: Tuple[bool, bool, bool] = (True, True, True),
centered: Union[bool, Tuple[bool, bool, bool]] = True,
combine: bool = True,
clean: bool = True,
) -> "Workplane":
@ -3355,48 +3370,55 @@ class Workplane(object):
:type width: float > 0
:param height: box size in Z direction
:type height: float > 0
:param centered: should the box be centered, or should reference point be at the lower
bound of the range?
:param centered: If True, the box will be centered around the reference point.
If False, the corner of the box will be on the reference point and it will
extend in the positive x, y and z directions. Can also use a 3-tuple to
specify centering along each axis.
: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 clean: call :py:meth:`clean` afterwards to have a clean shape
Centered is a tuple that describes whether the box should be centered on the x,y, and
z axes. If true, the box is centered on the respective axis relative to the workplane
origin, if false, the workplane center will represent the lower bound of the resulting box
one box is created for each item on the current stack. If no items are on the stack, one box
One box is created for each item on the current stack. If no items are on the stack, one box
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 boxes produced
fused onto it otherwise, the result is the combination of all the produced boxes
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 boxes produced fused onto it otherwise, the
result is the combination of all the produced boxes.
if combine is false, the result will be a list of the boxes produced
If combine is false, the result will be a list of the boxes produced.
Most often boxes form the basis for a part::
#make a single box with lower left corner at origin
s = Workplane().box(1,2,3,centered=(False,False,False)
# make a single box with lower left corner at origin
s = Workplane().box(1, 2, 3, centered=False)
But sometimes it is useful to create an array of them:
But sometimes it is useful to create an array of them::
#create 4 small square bumps on a larger base plate:
s = Workplane().box(4,4,0.5).faces(">Z").workplane()\
.rect(3,3,forConstruction=True).vertices().box(0.25,0.25,0.25,combine=True)
# create 4 small square bumps on a larger base plate:
s = (
Workplane().
box(4, 4, 0.5).
faces(">Z").
workplane().
rect(3, 3, forConstruction=True)
.vertices()
.box(0.25, 0.25, 0.25, combine=True)
)
"""
(xp, yp, zp) = (0.0, 0.0, 0.0)
if centered[0]:
xp -= length / 2.0
if centered[1]:
yp -= width / 2.0
if centered[2]:
zp -= height / 2.0
if isinstance(centered, bool):
centered = (centered, centered, centered)
box = Solid.makeBox(length, width, height, Vector(xp, yp, zp))
offset = Vector()
if centered[0]:
offset += Vector(-length / 2, 0, 0)
if centered[1]:
offset += Vector(0, -width / 2, 0)
if centered[2]:
offset += Vector(0, 0, -height / 2)
box = Solid.makeBox(length, width, height, offset)
boxes = self.eachpoint(lambda loc: box.moved(loc), True)
@ -3414,7 +3436,7 @@ class Workplane(object):
angle1: float = -90,
angle2: float = 90,
angle3: float = 360,
centered: Tuple[bool, bool, bool] = (True, True, True),
centered: Union[bool, Tuple[bool, bool, bool]] = True,
combine: bool = True,
clean: bool = True,
) -> "Workplane":
@ -3431,44 +3453,42 @@ class Workplane(object):
:type angle2: float > 0
:param angle3: The third angle to sweep the sphere arc through
:type angle3: float > 0
:param centered: A three-tuple of booleans that determines whether the sphere is centered
on each axis origin
:param centered: If True, the sphere will be centered around the reference point. If False,
the corner of a bounding box around the sphere will be on the reference point and it
will extend in the positive x, y and z directions. Can also use a 3-tuple to specify
centering along each axis.
:param combine: Whether the results should be combined with other solids on the stack
(and each other)
:type combine: true to combine shapes, false otherwise
:param clean: call :py:meth:`clean` afterwards to have a clean shape
:return: A sphere object for each point on the stack
Centered is a tuple that describes whether the sphere should be centered on the x,y, and
z axes. If true, the sphere is centered on the respective axis relative to the workplane
origin, if false, the workplane center will represent the lower bound of the resulting
sphere.
One sphere is created for each item on the current stack. If no items are on the stack, one
box 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 spheres produced
fused onto it otherwise, the result is the combination of all the produced boxes
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 spheres produced fused onto it otherwise,
the result is the combination of all the produced spheres.
If combine is false, the result will be a list of the spheres produced
If combine is false, the result will be a list of the spheres produced.
"""
# Convert the direction tuple to a vector, if needed
if isinstance(direct, tuple):
direct = Vector(direct)
(xp, yp, zp) = (0.0, 0.0, 0.0)
if isinstance(centered, bool):
centered = (centered, centered, centered)
offset = Vector()
if not centered[0]:
xp += radius
offset += Vector(radius, 0, 0)
if not centered[1]:
yp += radius
offset += Vector(0, radius, 0)
if not centered[2]:
zp += radius
offset += Vector(0, 0, radius)
s = Solid.makeSphere(radius, Vector(xp, yp, zp), direct, angle1, angle2, angle3)
s = Solid.makeSphere(radius, offset, direct, angle1, angle2, angle3)
# We want a sphere for each point on the workplane
spheres = self.eachpoint(lambda loc: s.moved(loc), True)
@ -3490,7 +3510,7 @@ class Workplane(object):
zmax: float,
pnt: VectorLike = Vector(0, 0, 0),
dir: VectorLike = Vector(0, 0, 1),
centered: Tuple[bool, bool, bool] = (True, True, True),
centered: Union[bool, Tuple[bool, bool, bool]] = True,
combine: bool = True,
clean: bool = True,
) -> "Workplane":
@ -3499,24 +3519,28 @@ class Workplane(object):
: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 zmin: The minimum Z location
:param xmax: The maximum X location
:param zmax: The maximum Z location
: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 centered: If True, the wedge will be centered around the reference point.
If False, the corner of the wedge will be on the reference point and it will
extend in the positive x, y and z directions. Can also use a 3-tuple to
specify centering along each axis.
: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
: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 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
If combine is False, the result will be a list of the wedges produced.
"""
# Convert the point tuple to a vector, if needed
@ -3527,18 +3551,18 @@ class Workplane(object):
if isinstance(dir, tuple):
dir = Vector(dir)
(xp, yp, zp) = (0.0, 0.0, 0.0)
if isinstance(centered, bool):
centered = (centered, centered, centered)
offset = Vector()
if centered[0]:
xp -= dx / 2.0
offset += Vector(-dx / 2, 0, 0)
if centered[1]:
yp -= dy / 2.0
offset += Vector(0, -dy / 2, 0)
if centered[2]:
zp -= dz / 2.0
offset += Vector(0, 0, -dz / 2)
w = Solid.makeWedge(dx, dy, dz, xmin, zmin, xmax, zmax, Vector(xp, yp, zp), dir)
w = Solid.makeWedge(dx, dy, dz, xmin, zmin, xmax, zmax, offset, dir)
# We want a wedge for each point on the workplane
wedges = self.eachpoint(lambda loc: w.moved(loc), True)

View File

@ -1093,7 +1093,7 @@ Braille Example
base_height = get_plate_height(text_lines, cell_geometry)
base_thickness = get_base_plate_thickness(plate_thickness, cell_geometry)
base = cq.Workplane('XY').box(base_width, base_height, base_thickness,
centered=(False, False, False))
centered=False)
return base
@ -1126,7 +1126,7 @@ Braille Example
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))
base_width, base_height, base_thickness, centered=False)
result = hidding_box.union(base)
return result

View File

@ -357,6 +357,41 @@ class TestCadQuery(BaseTest):
assert before[direction] == approx(after[direction])
assert plane.origin.toTuple() == origin
def testRect(self):
x = 10
y = 11
s = Workplane().rect(x, y)
# a rectangle has 4 sides
self.assertEqual(s.edges().size(), 4)
# assert that the lower left corner is in the correct spot for all
# possible values of centered
for centered_x, xval in zip([True, False], [-x / 2, 0]):
for centered_y, yval in zip([True, False], [-y / 2, 0]):
s = (
Workplane()
.rect(x, y, centered=(centered_x, centered_y))
.vertices("<X and <Y")
)
self.assertEqual(s.size(), 1)
self.assertTupleAlmostEquals(s.val().toTuple(), (xval, yval, 0), 3)
# check that centered=True is the same as centered=(True, True)
for option0 in [True, False]:
v0 = (
Workplane()
.rect(x, y, centered=option0)
.vertices(">X and >Y")
.val()
.toTuple()
)
v1 = (
Workplane()
.rect(x, y, centered=(option0, option0))
.vertices(">X and >Y")
.val()
.toTuple()
)
self.assertTupleAlmostEquals(v0, v1, 3)
def testLoft(self):
"""
Test making a lofted solid
@ -1002,24 +1037,50 @@ class TestCadQuery(BaseTest):
self.assertEqual(6, r.faces().size())
def testRectArray(self):
NUMX = 3
NUMY = 3
x_num = 3
y_num = 3
x_spacing = 8.0
y_spacing = 8.0
s = (
Workplane("XY")
.box(40, 40, 5, centered=(True, True, True))
.faces(">Z")
.workplane()
.rarray(8.0, 8.0, NUMX, NUMY, True)
.rarray(x_spacing, y_spacing, x_num, y_num, 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())
self.assertEqual(6 + x_num * y_num * 2, s.faces().size())
with raises(ValueError):
Workplane().rarray(0, 0, NUMX, NUMY, True)
Workplane().rarray(0, 0, x_num, y_num, True)
# check lower and upper corner points are correct for all combinations of centering
for x_opt, x_min, x_max in zip(
[True, False], [-x_spacing, 0.0], [x_spacing, x_spacing * 2]
):
for y_opt, y_min, y_max in zip(
[True, False], [-y_spacing, 0.0], [y_spacing, y_spacing * 2]
):
s = Workplane().rarray(
x_spacing, y_spacing, x_num, y_num, center=(x_opt, y_opt)
)
lower = Vector(x_min, y_min, 0)
upper = Vector(x_max, y_max, 0)
self.assertTrue(lower in s.objects)
self.assertTrue(upper in s.objects)
# check centered=True is equivalent to centered=(True, True)
for val in [True, False]:
s0 = Workplane().rarray(x_spacing, y_spacing, x_num, y_num, center=val)
s1 = Workplane().rarray(
x_spacing, y_spacing, x_num, y_num, center=(val, val)
)
# check all the points in s0 are present in s1
self.assertTrue(all(pnt in s1.objects for pnt in s0.objects))
self.assertEqual(s0.size(), s1.size())
def testPolarArray(self):
radius = 10
@ -2019,6 +2080,35 @@ class TestCadQuery(BaseTest):
# should have 26 faces. 6 for the box, and 4x5 for the smaller cubes
self.assertEqual(26, s.faces().size())
def testBoxCentered(self):
x, y, z = 10, 11, 12
# check that the bottom corner is where we expect it for all possible combinations of centered
b = [True, False]
expected_x = [-x / 2, 0]
expected_y = [-y / 2, 0]
expected_z = [-z / 2, 0]
for (xopt, xval), (yopt, yval), (zopt, zval) in product(
zip(b, expected_x), zip(b, expected_y), zip(b, expected_z)
):
s = (
Workplane()
.box(x, y, z, centered=(xopt, yopt, zopt))
.vertices("<X and <Y and <Z")
)
self.assertEqual(s.size(), 1)
self.assertTupleAlmostEquals(s.val().toTuple(), (xval, yval, zval), 3)
# check centered=True produces the same result as centered=(True, True, True)
for val in b:
s0 = Workplane().box(x, y, z, centered=val).vertices(">X and >Y and >Z")
self.assertEqual(s0.size(), 1)
s1 = (
Workplane()
.box(x, y, z, centered=(val, val, val))
.vertices(">X and >Y and >Z")
)
self.assertEqual(s0.size(), 1)
self.assertTupleAlmostEquals(s0.val().toTuple(), s1.val().toTuple(), 3)
def testSphereDefaults(self):
s = Workplane("XY").sphere(10)
self.saveModel(s) # Until FreeCAD fixes their sphere operation
@ -2033,6 +2123,26 @@ class TestCadQuery(BaseTest):
self.assertEqual(1, s.solids().size())
self.assertEqual(2, s.faces().size())
# check that the bottom corner is where we expect it for all possible combinations of centered
radius = 10
for (xopt, xval), (yopt, yval), (zopt, zval) in product(
zip((True, False), (0, radius)), repeat=3
):
s = Workplane().sphere(radius, centered=(xopt, yopt, zopt))
self.assertEqual(s.size(), 1)
self.assertTupleAlmostEquals(
s.val().Center().toTuple(), (xval, yval, zval), 3
)
# check centered=True produces the same result as centered=(True, True, True)
for val in (True, False):
s0 = Workplane().sphere(radius, centered=val)
self.assertEqual(s0.size(), 1)
s1 = Workplane().sphere(radius, centered=(val, val, val))
self.assertEqual(s0.size(), 1)
self.assertTupleAlmostEquals(
s0.val().Center().toTuple(), s1.val().Center().toTuple(), 3
)
def testSpherePointList(self):
s = (
Workplane("XY")
@ -2070,6 +2180,7 @@ class TestCadQuery(BaseTest):
self.assertEqual(1, s.solids().size())
self.assertEqual(5, s.faces().size())
self.assertEqual(5, s.vertices().size())
# check that the bottom corner is where we expect it for all possible combinations of centered
x, y, z = 10, 11, 12
b = [True, False]
@ -2086,6 +2197,21 @@ class TestCadQuery(BaseTest):
)
self.assertEqual(s.size(), 1)
self.assertTupleAlmostEquals(s.val().toTuple(), (xval, yval, zval), 3)
# check centered=True produces the same result as centered=(True, True, True)
for val in b:
s0 = (
Workplane()
.wedge(x, y, z, 2, 2, x - 2, z - 2, centered=val)
.vertices(">X and >Z")
)
self.assertEqual(s0.size(), 1)
s1 = (
Workplane()
.wedge(x, y, z, 2, 2, x - 2, z - 2, centered=(val, val, val))
.vertices(">X and >Z")
)
self.assertEqual(s0.size(), 1)
self.assertTupleAlmostEquals(s0.val().toTuple(), s1.val().toTuple(), 3)
def testWedgePointList(self):
s = (