diff --git a/.gitignore b/.gitignore index e7df8cfc4..8a19d8f0c 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 2332a5b5a..0bc06150e 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -1570,6 +1570,65 @@ def parametricCurve(self, func, N=400, start=0, stop=1): 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 @@ def makeCircleWire(obj): 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 72c4e7b9c..5a425b175 100644 --- a/cadquery/occ_impl/shapes.py +++ b/cadquery/occ_impl/shapes.py @@ -11,6 +11,7 @@ gp_Ax3, gp_Dir, gp_Circ, + gp_Elips, gp_Trsf, gp_Pln, gp_GTrsf, @@ -76,7 +77,7 @@ 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 @@ def makeCircle( ).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 @@ def makeCircle(cls, radius, center, normal): 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 42ac9093f..254c400ce 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 c556678a6..53cd3c7c8 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 @@ def _make_circle(self): 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 @@ def testEdgeWrapperCenter(self): 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 @@ def testEdgeWrapperMakeCircle(self): (-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 b07f91213..e41e3f733 100644 --- a/tests/test_cadquery.py +++ b/tests/test_cadquery.py @@ -552,6 +552,199 @@ def testSpline(self): 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 @@ def testNestedCircle(self): 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