File size: 9,398 Bytes
985c397
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# SPDX-License-Identifier: LGPL-2.1-or-later
# ***************************************************************************
# *   Copyright (c) 2022 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 Path
import math

__title__ = "PathLanguage - classes for an internal language/representation for Path"
__author__ = "sliptonic (Brad Collette)"
__url__ = "https://www.freecad.org"
__doc__ = "Functions to extract and convert between Path.Command and Part.Edge and utility functions to reason about them."

CmdMoveStraight = Path.Geom.CmdMoveStraight + Path.Geom.CmdMoveRapid


class Instruction(object):
    """An Instruction is a pure python replacement of Path.Command which also tracks its begin position."""

    def __init__(self, begin, cmd, param=None):
        self.begin = begin
        if isinstance(cmd, Path.Command):
            self.cmd = Path.Name
            self.param = Path.Parameters
        else:
            self.cmd = cmd
            if param is None:
                self.param = {}
            else:
                self.param = param

    def anglesOfTangents(self):
        return (0, 0)

    def setPositionBegin(self, begin):
        self.begin = begin

    def positionBegin(self):
        """positionBegin() ... returns a Vector of the begin position"""
        return self.begin

    def positionEnd(self):
        """positionEnd() ... returns a Vector of the end position"""
        return FreeCAD.Vector(self.x(self.begin.x), self.y(self.begin.y), self.z(self.begin.z))

    def pathLength(self):
        """pathLength() ... returns the length in mm"""
        return 0

    def isMove(self):
        return False

    def isRapid(self):
        return False

    def isPlunge(self):
        """isPlunge() ... return true if this moves is vertical"""
        if self.isMove():
            if (
                Path.Geom.isRoughly(self.begin.x, self.x(self.begin.x))
                and Path.Geom.isRoughly(self.begin.y, self.y(self.begin.y))
                and not Path.Geom.isRoughly(self.begin.z, self.z(self.begin.z))
            ):
                return True
        return False

    def leadsInto(self, instr):
        """leadsInto(instr) ... return true if instr is a continuation of self"""
        return Path.Geom.pointsCoincide(self.positionEnd(), instr.positionBegin())

    def x(self, default=0):
        return self.param.get("X", default)

    def y(self, default=0):
        return self.param.get("Y", default)

    def z(self, default=0):
        return self.param.get("Z", default)

    def i(self, default=0):
        return self.param.get("I", default)

    def j(self, default=0):
        return self.param.get("J", default)

    def k(self, default=0):
        return self.param.get("K", default)

    def xyBegin(self):
        """xyBegin() ... internal convenience function"""
        return FreeCAD.Vector(self.begin.x, self.begin.y, 0)

    def xyEnd(self):
        """xyEnd() ... internal convenience function"""
        return FreeCAD.Vector(self.x(self.begin.x), self.y(self.begin.y), 0)

    def __repr__(self):
        return f"{self.cmd}{self.param}"

    def str(self, digits=2):
        if digits == 0:
            s = [f"{k}: {int(v)}" for k, v in self.param.items()]
        else:
            fmt = f"{{}}: {{:.{digits}}}"
            s = [fmt.format(k, v) for k, v in self.param.items()]
        return f"{self.cmd}{{{', '.join(s)}}}"


class MoveStraight(Instruction):
    def anglesOfTangents(self):
        """anglesOfTangents() ... return a tuple with the tangent angles at begin and end position"""
        begin = self.xyBegin()
        end = self.xyEnd()
        if end == begin:
            return (0, 0)
        a = Path.Geom.getAngle(end - begin)
        return (a, a)

    def isMove(self):
        return True

    def isStraight(self):
        return True

    def isArc(self):
        return False

    def isRapid(self):
        return self.cmd in Path.Geom.CmdMoveRapid

    def pathLength(self):
        return (self.positionEnd() - self.positionBegin()).Length


