diff --git a/cadquery/cq.py b/cadquery/cq.py index ea1dec03..38bd0693 100644 --- a/cadquery/cq.py +++ b/cadquery/cq.py @@ -267,8 +267,10 @@ class CQ(object): :param offset: offset for the work plane in the Z direction. Default :param invert: invert the Z direction from that of the face. + :param centerOption: how local origin of workplane is determined :type offset: float or None=0.0 :type invert: boolean or None=False + :type centerOption: string or None='CenterOfMass' :rtype: Workplane object ( which is a subclass of CQ ) The first element on the stack must be a face, a set of @@ -282,7 +284,8 @@ class CQ(object): * The origin will be located in the *center* of the face/faces, if a face/faces was selected. If a vertex was selected, the origin will be at the vertex, and located - on the face. + on the face. The centerOption paramter sets how the center is defined. + Options are 'CenterOfMass', 'CenterOfBoundBox', or 'ProjectedOrigin'. * The Z direction will be normal to the plane of the face,computed at the center point. * The X direction will be parallel to the x-y plane. If the workplane is parallel to @@ -331,6 +334,23 @@ class CQ(object): xd = Vector(1, 0, 0) return xd + def _pointOnPlane(center, normal, point): + """ + Returns the point on the plane defined by center and normal nearest to the input + argument point. + """ + a = normal.x; b = normal.y; c = normal.z + x0 = center.x; y0 = center.y; z0 = center.z + x1 = point.x; y1 = point.y; z1 = point.z + + denom = a**2 + b**2 + c**2 + + nearest_x = (-a*b*y1 - a*c*z1 + a*(a*x0 + b*y0 + c*z0) + x1*(b**2 + c**2))/denom + nearest_y = (-a*b*x1 - b*c*z1 + b*(a*x0 + b*y0 + c*z0) + y1*(a**2 + c**2))/denom + nearest_z = (-a*c*x1 - b*c*y1 + c*(a*x0 + b*y0 + c*z0) + z1*(a**2 + b**2))/denom + + return Vector(nearest_x, nearest_y, nearest_z) + if len(self.objects) > 1: # are all objects 'PLANE'? if not all(o.geomType() in ('PLANE', 'CIRCLE') for o in self.objects): @@ -341,7 +361,7 @@ class CQ(object): if not all(_isCoPlanar(self.objects[0], f) for f in self.objects[1:]): raise ValueError("Selected faces must be co-planar.") - if centerOption == 'CenterOfMass': + if centerOption == 'CenterOfMass' or centerOption == 'ProjectedOrigin': center = Shape.CombinedCenter(self.objects) elif centerOption == 'CenterOfBoundBox': center = Shape.CombinedCenterOfBoundBox(self.objects) @@ -353,7 +373,7 @@ class CQ(object): obj = self.objects[0] if isinstance(obj, Face): - if centerOption == 'CenterOfMass': + if centerOption == 'CenterOfMass' or centerOption == 'ProjectedOrigin': center = obj.Center() elif centerOption == 'CenterOfBoundBox': center = obj.CenterOfBoundBox() @@ -361,7 +381,7 @@ class CQ(object): xDir = _computeXdir(normal) else: if hasattr(obj, 'Center'): - if centerOption == 'CenterOfMass': + if centerOption == 'CenterOfMass' or centerOption == 'ProjectedOrigin': center = obj.Center() elif centerOption == 'CenterOfBoundBox': center = obj.CenterOfBoundBox() @@ -371,6 +391,10 @@ class CQ(object): raise ValueError( "Needs a face or a vertex or point on a work plane") + # update center to projected origin if desired + if centerOption == 'ProjectedOrigin': + center = _pointOnPlane(center, normal, Vector(0, 0, 0)) + # invert if requested if invert: normal = normal.multiply(-1.0) diff --git a/tests/TestCadQuery.py b/tests/TestCadQuery.py index db4dbb33..9efc2402 100644 --- a/tests/TestCadQuery.py +++ b/tests/TestCadQuery.py @@ -1901,4 +1901,56 @@ class TestCadQuery(BaseTest): self.assertEqual(len(solid.Vertices()),4) self.assertEqual(len(solid.Faces()),4) + + def testWorkplaneCenterOptions(self): + """ + Test options for specifiying origin of workplane + """ + decimal_places = 9 + + pts = [(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() + self.assertTupleAlmostEquals(origin, (0.0, 0.0, 10.0), decimal_places) + + 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() + self.assertTupleAlmostEquals(origin, (45.0, 30.0, 10.0), decimal_places) + + r = Workplane("YZ").polyline(pts).close().extrude(10.0) + + 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() + self.assertTupleAlmostEquals(origin, (10.0, 37.5, 22.5), decimal_places) + + 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("