# 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 general functions to work with topological shapes.""" ## @package general # \ingroup draftgeoutils # \brief Provides general functions to work with topological shapes. import math import lazy_loader.lazy_loader as lz import FreeCAD as App import DraftVecUtils from draftutils import params # Delay import of module until first use because it is heavy Part = lz.LazyLoader("Part", globals(), "Part") ## \addtogroup draftgeoutils # @{ # Default normal direction for all geometry operations NORM = App.Vector(0, 0, 1) def precision(): """Return the Draft precision setting.""" # Set precision level with a cap to avoid overspecification that: # 1 - whilst it is precise enough (e.g. that OCC would consider # 2 points are coincident) # (not sure what it should be 10 or otherwise); # 2 - but FreeCAD/OCC can handle 'internally' # (e.g. otherwise user may set something like # 15 that the code would never consider 2 points are coincident # as internal float is not that precise) precisionMax = 10 precisionInt = params.get_param("precision") precisionInt = precisionInt if precisionInt <= 10 else precisionMax return precisionInt def vec(edge, use_orientation=False): """Return a vector from an edge or a Part.LineSegment. If use_orientation is True, it takes into account the edges orientation. If edge is not straight, you'll get strange results! """ if isinstance(edge, Part.Shape): if use_orientation and isinstance(edge, Part.Edge) and edge.Orientation == "Reversed": return edge.Vertexes[0].Point.sub(edge.Vertexes[-1].Point) else: return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) elif isinstance(edge, Part.LineSegment): return edge.EndPoint.sub(edge.StartPoint) else: return None def edg(p1, p2): """Return an edge from 2 vectors.""" if isinstance(p1, App.Vector) and isinstance(p2, App.Vector): if DraftVecUtils.equals(p1, p2): return None return Part.LineSegment(p1, p2).toShape() def getVerts(shape): """Return a list containing vectors of each vertex of the shape.""" if not hasattr(shape, "Vertexes"): return [] p = [] for v in shape.Vertexes: p.append(v.Point) return p def v1(edge): """Return the first point of an edge.""" return edge.Vertexes[0].Point def isNull(something): """Return True if the given shape, vector, or placement is Null. If the vector is (0, 0, 0), it will return True. """ if isinstance(something, Part.Shape): return something.isNull() elif isinstance(something, App.Vector): if something == App.Vector(0, 0, 0): return True else: return False elif isinstance(something, App.Placement): if something.Base == App.Vector(0, 0, 0) and something.Rotation.Q == (0, 0, 0, 1): return True else: return False def isPtOnEdge(pt, edge): """Test if a point lies on an edge.""" v = Part.Vertex(pt) try: d = v.distToShape(edge) except Part.OCCError: return False else: if d: if round(d[0], precision()) == 0: return True return False def hasCurves(shape): """Check if the given shape has curves.""" for e in shape.Edges: if not isinstance(e.Curve, (Part.LineSegment, Part.Line)): return True return False def isAligned(edge, axis="x"): """Check if the given edge or line is aligned to the given axis. The axis can be 'x', 'y' or 'z'. """ def is_same(a, b): return round(a, precision()) == round(b, precision()) if axis == "x": if isinstance(edge, Part.Edge): if len(edge.Vertexes) == 2: return is_same(edge.Vertexes[0].X, edge.Vertexes[-1].X) elif isinstance(edge, Part.LineSegment): return is_same(edge.StartPoint.x, edge.EndPoint.x) elif axis == "y": if isinstance(edge, Part.Edge): if len(edge.Vertexes) == 2: return is_same(edge.Vertexes[0].Y, edge.Vertexes[-1].Y) elif isinstance(edge, Part.LineSegment): return is_same(edge.StartPoint.y, edge.EndPoint.y) elif axis == "z": if isinstance(edge, Part.Edge): if len(edge.Vertexes) == 2: return is_same(edge.Vertexes[0].Z, edge.Vertexes[-1].Z) elif isinstance(edge, Part.LineSegment): return is_same(edge.StartPoint.z, edge.EndPoint.z) return False def getQuad(face): """Return a list of 3 vectors if the face is a quad, otherwise None. Returns ------- basepoint, Xdir, Ydir If the face is a quad. None If the face is not a quad. """ if len(face.Edges) != 4: return None v1 = vec(face.Edges[0]) # Warning redefinition of function v1 v2 = vec(face.Edges[1]) v3 = vec(face.Edges[2]) v4 = vec(face.Edges[3]) angles90 = [round(math.pi * 0.5, precision()), round(math.pi * 1.5, precision())] angles180 = [0, round(math.pi, precision()), round(math.pi * 2, precision())] for ov in [v2, v3, v4]: if not (round(v1.getAngle(ov), precision()) in angles90 + angles180): return None for ov in [v2, v3, v4]: if round(v1.getAngle(ov), precision()) in angles90: v1.normalize() ov.normalize() return [face.Edges[0].Vertexes[0].Point, v1, ov] def areColinear(e1, e2): """Return True if both edges are colinear.""" if not isinstance(e1.Curve, (Part.LineSegment, Part.Line)): return False if not isinstance(e2.Curve, (Part.LineSegment, Part.Line)): return False v1 = vec(e1) v2 = vec(e2) a = round(v1.getAngle(v2), precision()) if (a == 0) or (a == round(math.pi, precision())): v3 = e2.Vertexes[0].Point.sub(e1.Vertexes[0].Point) if DraftVecUtils.isNull(v3): return True else: a2 = round(v1.getAngle(v3), precision()) if (a2 == 0) or (a2 == round(math.pi, precision())): return True return False def hasOnlyWires(shape): """Return True if all edges are inside a wire.""" ne = 0 for w in shape.Wires: ne += len(w.Edges) if ne == len(shape.Edges): return True return False def geomType(edge): """Return the type of geometry this edge is based on. Parameters ---------- edge: the edge whose `Curve` attribute is to be checked. Returns ------- str Return the type of the edge's Curve attribute or "Unknown", if the parameter is missing. """ try: if isinstance(edge.Curve, (Part.LineSegment, Part.Line)): return "Line" elif isinstance(edge.Curve, Part.Circle): return "Circle" elif isinstance(edge.Curve, Part.BSplineCurve): return "BSplineCurve" elif isinstance(edge.Curve, Part.BezierCurve): return "BezierCurve" elif isinstance(edge.Curve, Part.Ellipse): return "Ellipse" else: return "Unknown" except Exception: # catch all errors, no only TypeError return "Unknown" def isValidPath(shape): """Return True if the shape can be used as an extrusion path.""" if shape.isNull(): return False if shape.Faces: return False if len(shape.Wires) > 1: return False if shape.Wires: if shape.Wires[0].isClosed(): return False if shape.isClosed(): return False return True def findClosest(base_point, point_list): """Find closest point in a list of points to the base point. Returns ------- int An index from the list of points is returned. None If point_list is empty. """ npoint = None if not point_list: return None smallest = 1000000 for n in range(len(point_list)): new = base_point.sub(point_list[n]).Length if new < smallest: smallest = new npoint = n return npoint def getBoundaryAngles(angle, alist): """Return the 2 closest angles that encompass the given angle.""" negs = True while negs: negs = False for i in range(len(alist)): if alist[i] < 0: alist[i] = 2 * math.pi + alist[i] negs = True if angle < 0: angle = 2 * math.pi + angle negs = True lower = None for a in alist: if a < angle: if lower is None: lower = a else: if a > lower: lower = a if lower is None: lower = 0 for a in alist: if a > lower: lower = a higher = None for a in alist: if a > angle: if higher is None: higher = a else: if a < higher: higher = a if higher is None: higher = 2 * math.pi for a in alist: if a < higher: higher = a return lower, higher ## @}