class MoveArc(Instruction):
    def anglesOfTangents(self):
        """anglesOfTangents() ... return a tuple with the tangent angles at begin and end position"""
        begin = self.xyBegin()
        end = self.xyEnd()
        center = self.xyCenter()
        # calculate angle of the hypotenuse at begin and end
        s0 = Path.Geom.getAngle(begin - center)
        s1 = Path.Geom.getAngle(end - center)
        # the tangents are perpendicular to the hypotenuse with the sign determined by the
        # direction of the arc
        return (
            Path.Geom.normalizeAngle(s0 + self.arcDirection()),
            Path.Geom.normalizeAngle(s1 + self.arcDirection()),
        )

    def isMove(self):
        return True

    def isArc(self):
        return True

    def isStraight(self):
        return False

    def isCW(self):
        return self.arcDirection() < 0

    def isCCW(self):
        return self.arcDirection() > 0

    def arcAngle(self):
        """arcAngle() ... return the angle of the arc opening"""
        begin = self.xyBegin()
        end = self.xyEnd()
        center = self.xyCenter()
        s0 = Path.Geom.getAngle(begin - center)
        s1 = Path.Geom.getAngle(end - center)

        if self.isCW():
            while s0 < s1:
                s0 = s0 + 2 * math.pi
            return s0 - s1

        # CCW
        while s1 < s0:
            s1 = s1 + 2 * math.pi
        return s1 - s0

    def arcRadius(self):
        """arcRadius() ... return the radius"""
        return (self.xyBegin() - self.xyCenter()).Length

    def pathLength(self):
        return self.arcAngle() * self.arcRadius()

    def xyCenter(self):
        return FreeCAD.Vector(self.begin.x + self.i(), self.begin.y + self.j(), 0)


class MoveArcCW(MoveArc):
    def arcDirection(self):
        return -math.pi / 2


class MoveArcCCW(MoveArc):
    def arcDirection(self):
        return math.pi / 2


class Maneuver(object):
    """A series of instructions and moves"""

    def __init__(self, begin=None, instr=None):
        self.instr = instr if instr else []
        self.setPositionBegin(begin if begin else FreeCAD.Vector(0, 0, 0))

    def setPositionBegin(self, begin):
        self.begin = begin
        for i in self.instr:
            i.setPositionBegin(begin)
            begin = i.positionEnd()

    def positionBegin(self):
        return self.begin

    def getMoves(self):
        return [instr for instr in self.instr if instr.isMove()]

    def addInstruction(self, instr):
        self.instr.append(instr)

    def addInstructions(self, coll):
        self.instr.extend(coll)

    def toPath(self):
        return Path.Path([instruction_to_command(instr) for instr in self.instr])

    def __repr__(self):
        if self.instr:
            return "\n".join([str(i) for i in self.instr])
        return ""

    @classmethod
    def InstructionFromCommand(cls, cmd, begin=None):
        if not begin:
            begin = FreeCAD.Vector(0, 0, 0)

        if cmd.Name in CmdMoveStraight:
            return MoveStraight(begin, cmd.Name, cmd.Parameters)
        if cmd.Name in Path.Geom.CmdMoveCW:
            return MoveArcCW(begin, cmd.Name, cmd.Parameters)
        if cmd.Name in Path.Geom.CmdMoveCCW:
            return MoveArcCCW(begin, cmd.Name, cmd.Parameters)
        return Instruction(begin, cmd.Name, cmd.Parameters)

    @classmethod
    def FromPath(cls, path, begin=None):
        maneuver = Maneuver(begin)
        instr = []
        begin = maneuver.positionBegin()
        for cmd in path.Commands:
            i = cls.InstructionFromCommand(cmd, begin)
            instr.append(i)
            begin = i.positionEnd()
        maneuver.instr = instr
        return maneuver

    @classmethod
    def FromGCode(cls, gcode, begin=None):
        return cls.FromPath(Path.Path(gcode), begin)


def instruction_to_command(instr):
    return Path.Command(instr.cmd, instr.param)