The trick is really to over-extend edges before creationg shapes for the common operation, and trying to avoid alignment of the edge with the cone's seam.
370 lines
16 KiB
Python
370 lines
16 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
|
|
# * *
|
|
# * 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. *
|
|
# * *
|
|
# * This program 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 this program; if not, write to the Free Software *
|
|
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
# * USA *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
import FreeCAD
|
|
import math
|
|
import Part
|
|
import Path
|
|
|
|
from FreeCAD import Vector
|
|
|
|
class Side:
|
|
"""Class to determine and define the side a Path is on, or Vectors are in relation to each other."""
|
|
Left = +1
|
|
Right = -1
|
|
Straight = 0
|
|
On = 0
|
|
|
|
@classmethod
|
|
def toString(cls, side):
|
|
"""(side)
|
|
Returns a string representation of the enum value."""
|
|
if side == cls.Left:
|
|
return 'Left'
|
|
if side == cls.Right:
|
|
return 'Right'
|
|
return 'On'
|
|
|
|
@classmethod
|
|
def of(cls, ptRef, pt):
|
|
"""(ptRef, pt)
|
|
Determine the side of pt in relation to ptRef.
|
|
If both Points are viewed as vectors with their origin in (0,0,0)
|
|
then the two vectors are either form a straigt line (On) or pt
|
|
lies in the left or right hemishpere in regards to ptRef."""
|
|
d = -ptRef.x*pt.y + ptRef.y*pt.x
|
|
if d < 0:
|
|
return cls.Left
|
|
if d > 0:
|
|
return cls.Right
|
|
return cls.Straight
|
|
|
|
class PathGeom:
|
|
"""Class to transform Path Commands into Edges and Wire and back again.
|
|
The interface might eventuallly become part of Path itself."""
|
|
CmdMoveRapid = ['G0', 'G00']
|
|
CmdMoveStraight = ['G1', 'G01']
|
|
CmdMoveCW = ['G2', 'G02']
|
|
CmdMoveCCW = ['G3', 'G03']
|
|
CmdMoveArc = CmdMoveCW + CmdMoveCCW
|
|
CmdMove = CmdMoveStraight + CmdMoveArc
|
|
|
|
@classmethod
|
|
def isRoughly(cls, float1, float2, error=0.0000001):
|
|
"""(float1, float2, [error=0.0000001])
|
|
Returns true if the two values are the same within a given error."""
|
|
return math.fabs(float1 - float2) <= error
|
|
|
|
@classmethod
|
|
def pointsCoincide(cls, p1, p2, error=0.0000001):
|
|
"""(p1, p2, [error=0.0000001])
|
|
Return True if two points are roughly identical (see also isRoughly)."""
|
|
return cls.isRoughly(p1.x, p2.x, error) and cls.isRoughly(p1.y, p2.y, error) and cls.isRoughly(p1.z, p2.z, error)
|
|
|
|
@classmethod
|
|
def edgesMatch(cls, e0, e1, error=0.0000001):
|
|
"""(e0, e1, [error=0.0000001]
|
|
Return true if the edges start and end at the same point and have the same type of curve."""
|
|
if type(e0.Curve) != type(e1.Curve):
|
|
return False
|
|
if not cls.pointsCoincide(e0.valueAt(e0.FirstParameter), e1.valueAt(e1.FirstParameter)):
|
|
return False
|
|
if not cls.pointsCoincide(e0.valueAt(e0.LastParameter), e1.valueAt(e1.LastParameter)):
|
|
return False
|
|
return True
|
|
|
|
@classmethod
|
|
def edgeConnectsTo(cls, edge, vector):
|
|
"""(edge, vector)
|
|
Returns True if edge connects to given vector."""
|
|
return cls.pointsCoincide(edge.valueAt(edge.FirstParameter), vector) or cls.pointsCoincide(edge.valueAt(edge.LastParameter), vector)
|
|
|
|
@classmethod
|
|
def getAngle(cls, vector):
|
|
"""(vector)
|
|
Returns the angle [-pi,pi] of a vector using the X-axis as the reference.
|
|
Positive angles for vertexes in the upper hemishpere (positive y values)
|
|
and negative angles for the lower hemishpere."""
|
|
a = vector.getAngle(FreeCAD.Vector(1,0,0))
|
|
if vector.y < 0:
|
|
return -a
|
|
return a
|
|
|
|
@classmethod
|
|
def diffAngle(cls, a1, a2, direction = 'CW'):
|
|
"""(a1, a2, [direction='CW'])
|
|
Returns the difference between two angles (a1 -> a2) into a given direction."""
|
|
if direction == 'CW':
|
|
while a1 < a2:
|
|
a1 += 2*math.pi
|
|
a = a1 - a2
|
|
else:
|
|
while a2 < a1:
|
|
a2 += 2*math.pi
|
|
a = a2 - a1
|
|
return a
|
|
|
|
@classmethod
|
|
def commandEndPoint(cls, cmd, defaultPoint = Vector(), X='X', Y='Y', Z='Z'):
|
|
"""(cmd, [defaultPoint=Vector()], [X='X'], [Y='Y'], [Z='Z'])
|
|
Extracts the end point from a Path Command."""
|
|
x = cmd.Parameters.get(X, defaultPoint.x)
|
|
y = cmd.Parameters.get(Y, defaultPoint.y)
|
|
z = cmd.Parameters.get(Z, defaultPoint.z)
|
|
return FreeCAD.Vector(x, y, z)
|
|
|
|
@classmethod
|
|
def xy(cls, point):
|
|
"""(point)
|
|
Convenience function to return the projection of the Vector in the XY-plane."""
|
|
return Vector(point.x, point.y, 0)
|
|
|
|
@classmethod
|
|
def cmdsForEdge(cls, edge, flip = False, useHelixForBSpline = True):
|
|
"""(edge, flip = False, useHelixForBSpline = True) -> List(Path.Command)
|
|
Returns a list of Path.Command representing the given edge.
|
|
If flip is True the edge is considered to be backwards.
|
|
If useHelixForBSpline is True an Edge based on a BSplineCurve is considered
|
|
to represent a helix and results in G2 or G3 command. Otherwise edge has
|
|
no direct Path.Command mapping and will be approximated by straight segments.
|
|
Approximation is also the approach for edges that are neither straight lines
|
|
nor arcs (nor helixes)."""
|
|
pt = edge.valueAt(edge.LastParameter) if not flip else edge.valueAt(edge.FirstParameter)
|
|
params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z}
|
|
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
|
|
commands = [Path.Command('G1', params)]
|
|
else:
|
|
if not flip:
|
|
p1 = edge.valueAt(edge.FirstParameter)
|
|
p3 = pt
|
|
else:
|
|
p1 = pt
|
|
p3 = edge.valueAt(edge.LastParameter)
|
|
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2)
|
|
if type(edge.Curve) == Part.Circle or (useHelixForBSpline and type(edge.Curve) == Part.BSplineCurve):
|
|
if Side.Left == Side.of(p2 - p1, p3 - p2):
|
|
cmd = 'G3'
|
|
else:
|
|
cmd = 'G2'
|
|
#print("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z))
|
|
pd = Part.Circle(PathGeom.xy(p1), PathGeom.xy(p2), PathGeom.xy(p3)).Center
|
|
|
|
pa = PathGeom.xy(p1)
|
|
pb = PathGeom.xy(p2)
|
|
pc = PathGeom.xy(p3)
|
|
#print("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (pa.x, pa.y, pa.z, pc.x, pc.y, pc.z))
|
|
#print("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (pb.x, pb.y, pb.z, pd.x, pd.y, pd.z))
|
|
offset = Part.Circle(PathGeom.xy(p1), PathGeom.xy(p2), PathGeom.xy(p3)).Center - p1
|
|
#print("**** (%.2f, %.2f, %.2f)" % (offset.x, offset.y, offset.z))
|
|
params.update({'I': offset.x, 'J': offset.y, 'K': (p3.z - p1.z)/2})
|
|
commands = [ Path.Command(cmd, params) ]
|
|
else:
|
|
eStraight = Part.Edge(Part.LineSegment(p1, p3))
|
|
esP2 = eStraight.valueAt((eStraight.FirstParameter + eStraight.LastParameter)/2)
|
|
deviation = (p2 - esP2).Length
|
|
if cls.isRoughly(deviation, 0):
|
|
return [ Path.Command('G1', {'X': p3.x, 'Y': p3.y, 'Z': p3.z}) ]
|
|
# at this point pixellation is all we can do
|
|
commands = []
|
|
segments = int(math.ceil((deviation / eStraight.Length) * 1000))
|
|
#print("**** pixellation with %d segments" % segments)
|
|
dParameter = (edge.LastParameter - edge.FirstParameter) / segments
|
|
for i in range(0, segments):
|
|
if flip:
|
|
p = edge.valueAt(edge.LastParameter - (i + 1) * dParameter)
|
|
else:
|
|
p = edge.valueAt(edge.FirstParameter + (i + 1) * dParameter)
|
|
cmd = Path.Command('G1', {'X': p.x, 'Y': p.y, 'Z': p.z})
|
|
#print("***** %s" % cmd)
|
|
commands.append(cmd)
|
|
#print commands
|
|
return commands
|
|
|
|
@classmethod
|
|
def edgeForCmd(cls, cmd, startPoint):
|
|
"""(cmd, startPoint).
|
|
Returns an Edge representing the given command, assuming a given startPoint."""
|
|
|
|
endPoint = cls.commandEndPoint(cmd, startPoint)
|
|
if (cmd.Name in cls.CmdMoveStraight) or (cmd.Name in cls.CmdMoveRapid):
|
|
if cls.pointsCoincide(startPoint, endPoint):
|
|
return None
|
|
return Part.Edge(Part.LineSegment(startPoint, endPoint))
|
|
|
|
if cmd.Name in cls.CmdMoveArc:
|
|
center = startPoint + cls.commandEndPoint(cmd, Vector(0,0,0), 'I', 'J', 'K')
|
|
A = cls.xy(startPoint - center)
|
|
B = cls.xy(endPoint - center)
|
|
d = -B.x * A.y + B.y * A.x
|
|
|
|
if d == 0:
|
|
# we're dealing with half a circle here
|
|
angle = cls.getAngle(A) + math.pi/2
|
|
if cmd.Name in cls.CmdMoveCW:
|
|
angle -= math.pi
|
|
else:
|
|
C = A + B
|
|
angle = cls.getAngle(C)
|
|
|
|
R = A.Length
|
|
#print("arc: p1=(%.2f, %.2f) p2=(%.2f, %.2f) -> center=(%.2f, %.2f)" % (startPoint.x, startPoint.y, endPoint.x, endPoint.y, center.x, center.y))
|
|
#print("arc: A=(%.2f, %.2f) B=(%.2f, %.2f) -> d=%.2f" % (A.x, A.y, B.x, B.y, d))
|
|
#print("arc: R=%.2f angle=%.2f" % (R, angle/math.pi))
|
|
if startPoint.z == endPoint.z:
|
|
midPoint = center + FreeCAD.Vector(math.cos(angle), math.sin(angle), 0) * R
|
|
return Part.Edge(Part.Arc(startPoint, midPoint, endPoint))
|
|
|
|
# It's a Helix
|
|
#print('angle: A=%.2f B=%.2f' % (cls.getAngle(A)/math.pi, cls.getAngle(B)/math.pi))
|
|
if cmd.Name in cls.CmdMoveCW:
|
|
cw = True
|
|
else:
|
|
cw = False
|
|
angle = cls.diffAngle(cls.getAngle(A), cls.getAngle(B), 'CW' if cw else 'CCW')
|
|
height = endPoint.z - startPoint.z
|
|
pitch = height * math.fabs(2 * math.pi / angle)
|
|
if angle > 0:
|
|
cw = not cw
|
|
#print("Helix: R=%.2f h=%.2f angle=%.2f pitch=%.2f" % (R, height, angle/math.pi, pitch))
|
|
helix = Part.makeHelix(pitch, height, R, 0, not cw)
|
|
helix.rotate(Vector(), Vector(0,0,1), 180 * cls.getAngle(A) / math.pi)
|
|
e = helix.Edges[0]
|
|
helix.translate(startPoint - e.valueAt(e.FirstParameter))
|
|
return helix.Edges[0]
|
|
return None
|
|
|
|
@classmethod
|
|
def wireForPath(cls, path, startPoint = FreeCAD.Vector(0, 0, 0)):
|
|
"""(path, [startPoint=Vector(0,0,0)])
|
|
Returns a wire representing all move commands found in the given path."""
|
|
edges = []
|
|
rapid = []
|
|
if hasattr(path, "Commands"):
|
|
for cmd in path.Commands:
|
|
edge = cls.edgeForCmd(cmd, startPoint)
|
|
if edge:
|
|
if cmd.Name in cls.CmdMoveRapid:
|
|
rapid.append(edge)
|
|
edges.append(edge)
|
|
startPoint = cls.commandEndPoint(cmd, startPoint)
|
|
return (Part.Wire(edges), rapid)
|
|
|
|
@classmethod
|
|
def wiresForPath(cls, path, startPoint = FreeCAD.Vector(0, 0, 0)):
|
|
"""(path, [startPoint=Vector(0,0,0)])
|
|
Returns a collection of wires, each representing a continuous cutting Path in path."""
|
|
wires = []
|
|
if hasattr(path, "Commands"):
|
|
edges = []
|
|
for cmd in path.Commands:
|
|
if cmd.Name in cls.CmdMove:
|
|
edges.append(cls.edgeForCmd(cmd, startPoint))
|
|
startPoint = cls.commandEndPoint(cmd, startPoint)
|
|
elif cmd.Name in cls.CmdMoveRapid:
|
|
wires.append(Part.Wire(edges))
|
|
edges = []
|
|
startPoint = cls.commandEndPoint(cmd, startPoint)
|
|
if edges:
|
|
wires.append(Part.Wire(edges))
|
|
return wires
|
|
|
|
@classmethod
|
|
def arcToHelix(cls, edge, z0, z1):
|
|
"""(edge, z0, z1)
|
|
Assuming edge is an arc it'll return a helix matching the arc starting at z0 and rising/falling to z1."""
|
|
|
|
|
|
p1 = edge.valueAt(edge.FirstParameter)
|
|
p2 = edge.valueAt(edge.LastParameter)
|
|
|
|
cmd = cls.cmdsForEdge(edge)[0]
|
|
params = cmd.Parameters
|
|
params.update({'Z': z1, 'K': (z1 - z0)/2})
|
|
command = Path.Command(cmd.Name, params)
|
|
|
|
#print("- (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f): %.2f:%.2f" % (edge.Vertexes[0].X, edge.Vertexes[0].Y, edge.Vertexes[0].Z, edge.Vertexes[1].X, edge.Vertexes[1].Y, edge.Vertexes[1].Z, z0, z1))
|
|
#print("- %s -> %s" % (cmd, command))
|
|
|
|
return cls.edgeForCmd(command, FreeCAD.Vector(p1.x, p1.y, z0))
|
|
|
|
|
|
@classmethod
|
|
def helixToArc(cls, edge, z = 0):
|
|
"""(edge, z=0)
|
|
Returns the projection of the helix onto the XY-plane with a given offset."""
|
|
p1 = edge.valueAt(edge.FirstParameter)
|
|
p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2)
|
|
p3 = edge.valueAt(edge.LastParameter)
|
|
p01 = FreeCAD.Vector(p1.x, p1.y, z)
|
|
p02 = FreeCAD.Vector(p2.x, p2.y, z)
|
|
p03 = FreeCAD.Vector(p3.x, p3.y, z)
|
|
return Part.Edge(Part.Arc(p01, p02, p03))
|
|
|
|
@classmethod
|
|
def splitArcAt(cls, edge, pt):
|
|
"""(edge, pt)
|
|
Returns a list of 2 edges which together form the original arc split at the given point.
|
|
The Vector pt has to represnt a point on the given arc."""
|
|
p1 = edge.valueAt(edge.FirstParameter)
|
|
p2 = pt
|
|
p3 = edge.valueAt(edge.LastParameter)
|
|
edges = []
|
|
|
|
p = edge.Curve.parameter(p2)
|
|
#print("splitArcAt(%.2f, %.2f, %.2f): %.2f - %.2f - %.2f" % (pt.x, pt.y, pt.z, edge.FirstParameter, p, edge.LastParameter))
|
|
|
|
p12 = edge.Curve.value((edge.FirstParameter + p)/2)
|
|
p23 = edge.Curve.value((p + edge.LastParameter)/2)
|
|
#print("splitArcAt: p12=(%.2f, %.2f, %.2f) p23=(%.2f, %.2f, %.2f)" % (p12.x, p12.y, p12.z, p23.x, p23.y, p23.z))
|
|
|
|
edges.append(Part.Edge(Part.Arc(p1, p12, p2)))
|
|
edges.append(Part.Edge(Part.Arc(p2, p23, p3)))
|
|
|
|
return edges
|
|
|
|
@classmethod
|
|
def splitEdgeAt(cls, edge, pt):
|
|
"""(edge, pt)
|
|
Returns a list of 2 edges, forming the original edge split at the given point.
|
|
The results are undefined if the Vector representing the point is not part of the edge."""
|
|
# I could not get the OCC parameterAt and split to work ...
|
|
# pt HAS to be on the edge, otherwise the results are undefined
|
|
p1 = edge.valueAt(edge.FirstParameter)
|
|
p2 = pt
|
|
p3 = edge.valueAt(edge.LastParameter)
|
|
edges = []
|
|
|
|
if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
|
|
# it's a line
|
|
return [Part.Edge(Part.LineSegment(p1, p2)), Part.Edge(Part.LineSegment(p2, p3))]
|
|
elif type(edge.Curve) == Part.Circle:
|
|
# it's an arc
|
|
return cls.splitArcAt(edge, pt)
|
|
else:
|
|
# it's a helix
|
|
arc = cls.helixToArc(edge, 0)
|
|
aes = cls.splitArcAt(arc, FreeCAD.Vector(pt.x, pt.y, 0))
|
|
return [cls.arcToHelix(aes[0], p1.z, p2.z), cls.arcToHelix(aes[1], p2.z, p3.z)]
|
|
|