Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mirror from face features (with example now fixed) #527

Merged
merged 21 commits into from
Dec 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7bb2fe1
Add the ability to mirror from a selected face
blooop Nov 29, 2020
b9e2e6b
Fix errors in example.rst
blooop Nov 30, 2020
9961f45
Add mirror from face features (with example now fixed)
blooop Nov 30, 2020
241ebe8
actually merge the fixed documentation this time...
blooop Nov 30, 2020
885be6e
Merge branch 'master' into mirror-faces-fx1
blooop Nov 30, 2020
94ec465
recommit merge conflicts in shape.py that were stopping CI merging in…
blooop Nov 30, 2020
7fb9fdb
use correct variable name when selecting face
blooop Nov 30, 2020
0d9ccc8
formatting with black v19 instead of black v21
blooop Nov 30, 2020
4acfd79
add missing string literal types and clean up docstring
blooop Nov 30, 2020
fadd4c7
Add volume assertions to the test cases
blooop Nov 30, 2020
c0744e2
black formatting
blooop Nov 30, 2020
22bd3c4
Mypy fix
adam-urbanczyk Dec 2, 2020
7c3c6ce
Restructured mirror
adam-urbanczyk Dec 3, 2020
064aca7
update examples to use basePoint instead of basePointVector
blooop Dec 3, 2020
402fa23
Merge branch 'mirror-faces-fx1' of https://github.com/blooop/cadquery…
blooop Dec 3, 2020
48fcf5a
Add tests for other workplane strings and non union option to increat…
blooop Dec 3, 2020
7b85b7d
Add test to check that exception is thown for incorrect input
blooop Dec 3, 2020
658c80e
Go back to basePointVector
adam-urbanczyk Dec 4, 2020
fb3ab34
Update arg naming in examples
adam-urbanczyk Dec 4, 2020
4280340
Mirror all objects
adam-urbanczyk Dec 7, 2020
bc3ad1d
Typo fix
adam-urbanczyk Dec 7, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 49 additions & 4 deletions cadquery/cq.py
Original file line number Diff line number Diff line change
Expand Up @@ -1027,17 +1027,62 @@ def rotate(
]
)

def mirror(self, mirrorPlane="XY", basePointVector=(0, 0, 0)):
def mirror(
self,
mirrorPlane: Union[
Literal["XY", "YX", "XZ", "ZX", "YZ", "ZY"], VectorLike, Face, "Workplane"
] = "XY",
basePointVector: Optional[VectorLike] = None,
union: bool = False,
):
"""
Mirror a single CQ object. This operation is the same as in the FreeCAD PartWB's mirroring

:param mirrorPlane: the plane to mirror about
:type mirrorPlane: string, one of "XY", "YX", "XZ", "ZX", "YZ", "ZY" the planes
:param basePointVector: the base point to mirror about
or the normal vector of the plane eg (1,0,0) or a Face object
:param basePointVector: the base point to mirror about (this is overwritten if a Face is passed)
:type basePointVector: tuple
:param union: If true will perform a union operation on the mirrored object
:type union: bool
"""
newS = self.newObject([self.objects[0].mirror(mirrorPlane, basePointVector)])
return newS.first()

mp: Union[Literal["XY", "YX", "XZ", "ZX", "YZ", "ZY"], Vector]
bp: Vector
face: Optional[Face] = None

# handle mirrorPLane
if isinstance(mirrorPlane, Workplane):
val = mirrorPlane.val()
if isinstance(val, Face):
mp = val.normalAt()
face = val
else:
raise ValueError(f"Face required, got {val}")
elif isinstance(mirrorPlane, Face):
mp = mirrorPlane.normalAt()
face = mirrorPlane
elif not isinstance(mirrorPlane, str):
mp = Vector(mirrorPlane)
else:
mp = mirrorPlane

# handle basePointVector
if face and basePointVector is None:
bp = face.Center()
elif basePointVector is None:
bp = Vector()
else:
bp = Vector(basePointVector)

newS = self.newObject(
[obj.mirror(mp, bp) for obj in self.vals() if isinstance(obj, Shape)]
)

if union:
return self.union(newS)
else:
return newS

