# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2009, 2010 Yorik van Havre * # * Copyright (c) 2009, 2010 Ken Cline * # * * # * This file is part of the FreeCAD CAx development system. * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * FreeCAD is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with FreeCAD; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** """Provides various functions to work with circles.""" ## @package circles # \ingroup draftgeoutils # \brief Provides various functions to work with circles. import math import lazy_loader.lazy_loader as lz import FreeCAD as App import DraftVecUtils from draftgeoutils.general import geomType, edg, vec, NORM, v1 from draftgeoutils.geometry import mirror, findDistance from draftgeoutils.edges import findMidpoint from draftgeoutils.intersections import findIntersection, angleBisection # Delay import of module until first use because it is heavy Part = lz.LazyLoader("Part", globals(), "Part") ## \addtogroup draftgeoutils # @{ def findClosestCircle(point, circles): """Return the circle with closest center to a given point.""" dist = 1000000 closest = None for c in circles: if c.Center.sub(point).Length < dist: dist = c.Center.sub(point).Length closest = c return closest def getCircleFromSpline(edge): """Return a circle-based edge from a bspline-based edge. Return None if the edge is not a BSplineCurve. """ if geomType(edge) != "BSplineCurve" or len(edge.Vertexes) != 1: return None # get 2 points p1 = edge.Curve.value(0) p2 = edge.Curve.value(math.pi / 2) # get 2 tangents t1 = edge.Curve.tangent(0)[0] t2 = edge.Curve.tangent(math.pi / 2)[0] # get normal n = p1.cross(p2) if DraftVecUtils.isNull(n): return None # get rays r1 = DraftVecUtils.rotate(t1, math.pi / 2, n) r2 = DraftVecUtils.rotate(t2, math.pi / 2, n) # get center (intersection of rays) i = findIntersection(p1, p1.add(r1), p2, p2.add(r2), True, True) if not i: return None c = i[0] r = (p1.sub(c)).Length circle = Part.makeCircle(r, c, n) # print(circle.Curve) return circle def circlefrom1Line2Points(edge, p1, p2): """Return a list of circles created from an edge and two points. It calculates up to 2 possible centers. """ p1_p2 = edg(p1, p2) s = findIntersection(edge, p1_p2, True, True) if not s: return None s = s[0] v1 = p1.sub(s) v2 = p2.sub(s) projectedDist = math.sqrt(abs(v1.dot(v2))) edgeDir = vec(edge) edgeDir.normalize() projectedCen1 = App.Vector.add(s, App.Vector(edgeDir).multiply(projectedDist)) projectedCen2 = App.Vector.add(s, App.Vector(edgeDir).multiply(-projectedDist)) perpEdgeDir = edgeDir.cross(App.Vector(0, 0, 1)) perpCen1 = App.Vector.add(projectedCen1, perpEdgeDir) perpCen2 = App.Vector.add(projectedCen2, perpEdgeDir) mid = findMidpoint(p1_p2) x = DraftVecUtils.crossproduct(vec(p1_p2)) x.normalize() perp_mid = App.Vector.add(mid, x) cen1 = findIntersection(edg(projectedCen1, perpCen1), edg(mid, perp_mid), True, True) cen2 = findIntersection(edg(projectedCen2, perpCen2), edg(mid, perp_mid), True, True) circles = [] if cen1: radius = DraftVecUtils.dist(projectedCen1, cen1[0]) circles.append(Part.Circle(cen1[0], NORM, radius)) if cen2: radius = DraftVecUtils.dist(projectedCen2, cen2[0]) circles.append(Part.Circle(cen2[0], NORM, radius)) if circles: return circles else: return None def circlefrom2Lines1Point(edge1, edge2, point): """Return a list of circles from two edges and one point.""" bis = angleBisection(edge1, edge2) if not bis: return None mirrPoint = mirror(point, bis) return circlefrom1Line2Points(edge1, point, mirrPoint) def circleFrom2LinesRadius(edge1, edge2, radius): """Return a list of circles from two edges and one radius. It calculates 4 centers. """ intsec = findIntersection(edge1, edge2, True, True) if not intsec: return None intsec = intsec[0] bis12 = angleBisection(edge1, edge2) bis21 = Part.LineSegment( bis12.Vertexes[0].Point, DraftVecUtils.rotate(vec(bis12), math.pi / 2.0) ) ang12 = abs(DraftVecUtils.angle(vec(edge1), vec(edge2))) ang21 = math.pi - ang12 dist12 = radius / math.sin(ang12 * 0.5) dist21 = radius / math.sin(ang21 * 0.5) circles = [] cen = App.Vector.add(intsec, vec(bis12).multiply(dist12)) circles.append(Part.Circle(cen, NORM, radius)) cen = App.Vector.add(intsec, vec(bis12).multiply(-dist12)) circles.append(Part.Circle(cen, NORM, radius)) cen = App.Vector.add(intsec, vec(bis21).multiply(dist21)) circles.append(Part.Circle(cen, NORM, radius)) cen = App.Vector.add(intsec, vec(bis21).multiply(-dist21)) circles.append(Part.Circle(cen, NORM, radius)) return circles def circleFrom3LineTangents(edge1, edge2, edge3): """Return a list of circles from three edges. It calculates up to 6 possible centers. """ def rot(ed): geo = Part.LineSegment(v1(ed), v1(ed).add(DraftVecUtils.rotate(vec(ed), math.pi / 2))) return geo.toShape() bis12 = angleBisection(edge1, edge2) bis23 = angleBisection(edge2, edge3) bis31 = angleBisection(edge3, edge1) intersections = [] intsec = findIntersection(bis12, bis23, True, True) if intsec: radius = findDistance(intsec[0], edge1).Length intersections.append(Part.Circle(intsec[0], NORM, radius)) intsec = findIntersection(bis23, bis31, True, True) if intsec: radius = findDistance(intsec[0], edge1).Length intersections.append(Part.Circle(intsec[0], NORM, radius)) intsec = findIntersection(bis31, bis12, True, True) if intsec: radius = findDistance(intsec[0], edge1).Length intersections.append(Part.Circle(intsec[0], NORM, radius)) intsec = findIntersection(rot(bis12), rot(bis23), True, True) if intsec: radius = findDistance(intsec[0], edge1).Length intersections.append(Part.Circle(intsec[0], NORM, radius)) intsec = findIntersection(rot(bis23), rot(bis31), True, True) if intsec: radius = findDistance(intsec[0], edge1).Length intersections.append(Part.Circle(intsec[0], NORM, radius)) intsec = findIntersection(rot(bis31), rot(bis12), True, True) if intsec: radius = findDistance(intsec[0], edge1).Length intersections.append(Part.Circle(intsec[0], NORM, radius)) circles = [] for intsec in intersections: exists = False for cir in circles: if DraftVecUtils.equals(cir.Center, intsec.Center): exists = True break if not exists: circles.append(intsec) if circles: return circles else: return None def circleFromPointLineRadius(point, edge, radius): """Return a list of circles from one point, one edge, and one radius. It calculates up to 2 possible centers. """ dist = findDistance(point, edge, False) center1 = None center2 = None if dist.Length == 0: segment = vec(edge) perpVec = DraftVecUtils.crossproduct(segment) perpVec.normalize() normPoint_c1 = App.Vector(perpVec).multiply(radius) normPoint_c2 = App.Vector(perpVec).multiply(-radius) center1 = point.add(normPoint_c1) center2 = point.add(normPoint_c2) elif dist.Length > 2 * radius: return None elif dist.Length == 2 * radius: normPoint = point.add(findDistance(point, edge, False)) dummy = (normPoint.sub(point)).multiply(0.5) cen = point.add(dummy) circ = Part.Circle(cen, NORM, radius) if circ: return [circ] else: return None else: normPoint = point.add(findDistance(point, edge, False)) normDist = DraftVecUtils.dist(normPoint, point) dist = math.sqrt(radius**2 - (radius - normDist) ** 2) centerNormVec = DraftVecUtils.scaleTo(point.sub(normPoint), radius) edgeDir = edge.Vertexes[0].Point.sub(normPoint) edgeDir.normalize() center1 = centerNormVec.add(normPoint.add(App.Vector(edgeDir).multiply(dist))) center2 = centerNormVec.add(normPoint.add(App.Vector(edgeDir).multiply(-dist))) circles = [] if center1: circ = Part.Circle(center1, NORM, radius) if circ: circles.append(circ) if center2: circ = Part.Circle(center2, NORM, radius) if circ: circles.append(circ) if circles: return circles else: return None def circleFrom2PointsRadius(p1, p2, radius): """Return a list of circles from two points, and one radius. The two points must not be equal. It calculates up to 2 possible centers. """ if DraftVecUtils.equals(p1, p2): return None p1_p2 = Part.LineSegment(p1, p2).toShape() dist_p1p2 = DraftVecUtils.dist(p1, p2) mid = findMidpoint(p1_p2) if dist_p1p2 == 2 * radius: circle = Part.Circle(mid, NORM, radius) if circle: return [circle] else: return None _dir = vec(p1_p2) _dir.normalize() perpDir = _dir.cross(App.Vector(0, 0, 1)) perpDir.normalize() dist = math.sqrt(radius**2 - (dist_p1p2 / 2.0) ** 2) cen1 = App.Vector.add(mid, App.Vector(perpDir).multiply(dist)) cen2 = App.Vector.add(mid, App.Vector(perpDir).multiply(-dist)) circles = [] if cen1: circles.append(Part.Circle(cen1, NORM, radius)) if cen2: circles.append(Part.Circle(cen2, NORM, radius)) if circles: return circles else: return None def findHomotheticCenterOfCircles(circle1, circle2): """Calculate the homothetic centers from two circles. Return None if the objects are not circles, or if they are concentric. http://en.wikipedia.org/wiki/Homothetic_center http://mathworld.wolfram.com/HomotheticCenter.html """ if geomType(circle1) == "Circle" and geomType(circle2) == "Circle": print("debug: findHomotheticCenterOfCircles bad parameters!") return None if DraftVecUtils.equals(circle1.Curve.Center, circle2.Curve.Center): return None cen1_cen2 = Part.LineSegment(circle1.Curve.Center, circle2.Curve.Center).toShape() cenDir = vec(cen1_cen2) cenDir.normalize() # Get the perpendicular vector. perpCenDir = cenDir.cross(App.Vector(0, 0, 1)) perpCenDir.normalize() # Get point on first circle p1 = App.Vector.add(circle1.Curve.Center, App.Vector(perpCenDir).multiply(circle1.Curve.Radius)) centers = [] # Calculate inner homothetic center # Get point on second circle p2_inner = App.Vector.add( circle1.Curve.Center, App.Vector(perpCenDir).multiply(-circle1.Curve.Radius) ) hCenterInner = DraftVecUtils.intersect( circle1.Curve.Center, circle2.Curve.Center, p1, p2_inner, True, True ) if hCenterInner: centers.append(hCenterInner) # Calculate outer homothetic center; it only exists if the circles # have different radii if circle1.Curve.Radius != circle2.Curve.Radius: # Get point on second circle p2_outer = App.Vector.add( circle1.Curve.Center, App.Vector(perpCenDir).multiply(circle1.Curve.Radius) ) hCenterOuter = DraftVecUtils.intersect( circle1.Curve.Center, circle2.Curve.Center, p1, p2_outer, True, True ) if hCenterOuter: centers.append(hCenterOuter) if centers: return centers else: return None def findRadicalAxis(circle1, circle2): """Calculate the radical axis of two circles. On the radical axis (also called power line) of two circles any tangents drawn from a point on the axis to both circles have the same length. http://en.wikipedia.org/wiki/Radical_axis http://mathworld.wolfram.com/RadicalLine.html See Also -------- findRadicalCenter """ if (geomType(circle1) == "Circle") and (geomType(circle2) == "Circle"): print("debug: findRadicalAxis bad parameters! Must be circles.") return None if DraftVecUtils.equals(circle1.Curve.Center, circle2.Curve.Center): return None r1 = circle1.Curve.Radius r2 = circle1.Curve.Radius cen1 = circle1.Curve.Center # dist .. the distance from cen1 to cen2. dist = DraftVecUtils.dist(cen1, circle2.Curve.Center) cenDir = cen1.sub(circle2.Curve.Center) cenDir.normalize() # Get the perpendicular vector. perpCenDir = cenDir.cross(App.Vector(0, 0, 1)) perpCenDir.normalize() # J ... The radical center. # K ... The point where the cadical axis crosses the line of cen1->cen2. # k1 ... Distance from cen1 to K. # k2 ... Distance from cen2 to K. # dist = k1 + k2 k1 = (dist + (r1**2 - r2**2) / dist) / 2.0 # k2 = dist - k1 K = App.Vector.add(cen1, cenDir.multiply(k1)) # K_ .. A point somewhere between K and J; actually with a distance # of 1 unit from K. K_ = App.Vector.add(K, perpCenDir) # Original code didn't specify the value of origin nor dir, # so this is a guess: # radicalAxis = Part.LineSegment(K, Vector.add(origin, dir)) origin = App.Vector(0, 0, 0) radicalAxis = Part.LineSegment(K, App.Vector.add(origin, perpCenDir)) if radicalAxis: return radicalAxis else: return None def findRadicalCenter(circle1, circle2, circle3): """Calculate the radical center of three circles. It is also called the power center. It is the intersection point of the three radical axes of the pairs of circles. http://en.wikipedia.org/wiki/Power_center_(geometry) http://mathworld.wolfram.com/RadicalCenter.html See Also -------- findRadicalAxis """ if ( geomType(circle1) != "Circle" or geomType(circle2) != "Circle" or geomType(circle3) != "Circle" ): print("debug: findRadicalCenter bad parameters! Must be circles.") return None radicalAxis12 = findRadicalAxis(circle1, circle2) radicalAxis23 = findRadicalAxis(circle2, circle3) if not radicalAxis12 or not radicalAxis23: # No radical center could be calculated. return None intersect = findIntersection(radicalAxis12, radicalAxis23, True, True) if intersect: return intersect else: # No radical center could be calculated. return None ## @}