| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | """Provides various functions for general geometrical calculations.""" |
| | |
| | |
| | |
| |
|
| | import math |
| | import lazy_loader.lazy_loader as lz |
| |
|
| | import FreeCAD as App |
| | import DraftVecUtils |
| |
|
| | import draftutils.gui_utils as gui_utils |
| |
|
| | from draftgeoutils.general import geomType, vec |
| |
|
| | |
| | Part = lz.LazyLoader("Part", globals(), "Part") |
| |
|
| | |
| | |
| |
|
| |
|
| | def findPerpendicular(point, edgeslist, force=None): |
| | """Find the perpendicular distance between a point and a list of edges. |
| | |
| | If force is specified, only the edge[force] will be considered, |
| | and it will be considered infinite. |
| | |
| | Returns |
| | ------- |
| | [vector_from_point_to_closest_edge, edge_index] |
| | The vector and the index in the list. |
| | |
| | None |
| | If no perpendicular vector could be found. |
| | """ |
| | if not isinstance(edgeslist, list): |
| | try: |
| | edgeslist = edgeslist.Edges |
| | except AttributeError: |
| | print("Doesn't have 'Edges'") |
| | return None |
| |
|
| | if force is None: |
| | valid = None |
| | for edge in edgeslist: |
| | dist = findDistance(point, edge, strict=True) |
| | if dist: |
| | if not valid: |
| | valid = [dist, edgeslist.index(edge)] |
| | else: |
| | if dist.Length < valid[0].Length: |
| | valid = [dist, edgeslist.index(edge)] |
| | return valid |
| | else: |
| | edge = edgeslist[force] |
| | dist = findDistance(point, edge) |
| | if dist: |
| | return [dist, force] |
| | else: |
| | return None |
| | return None |
| |
|
| |
|
| | def findDistance(point, edge, strict=False): |
| | """Return a vector from the point to its closest point on the edge. |
| | |
| | If `strict` is `True`, the vector will be returned |
| | only if its endpoint lies on the `edge`. |
| | Edge can also be a list of 2 points. |
| | """ |
| | if isinstance(point, App.Vector): |
| | if isinstance(edge, list): |
| | segment = edge[1].sub(edge[0]) |
| | chord = edge[0].sub(point) |
| | norm = segment.cross(chord) |
| | perp = segment.cross(norm) |
| | dist = DraftVecUtils.project(chord, perp) |
| |
|
| | if not dist: |
| | return None |
| |
|
| | newpoint = point.add(dist) |
| |
|
| | if dist.Length == 0: |
| | return None |
| |
|
| | if strict: |
| | s1 = newpoint.sub(edge[0]) |
| | s2 = newpoint.sub(edge[1]) |
| | if s1.Length <= segment.Length and s2.Length <= segment.Length: |
| | return dist |
| | else: |
| | return None |
| | else: |
| | return dist |
| |
|
| | elif geomType(edge) == "Line": |
| | segment = vec(edge) |
| | chord = edge.Vertexes[0].Point.sub(point) |
| | norm = segment.cross(chord) |
| | perp = segment.cross(norm) |
| | dist = DraftVecUtils.project(chord, perp) |
| |
|
| | if not dist: |
| | return None |
| |
|
| | newpoint = point.add(dist) |
| |
|
| | if dist.Length == 0: |
| | return None |
| |
|
| | if strict: |
| | s1 = newpoint.sub(edge.Vertexes[0].Point) |
| | s2 = newpoint.sub(edge.Vertexes[-1].Point) |
| | if s1.Length <= segment.Length and s2.Length <= segment.Length: |
| | return dist |
| | else: |
| | return None |
| | else: |
| | return dist |
| |
|
| | elif geomType(edge) == "Circle": |
| | ve1 = edge.Vertexes[0].Point |
| | if len(edge.Vertexes) > 1: |
| | ve2 = edge.Vertexes[-1].Point |
| | else: |
| | ve2 = None |
| | center = edge.Curve.Center |
| | segment = center.sub(point) |
| |
|
| | if segment.Length == 0: |
| | return None |
| |
|
| | ratio = (segment.Length - edge.Curve.Radius) / segment.Length |
| | dist = segment.multiply(ratio) |
| | newpoint = App.Vector.add(point, dist) |
| |
|
| | if dist.Length == 0: |
| | return None |
| |
|
| | if strict and ve2: |
| | |
| | |
| | ang1 = DraftVecUtils.angle(ve1.sub(center)) |
| | ang2 = DraftVecUtils.angle(ve2.sub(center)) |
| | angpt = DraftVecUtils.angle(newpoint.sub(center)) |
| | if ang1 >= ang2: |
| | if ang1 >= angpt and angpt >= ang2: |
| | return dist |
| | else: |
| | return None |
| | elif ang1 >= angpt or angpt >= ang2: |
| | return dist |
| | else: |
| | return None |
| | else: |
| | return dist |
| |
|
| | elif geomType(edge) == "BSplineCurve" or geomType(edge) == "BezierCurve": |
| | try: |
| | pr = edge.Curve.parameter(point) |
| | np = edge.Curve.value(pr) |
| | dist = np.sub(point) |
| | except Part.OCCError: |
| | print("DraftGeomUtils: Unable to get curve parameter " "for point ", point) |
| | return None |
| | else: |
| | return dist |
| | else: |
| | print("DraftGeomUtils: Couldn't project point") |
| | return None |
| | else: |
| | print("DraftGeomUtils: Couldn't project point") |
| | return None |
| |
|
| |
|
| | def get_spline_normal(edge, tol=-1): |
| | """Find the normal of a BSpline edge.""" |
| |
|
| | if edge.isNull(): |
| | return None |
| |
|
| | if is_straight_line(shape, tol): |
| | return None |
| |
|
| | plane = edge.findPlane(tol) |
| | if plane: |
| | normal = plane.Axis |
| | return normal |
| | else: |
| | return None |
| |
|
| |
|
| | def get_shape_normal(shape): |
| | """Find the normal of a shape or list of points or colinear edges, if possible.""" |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | if shape.isNull(): |
| | return None |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | shape_rot = shape.Placement.Rotation |
| | plane = shape.findPlane() |
| |
|
| | if plane is None: |
| | if not is_straight_line(shape): |
| | return None |
| | start_edge = shape.Edges[0] |
| | x_vec = start_edge.tangentAt( |
| | start_edge.FirstParameter |
| | ) |
| | local_x_vec = shape_rot.inverted().multVec(x_vec) |
| | local_rot = App.Rotation(local_x_vec, App.Vector(0, 1, 0), App.Vector(0, 0, 1), "XZY") |
| | |
| | |
| | return shape_rot.multiply(local_rot).multVec(App.Vector(0, 0, 1)) |
| |
|
| | normal = plane.Axis |
| | shape_normal = shape_rot.multVec(App.Vector(0, 0, 1)) |
| | |
| | |
| | |
| | if normal.getAngle(shape_normal) > math.pi / 2: |
| | normal = normal.negative() |
| | return normal |
| |
|
| |
|
| | def get_normal(shape, tol=-1): |
| | """Find the normal of a shape or list of points, if possible.""" |
| |
|
| | |
| | if isinstance(shape, (list, tuple)): |
| | if len(shape) <= 2: |
| | return None |
| | else: |
| | poly = Part.makePolygon(shape) |
| | if is_straight_line(poly, tol): |
| | return None |
| |
|
| | plane = poly.findPlane(tol) |
| | if plane: |
| | normal = plane.Axis |
| | return normal |
| | else: |
| | return None |
| |
|
| | |
| | if shape.isNull(): |
| | return None |
| |
|
| | if is_straight_line(shape, tol): |
| | return None |
| | else: |
| | plane = find_plane(shape, tol) |
| | if plane: |
| | normal = plane.Axis |
| | else: |
| | return None |
| |
|
| | |
| | if App.GuiUp: |
| | view = gui_utils.get_3d_view() |
| | if view is not None: |
| | v_dir = view.getViewDirection() |
| | if normal.getAngle(v_dir) < 0.78: |
| | normal = normal.negative() |
| |
|
| | return normal |
| |
|
| |
|
| | def getRotation(v1, v2=App.Vector(0, 0, 1)): |
| | """Get the rotation Quaternion between 2 vectors.""" |
| | if (v1.dot(v2) > 0.999999) or (v1.dot(v2) < -0.999999): |
| | |
| | return None |
| |
|
| | axis = v1.cross(v2) |
| | axis.normalize() |
| | |
| | angle = math.degrees(DraftVecUtils.angle(v1, v2, axis)) |
| | return App.Rotation(axis, angle) |
| |
|
| |
|
| | def is_planar(shape, tol=-1): |
| | """Return True if the given shape or list of points is planar.""" |
| |
|
| | |
| | if isinstance(shape, list): |
| | if len(shape) <= 3: |
| | return True |
| | else: |
| | poly = Part.makePolygon(shape) |
| | if is_straight_line(poly, tol): |
| | return True |
| |
|
| | plane = poly.findPlane(tol) |
| | if plane: |
| | return True |
| | else: |
| | return False |
| |
|
| | |
| | if shape.isNull(): |
| | return False |
| |
|
| | |
| | if shape.ShapeType == "Vertex": |
| | return True |
| |
|
| | if is_straight_line(shape, tol): |
| | return True |
| |
|
| | plane = find_plane(shape, tol) |
| | if plane: |
| | return True |
| | else: |
| | return False |
| |
|
| |
|
| | def is_straight_line(shape, tol=-1): |
| | """Return True if shape is a straight line. |
| | function used in other methods because Part.Shape.findPlane assign a |
| | plane and normal to straight wires creating privileged directions |
| | and to deal with straight wires with overlapped edges.""" |
| |
|
| | if shape.isNull(): |
| | return False |
| |
|
| | if len(shape.Faces) != 0: |
| | return False |
| |
|
| | if len(shape.Edges) == 0: |
| | return False |
| |
|
| | if len(shape.Edges) >= 1: |
| | start_edge = shape.Edges[0] |
| | dir_start_edge = start_edge.tangentAt(start_edge.FirstParameter) |
| | point_start_edge = start_edge.firstVertex().Point |
| |
|
| | |
| | if tol <= 0: |
| | err = shape.globalTolerance(tol) |
| | else: |
| | err = tol |
| |
|
| | for edge in shape.Edges: |
| | first_point = edge.firstVertex().Point |
| | last_point = edge.lastVertex().Point |
| | dir_edge = edge.tangentAt(edge.FirstParameter) |
| | |
| | |
| | |
| | if ( |
| | abs(edge.Length - first_point.distanceToPoint(last_point)) > err |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | or first_point.distanceToLine(point_start_edge, dir_start_edge) > err |
| | or dir_start_edge.cross(dir_edge).Length > err |
| | ): |
| | return False |
| |
|
| | return True |
| |
|
| |
|
| | def are_coplanar(shape_a, shape_b, tol=-1): |
| | """Return True if exist a plane containing both shapes.""" |
| |
|
| | if shape_a.isNull() or shape_b.isNull(): |
| | return False |
| |
|
| | if not is_planar(shape_a, tol) or not is_planar(shape_b, tol): |
| | return False |
| |
|
| | if shape_a.isEqual(shape_b): |
| | return True |
| |
|
| | plane_a = find_plane(shape_a, tol) |
| | plane_b = find_plane(shape_b, tol) |
| |
|
| | |
| | if tol <= 0: |
| | err = 1e-7 |
| | else: |
| | err = tol |
| |
|
| | if plane_a and plane_b: |
| | normal_a = plane_a.Axis |
| | normal_b = plane_b.Axis |
| | proj = plane_a.projectPoint(plane_b.Position) |
| | if normal_a.cross(normal_b).Length > err or plane_b.Position.sub(proj).Length > err: |
| | return False |
| | else: |
| | return True |
| |
|
| | elif plane_a and not plane_b: |
| | normal_a = plane_a.Axis |
| | for vertex in shape_b.Vertexes: |
| | dir_ver_b = vertex.Point.sub(plane_a.Position).normalize() |
| | if abs(normal_a.dot(dir_ver_b)) > err: |
| | proj = plane_a.projectPoint(vertex.Point) |
| | if vertex.Point.sub(proj).Length > err: |
| | return False |
| | return True |
| |
|
| | elif plane_b and not plane_a: |
| | normal_b = plane_b.Axis |
| | for vertex in shape_a.Vertexes: |
| | dir_ver_a = vertex.Point.sub(plane_b.Position).normalize() |
| | if abs(normal_b.dot(dir_ver_a)) > err: |
| | proj = plane_b.projectPoint(vertex.Point) |
| | if vertex.Point.sub(proj).Length > err: |
| | return False |
| | return True |
| | |
| | else: |
| | points_a = [vertex.Point for vertex in shape_a.Vertexes] |
| | points_b = [vertex.Point for vertex in shape_b.Vertexes] |
| | poly = Part.makePolygon(points_a + points_b) |
| | if is_planar(poly, tol): |
| | return True |
| | else: |
| | return False |
| |
|
| |
|
| | def get_spline_surface_normal(shape, tol=-1): |
| | """Check if shape formed by BSpline surfaces is planar and get normal. |
| | If shape is not planar return None.""" |
| |
|
| | if shape.isNull(): |
| | return None |
| |
|
| | if len(shape.Faces) == 0: |
| | return None |
| |
|
| | |
| | if tol <= 0: |
| | err = shape.globalTolerance(tol) |
| | else: |
| | err = tol |
| |
|
| | first_surf = shape.Faces[0].Surface |
| |
|
| | if not first_surf.isPlanar(tol): |
| | return None |
| |
|
| | |
| | u0, u1, v0, v1 = first_surf.bounds() |
| | u = (u0 + u1) / 2 |
| | v = (v0 + v1) / 2 |
| | first_normal = first_surf.normal(u, v) |
| | |
| | for face in shape.Faces: |
| | surf = face.Surface |
| | if not surf.isPlanar(tol): |
| | return None |
| | u0, u1, v0, v1 = surf.bounds() |
| | u = (u0 + u1) / 2 |
| | v = (v0 + v1) / 2 |
| | surf_normal = surf.normal(u, v) |
| | if first_normal.cross(surf_normal).Length > err: |
| | return None |
| |
|
| | normal = first_normal |
| |
|
| | return normal |
| |
|
| |
|
| | def find_plane(shape, tol=-1): |
| | """Find the plane containing the shape if possible. |
| | Use this function as a workaround due Part.Shape.findPlane |
| | fail to find plane on BSpline surfaces.""" |
| |
|
| | if shape.isNull(): |
| | return None |
| |
|
| | if shape.ShapeType == "Vertex": |
| | return None |
| |
|
| | if is_straight_line(shape, tol): |
| | return None |
| |
|
| | plane = shape.findPlane(tol) |
| | if plane: |
| | return plane |
| | elif len(shape.Faces) >= 1: |
| | |
| | normal = get_spline_surface_normal(shape, tol) |
| | if normal: |
| | position = shape.CenterOfMass |
| | return Part.Plane(position, normal) |
| | else: |
| | return None |
| | else: |
| | return None |
| |
|
| |
|
| | def calculatePlacement(shape): |
| | """Return a placement located in the center of gravity of the shape. |
| | |
| | If the given shape is planar, return a placement located at the center |
| | of gravity of the shape, and oriented towards the shape's normal. |
| | Otherwise, it returns a null placement. |
| | """ |
| | if not is_planar(shape): |
| | return App.Placement() |
| |
|
| | pos = shape.BoundBox.Center |
| | norm = get_normal(shape) |
| | |
| | if norm is None: |
| | norm = App.Vector(0, 0, 1) |
| | pla = App.Placement() |
| | pla.Base = pos |
| | r = getRotation(norm) |
| |
|
| | if r: |
| | pla.Rotation = r |
| |
|
| | return pla |
| |
|
| |
|
| | def mirror(point, edge): |
| | """Find mirror point relative to an edge.""" |
| | normPoint = point.add(findDistance(point, edge, False)) |
| |
|
| | if normPoint: |
| | normPoint_point = App.Vector.sub(point, normPoint) |
| | normPoint_refl = normPoint_point.negative() |
| | refl = App.Vector.add(normPoint, normPoint_refl) |
| | return refl |
| | else: |
| | return None |
| |
|
| |
|
| | def mirror_matrix(mtx, pos, nor): |
| | """Return a mirrored copy of a matrix. |
| | |
| | Parameters |
| | ---------- |
| | mtx: Base::Matrix |
| | Matrix. |
| | pos: Base::Vector3 |
| | Point on mirror plane. |
| | nor: Base::Vector3 |
| | Normal of mirror plane. |
| | |
| | Returns |
| | ------- |
| | Base::Matrix |
| | """ |
| | |
| | |
| | mtx_copy = App.Matrix(mtx) |
| | mtx_copy.move(-pos) |
| | mtx_copy.scale(-1) |
| | mtx_copy = App.Rotation(nor, 180) * mtx_copy |
| | mtx_copy.move(pos) |
| | return mtx_copy |
| |
|
| |
|
| | def uv_vectors_from_face(face, vec_z=App.Vector(0, 0, 1), tol=-1): |
| | """Return the u and v vectors of a planar face. |
| | |
| | It is up to the calling function to ensure the face is planar. |
| | |
| | If the u vector matches +/-vec_z, or the v vector matches -vec_z, the |
| | vectors are rotated to ensure the v vector matches +vec_z. |
| | |
| | Parameters |
| | ---------- |
| | face: Part.Face |
| | Face. |
| | vec_z: Base::Vector3, optional |
| | Defaults to Vector(0, 0, 1). |
| | Z axis vector used for reference. |
| | Is replaced by Vector(0, 0, 1) if it matches the +/-normal of the face. |
| | tol: float, optional |
| | Defaults to -1. |
| | Internal tolerance. 1e-7 is used if tol <=0. |
| | |
| | Returns |
| | ------- |
| | tuple |
| | U and v vector (Base::Vector3). |
| | """ |
| | err = 1e-7 if tol <= 0 else tol |
| | if not vec_z.isEqual(App.Vector(0, 0, 1), err): |
| | nor = face.normalAt(0, 0) |
| | if vec_z.isEqual(nor, err) or vec_z.isEqual(nor.negative(), err): |
| | vec_z = App.Vector(0, 0, 1) |
| | vec_u, vec_v = face.tangentAt(0, 0) |
| | if face.Orientation == "Reversed": |
| | vec_u, vec_v = vec_v, vec_u |
| | if vec_v.isEqual(vec_z.negative(), err): |
| | vec_u, vec_v = vec_u.negative(), vec_v.negative() |
| | elif vec_u.isEqual(vec_z, err): |
| | vec_u, vec_v = vec_v.negative(), vec_u |
| | elif vec_u.isEqual(vec_z.negative(), err): |
| | vec_u, vec_v = vec_v, vec_u.negative() |
| | return vec_u, vec_v |
| |
|
| |
|
| | def placement_from_face(face, vec_z=App.Vector(0, 0, 1), rotated=False, tol=-1): |
| | """Return a placement from the center of gravity, and the u and v vectors of a planar face. |
| | |
| | It is up to the calling function to ensure the face is planar. |
| | |
| | Parameters |
| | ---------- |
| | face: Part.Face |
| | Face. |
| | vec_z: Base::Vector3, optional |
| | Defaults to Vector(0, 0, 1). |
| | Z axis vector used for reference. |
| | Is replaced by Vector(0, 0, 1) if it matches the +/-normal of the face. |
| | rotated: bool, optional |
| | Defaults to `False`. |
| | If `False` the v vector of the face defines the Y axis of the placement. |
| | If `True` the -v vector of the face defines the Z axis of the placement |
| | (used by Arch_Window). |
| | The u vector defines the X axis in both cases. |
| | tol: float, optional |
| | Defaults to -1. |
| | Internal tolerance. 1e-7 is used if tol <=0. |
| | |
| | Returns |
| | ------- |
| | Base::Placement |
| | |
| | See also |
| | -------- |
| | DraftGeomUtils.uv_vectors_from_face |
| | """ |
| | pt_pos = face.CenterOfGravity |
| | vec_u, vec_v = uv_vectors_from_face(face, vec_z, tol) |
| | if rotated: |
| | return App.Placement(pt_pos, App.Rotation(vec_u, App.Vector(), vec_v.negative(), "XZY")) |
| | else: |
| | return App.Placement(pt_pos, App.Rotation(vec_u, vec_v, App.Vector(), "XYZ")) |
| |
|
| |
|
| | def placement_from_points(pt_pos, pt_x, pt_y, as_vectors=False, tol=-1): |
| | """Return a placement from 3 points defining an origin, an X axis and a Y axis. |
| | |
| | If the vectors calculated from the arguments are too short or parallel, |
| | the returned placement will have a default rotation. |
| | |
| | Parameters |
| | ---------- |
| | pt_pos: Base::Vector3 |
| | Origin (Base of Placement). |
| | pt_x: Base::Vector3 |
| | Point on positive X axis. Or X axis vector if as_vectors is `True`. |
| | pt_y: Base::Vector3 |
| | Point on positive Y axis. Or Y axis vector if as_vectors is `True`. |
| | as_vectors: bool, optional |
| | Defaults to `False`. |
| | If `True` treat pt_x and pt_y as axis vectors. |
| | tol: float, optional |
| | Defaults to -1. |
| | Internal tolerance. 1e-7 is used if tol <=0. |
| | |
| | Returns |
| | ------- |
| | Base::Placement |
| | |
| | See also |
| | -------- |
| | DraftGeomUtils.getRotation |
| | DraftVecUtils.getRotation |
| | """ |
| | err = 1e-7 if tol <= 0 else tol |
| | if as_vectors is False: |
| | vec_u = pt_x - pt_pos |
| | vec_v = pt_y - pt_pos |
| | else: |
| | vec_u = App.Vector(pt_x) |
| | vec_v = App.Vector(pt_y) |
| |
|
| | if vec_u.Length < err or vec_v.Length < err: |
| | rot = App.Rotation() |
| | else: |
| | vec_u.normalize() |
| | vec_v.normalize() |
| | if vec_u.isEqual(vec_v, err) or vec_u.isEqual(vec_v.negative(), err): |
| | rot = App.Rotation() |
| | else: |
| | rot = App.Rotation(vec_u, vec_v, App.Vector(), "XYZ") |
| |
|
| | return App.Placement(pt_pos, rot) |
| |
|
| |
|
| | |
| | |
| | def distance_to_plane(point, base, normal): |
| | """Return the signed distance from a plane to a point. |
| | |
| | The distance is positive if the point lies on the +normal side of the plane. |
| | |
| | Parameters |
| | ---------- |
| | point: Base::Vector3 |
| | Point to project. |
| | base: Base::Vector3 |
| | Point on plane. |
| | normal: Base::Vector3 |
| | Normal of plane. |
| | |
| | Returns |
| | ------- |
| | float |
| | """ |
| | return (point - base).dot(normal) |
| |
|
| |
|
| | |
| | |
| | def project_point_on_plane(point, base, normal, direction=None, force_projection=False, tol=-1): |
| | """Project a point onto a plane. |
| | |
| | Parameters |
| | ---------- |
| | point: Base::Vector3 |
| | Point to project. |
| | base: Base::Vector3 |
| | Point on plane. |
| | normal: Base::Vector3 |
| | Normal of plane. |
| | direction: Base::Vector3, optional |
| | Defaults to `None` in which case the normal is used. |
| | Direction of projection. |
| | force_projection: Bool, optional |
| | Defaults to `False`. |
| | If `True` forces the projection if the deviation between the direction |
| | and the normal is less than tol from the orthogonality. The direction |
| | of projection is then modified to a tol deviation between the direction |
| | and the orthogonal. |
| | tol: float, optional |
| | Defaults to -1. |
| | Internal tolerance. 1e-7 is used if tol <=0. |
| | |
| | Returns |
| | ------- |
| | Base::Vector3 or `None` |
| | """ |
| | err = 1e-7 if tol <= 0 else tol |
| | normal = App.Vector(normal).normalize() |
| | if direction is None: |
| | direction = normal |
| | else: |
| | direction = App.Vector(direction).normalize() |
| |
|
| | cos = direction.dot(normal) |
| | delta_ax_proj = (point - base).dot(normal) |
| | |
| | if abs(cos) < err: |
| | if force_projection: |
| | cos = math.copysign(err, delta_ax_proj) |
| | direction = normal.cross(direction).cross(normal) - cos * normal |
| | else: |
| | return None |
| |
|
| | return point - delta_ax_proj / cos * direction |
| |
|
| |
|
| | |
| |
|
| | getSplineNormal = get_spline_normal |
| |
|
| | getNormal = get_normal |
| |
|
| | isPlanar = is_planar |
| |
|
| |
|
| | |
| |
|