def translate(self, vec: VectorLike) -> "Workplane":
"""
Expand Down
23 changes: 15 additions & 8 deletions cadquery/occ_impl/shapes.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,7 +500,9 @@ def BoundingBox(

def mirror(
self,
mirrorPlane=Literal["XY", "YX", "XZ", "ZX", "YZ", "ZY"],
mirrorPlane: Union[
Literal["XY", "YX", "XZ", "ZX", "YZ", "ZY"], VectorLike
] = "XY",
basePointVector: VectorLike = (0, 0, 0),
) -> "Shape":
"""
Expand All @@ -512,13 +514,18 @@ def mirror(
:param basePointVector: The origin of the plane to mirror about
:returns: The mirrored shape
"""

if mirrorPlane == "XY" or mirrorPlane == "YX":
mirrorPlaneNormalVector = gp_Dir(0, 0, 1)
elif mirrorPlane == "XZ" or mirrorPlane == "ZX":
mirrorPlaneNormalVector = gp_Dir(0, 1, 0)
elif mirrorPlane == "YZ" or mirrorPlane == "ZY":
mirrorPlaneNormalVector = gp_Dir(1, 0, 0)
if isinstance(mirrorPlane, str):
if mirrorPlane == "XY" or mirrorPlane == "YX":
mirrorPlaneNormalVector = gp_Dir(0, 0, 1)
elif mirrorPlane == "XZ" or mirrorPlane == "ZX":
mirrorPlaneNormalVector = gp_Dir(0, 1, 0)
elif mirrorPlane == "YZ" or mirrorPlane == "ZY":
mirrorPlaneNormalVector = gp_Dir(1, 0, 0)
else:
if isinstance(mirrorPlane, tuple):
mirrorPlaneNormalVector = gp_Dir(*mirrorPlane)
elif isinstance(mirrorPlane, Vector):
mirrorPlaneNormalVector = mirrorPlane.toDir()

if isinstance(basePointVector, tuple):
basePointVector = Vector(basePointVector)
Expand Down
34 changes: 32 additions & 2 deletions doc/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -341,8 +341,8 @@ Mirroring 3D Objects

mirXY_neg = result.mirror(mirrorPlane="XY", basePointVector=(0, 0, -30))
mirXY_pos = result.mirror(mirrorPlane="XY", basePointVector=(0, 0, 30))
mirZY_neg = result.mirror(mirrorPlane="ZY", basePointVector=(-30,0,0))
mirZY_pos = result.mirror(mirrorPlane="ZY", basePointVector=(30,0,0))
mirZY_neg = result.mirror(mirrorPlane="ZY", basePointVector=(-30, 0, 0))
mirZY_pos = result.mirror(mirrorPlane="ZY", basePointVector=(30, 0, 0))

result = result.union(mirXY_neg).union(mirXY_pos).union(mirZY_neg).union(mirZY_pos)

Expand All @@ -360,6 +360,36 @@ Mirroring 3D Objects
* :py:meth:`Workplane.union`
* :py:meth:`Workplane.rotate`


Mirroring From Faces
-----------------------------

This example shows how you can mirror about a selected face. It also shows how the resulting mirrored object can be unioned immediately with the referenced mirror geometry.

.. cadquery::

result = (cq.Workplane("XY")
.line(0, 1)
.line(1, 0)
.line(0, -.5)
.close()
.extrude(1))

result = result.mirror(result.faces(">X"), union=True)


.. topic:: Api References

.. hlist::
:columns: 2

* :py:meth:`Workplane.line`
* :py:meth:`Workplane.close`
* :py:meth:`Workplane.extrude`
* :py:meth:`Workplane.faces`
* :py:meth:`Workplane.mirror`
* :py:meth:`Workplane.union`

Creating Workplanes on Faces
-----------------------------

Expand Down
98 changes: 98 additions & 0 deletions tests/test_workplanes.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,101 @@ def testZYPlaneBasics(self):
self.assertTupleAlmostEquals(p.zDir.toTuple(), xInvAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.xDir.toTuple(), zAxis_.toTuple(), 4)
self.assertTupleAlmostEquals(p.yDir.toTuple(), yAxis_.toTuple(), 4)

def test_mirror(self):
"""Create a unit box and mirror it so that it doubles in size"""
b2 = Workplane().box(1, 1, 1)
b2 = b2.mirror("XY", (0, 0, 0.5), union=True)
bbBox = b2.findSolid().BoundingBox()
assert [bbBox.xlen, bbBox.ylen, bbBox.zlen] == [1.0, 1.0, 2]
self.assertAlmostEqual(b2.findSolid().Volume(), 2, 5)

def test_all_planes(self):
b2 = Workplane().box(1, 1, 1)
for p in ["XY", "YX", "XZ", "ZX", "YZ", "ZY"]:
b2 = b2.mirror(p)
bbBox = b2.findSolid().BoundingBox()
assert [bbBox.xlen, bbBox.ylen, bbBox.zlen] == [1.0, 1.0, 1.0]
self.assertAlmostEqual(b2.findSolid().Volume(), 1, 5)

def test_bad_plane_input(self):
b2 = Workplane().box(1, 1, 1)
with self.assertRaises(ValueError) as context:
b2.mirror(b2.edges())
self.assertTrue("Face required, got" in str(context.exception))

def test_mirror_axis(self):
"""Create a unit box and mirror it so that it doubles in size"""
b2 = Workplane().box(1, 1, 1)
b2 = b2.mirror((0, 0, 1), (0, 0, 0.5), union=True)
bbBox = b2.findSolid().BoundingBox()
assert [bbBox.xlen, bbBox.ylen, bbBox.zlen] == [1.0, 1.0, 2]
self.assertAlmostEqual(b2.findSolid().Volume(), 2, 5)

def test_mirror_workplane(self):
"""Create a unit box and mirror it so that it doubles in size"""
b2 = Workplane().box(1, 1, 1)

# double in Z plane
b2 = b2.mirror(b2.faces(">Z"), union=True)
bbBox = b2.findSolid().BoundingBox()
assert [bbBox.xlen, bbBox.ylen, bbBox.zlen] == [1.0, 1.0, 2]
self.assertAlmostEqual(b2.findSolid().Volume(), 2, 5)

# double in Y plane
b2 = b2.mirror(b2.faces(">Y"), union=True)
bbBox = b2.findSolid().BoundingBox()
assert [bbBox.xlen, bbBox.ylen, bbBox.zlen] == [1.0, 2.0, 2]
self.assertAlmostEqual(b2.findSolid().Volume(), 4, 5)

# double in X plane
b2 = b2.mirror(b2.faces(">X"), union=True)
bbBox = b2.findSolid().BoundingBox()
assert [bbBox.xlen, bbBox.ylen, bbBox.zlen] == [2.0, 2.0, 2]
self.assertAlmostEqual(b2.findSolid().Volume(), 8, 5)

def test_mirror_equivalence(self):
"""test that the plane string, plane normal and face object perform a mirror operation in the same way"""
boxes = []
boxDims = 1
for i in range(3): # create 3 sets of identical boxes
boxTmp = Workplane("XY").box(boxDims, boxDims, boxDims)
boxTmp = boxTmp.translate([i * 2, 0, boxDims / 2])
boxes.append(boxTmp)

# 3 different types of plane definition
planeArg = ["XY", (0, 0, 1), boxes[0].faces("<Z")]
planeOffset = (0, 0, 0.5) # use the safe offset for each
boxResults = [] # store the resulting mirrored objects
for b, p in zip(boxes, planeArg):
boxResults.append(b.mirror(p, planeOffset, union=True))

# all resulting boxes should be equal to each other
for i in range(len(boxResults) - 1):
curBoxDims = boxResults[i].findSolid().BoundingBox() # get bbox dims
nextBoxDims = boxResults[i + 1].findSolid().BoundingBox() # get bbox dims
cbd = (curBoxDims.xlen, curBoxDims.ylen, curBoxDims.zlen)
nbd = (nextBoxDims.xlen, nextBoxDims.ylen, nextBoxDims.zlen)
self.assertTupleAlmostEquals(cbd, nbd, 4)
self.assertAlmostEqual(
boxResults[i].findSolid().Volume(),
boxResults[i + 1].findSolid().Volume(),
5,
)

def test_mirror_face(self):
"""Create a triangle and mirror into a unit box"""
r = Workplane("XY").line(0, 1).line(1, -1).close().extrude(1)

bbBox = r.findSolid().BoundingBox()
self.assertTupleAlmostEquals(
(bbBox.xlen, bbBox.ylen, bbBox.zlen), (1.0, 1.0, 1.0), 4
)
self.assertAlmostEqual(r.findSolid().Volume(), 0.5, 5)

r = r.mirror(r.faces().objects[1], union=True)
bbBox = r.findSolid().BoundingBox()
self.assertTupleAlmostEquals(
(bbBox.xlen, bbBox.ylen, bbBox.zlen), (1.0, 1.0, 1.0), 4
)
self.assertAlmostEqual(r.findSolid().Volume(), 1.0, 5)