File size: 15,553 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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *   Copyright (c) 2015 Dan Falck <ddfalck@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                                                                   *
# *                                                                         *
# ***************************************************************************

"""Post Process command that will make use of the Output File and Post
Processor entries in PathJob"""


import FreeCAD
import FreeCADGui
import Path
from PathScripts import PathUtils
from Path.Post.Utils import FilenameGenerator
import os
from Path.Post.Processor import PostProcessor, PostProcessorFactory
from PySide import QtCore, QtGui
from PySide.QtCore import QT_TRANSLATE_NOOP

LOG_MODULE = Path.Log.thisModule()

DEBUG = False
if DEBUG:
    Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
    Path.Log.trackModule(Path.Log.thisModule())
else:
    Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())


translate = FreeCAD.Qt.translate


def _resolve_post_processor_name(job):
    Path.Log.debug("_resolve_post_processor_name()")
    if job.PostProcessor:
        valid_name = job.PostProcessor
    elif Path.Preferences.defaultPostProcessor():
        valid_name = Path.Preferences.defaultPostProcessor()
    elif FreeCAD.GuiUp:
        valid_name = DlgSelectPostProcessor().exec_()  # Ensure DlgSelectPostProcessor is defined
    else:
        valid_name = None

    if valid_name and PostProcessor.exists(valid_name):
        return valid_name
    raise ValueError("Post processor not identified.")


class DlgSelectPostProcessor:
    """Provide user with list of available and active post processor
    choices."""

    def __init__(self):
        self.dialog = FreeCADGui.PySideUic.loadUi(":/panels/DlgSelectPostProcessor.ui")
        firstItem = None
        for post in Path.Preferences.allEnabledPostProcessors():
            item = QtGui.QListWidgetItem(post)
            item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled)
            self.dialog.lwPostProcessor.addItem(item)
            if not firstItem:
                firstItem = item
        if firstItem:
            self.dialog.lwPostProcessor.setCurrentItem(firstItem)
        else:
            self.dialog.buttonBox.button(QtGui.QDialogButtonBox.Ok).setEnabled(False)
        self.tooltips = {}
        self.dialog.lwPostProcessor.itemDoubleClicked.connect(self.dialog.accept)
        self.dialog.lwPostProcessor.setMouseTracking(True)
        self.dialog.lwPostProcessor.itemEntered.connect(self.updateTooltip)

    def updateTooltip(self, item):
        if item.text() in self.tooltips:
            tooltip = self.tooltips[item.text()]
        else:
            processor = PostProcessor.load(item.text())
            self.tooltips[item.text()] = processor.tooltip
            tooltip = processor.tooltip
        self.dialog.lwPostProcessor.setToolTip(tooltip)

    def exec_(self):
        if self.dialog.exec_() == 1:
            posts = self.dialog.lwPostProcessor.selectedItems()
            return posts[0].text()
        return None


class CommandPathPost:
    def GetResources(self):
        return {
            "Pixmap": "CAM_Post",
            "MenuText": QT_TRANSLATE_NOOP("CAM_Post", "Post Process"),
            "Accel": "P, P",
            "ToolTip": QT_TRANSLATE_NOOP("CAM_Post", "Post Processes the selected Job"),
        }

    def IsActive(self):
        selection = FreeCADGui.Selection.getSelection()
        if not selection:
            return False

        self.candidate = PathUtils.findParentJob(selection[0])

        return self.candidate is not None

    def _write_file(self, filename, gcode, policy):
        #
        # Up to this point the postprocessors have been using "\n" as the end-of-line
        # characters in the gcode and using the process of writing out the file as a way
        # to convert the "\n" into whatever end-of-line characters match the system
        # running the postprocessor.  This can be a problem if the controller which will
        # run the gcode doesn't like the same end-of-line characters as the system that
        # ran the postprocessor to generate the gcode.
        # The refactored code base now allows for four possible types of end-of-line
        # characters in the gcode.
        #
        if len(gcode) > 1 and gcode[0:2] == "\n\n":
            # The gcode shouldn't normally start with "\n\n".
            # This means that the gcode contains "\n" as the end-of-line characters and
            # that the gcode should be written out exactly that way.
            newline_handling = ""
            gcode = gcode[2:]
        elif "\r" in gcode:
            # Write out the gcode with whatever end-of-line characters it already has,
            # presumably either "\r" or "\r\n".
            newline_handling = ""
        else:
            # The gcode is assumed to contain "\n" as the end-of-line characters (if
            # there are any end-of-line characters in the gcode).  This case also
            # handles a zero-length gcode string.
            # Write out the gcode but convert "\n" to whatever the system uses.
            # This is also backwards compatible with the "previous" way of doing things.
            newline_handling = None

        if policy.casefold() == "open file dialog":
            dlg = QtGui.QFileDialog()
            dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile)
            dlg.setAcceptMode(QtGui.QFileDialog.AcceptMode.AcceptSave)
            dlg.setDirectory(os.path.dirname(filename))
            dlg.selectFile(os.path.basename(filename))
            if dlg.exec_():
                filename = dlg.selectedFiles()[0]
                Path.Log.debug(filename)
                with open(filename, "w", encoding="utf-8", newline=newline_handling) as f:
                    f.write(gcode)
            else:
                return

        elif policy.casefold() == "append unique id on conflict":
            while os.path.isfile(filename):
                base, ext = os.path.splitext(filename)
                filename = f"{base}-1{ext}"
            with open(filename, "w", encoding="utf-8", newline=newline_handling) as f:
                f.write(gcode)

        elif policy.casefold() == "open file dialog on conflict":
            if os.path.isfile(filename):
                dlg = QtGui.QFileDialog()
                dlg.setFileMode(QtGui.QFileDialog.FileMode.AnyFile)
                dlg.setAcceptMode(QtGui.QFileDialog.AcceptSave)
                dlg.setDirectory(os.path.dirname(filename))
                dlg.selectFile(os.path.basename(filename))
                if dlg.exec_():
                    filename = dlg.selectedFiles()[0]
                    Path.Log.debug(filename)
                    with open(filename, "w", encoding="utf-8", newline=newline_handling) as f:
                        f.write(gcode)
                else:
                    return
            else:
                with open(filename, "w", encoding="utf-8", newline=newline_handling) as f:
                    f.write(gcode)

        else:  # Overwrite
            with open(filename, "w", encoding="utf-8", newline=newline_handling) as f:
                f.write(gcode)

        FreeCAD.Console.PrintMessage(f"File written to {filename}\n")

    def Activated(self):
        """
        Handles the activation of post processing, initiating the process based
        on user selection and document context.
        """
        Path.Log.debug(self.candidate.Name)
        FreeCAD.ActiveDocument.openTransaction("Post Process the Selected Job")

        postprocessor_name = _resolve_post_processor_name(self.candidate)
        Path.Log.debug(f"Post Processor: {postprocessor_name}")

        if not postprocessor_name:
            FreeCAD.ActiveDocument.abortTransaction()
            return

        # get a postprocessor
        postprocessor = PostProcessorFactory.get_post_processor(
            self.candidate,
            postprocessor_name,
        )

        post_data = postprocessor.export()
        # None is returned if there was an error during argument processing
        # otherwise the "usual" post_data data structure is returned.
        if not post_data:
            FreeCAD.ActiveDocument.abortTransaction()
            return

        policy = Path.Preferences.defaultOutputPolicy()
        generator = FilenameGenerator(job=self.candidate)
        generated_filename = generator.generate_filenames()

        for item in post_data:
            subpart, gcode = item

            # get a name for the file
            subpart = "" if subpart == "allitems" else subpart
            Path.Log.debug(subpart)
            generator.set_subpartname(subpart)
            fname = next(generated_filename)

            #
            # It is useful for a postprocessor to be able to either skip writing out
            # a file or write out a zero-length file to indicate that something unusual
            # has happened.  The "gcode" variable is usually a string containing gcode
            # formatted for output.  If the gcode string is zero length then a zero
            # length file will be written out.  If the "gcode" variable contains None
            # instead, that indicates that the postprocessor doesn't want a file to be
            # written at all.
            #
            # There is at least one old-style postprocessor that currently puts the
            # gcode file out to a file server and doesn't need to write out a file to
            # the system where FreeCAD is running.  In the old-style postprocessors the
            # postprocessor code decided whether to write out a file.  Eventually a
            # newer (more object-oriented) version of that postprocessor will return
            # None for the "gcode" variable value to tell this code not to write out
            # a file.  There may be other uses found for this capability over time.
            #
            if gcode is not None:
                # write the results to the file
                self._write_file(fname, gcode, policy)

        FreeCAD.ActiveDocument.commitTransaction()
        FreeCAD.ActiveDocument.recompute()


class CommandPathPostSelected(CommandPathPost):
    def GetResources(self):
        return {
            "Pixmap": "CAM_PostSelected",
            "MenuText": QT_TRANSLATE_NOOP("CAM_Post", "Post Process Selected"),
            "Accel": "P, O",
            "ToolTip": QT_TRANSLATE_NOOP("CAM_Post", "Post Processes the selected operations"),
        }

    def IsActive(self):
        selection = FreeCADGui.Selection.getSelection()
        if not selection:
            return False

        return all(hasattr(op, "Path") and not op.Name.startswith("Job") for op in selection)

    def Activated(self):
        """
        Handles the activation of post processing, initiating the process based
        on user selection and document context.
        """
        FreeCAD.ActiveDocument.openTransaction("Post Process the Selected operations")

        selection = FreeCADGui.Selection.getSelection()
        job = PathUtils.findParentJob(selection[0])
        if (
            not job
            and hasattr(selection[0], "Base")
            and isinstance(selection[0].Base, list)
            and selection[0].Base
        ):
            # find 'job' for operation inside 'Array' with multi tool controller
            baseOp = FreeCAD.ActiveDocument.getObject(selection[0].Base[0])
            job = PathUtils.findParentJob(baseOp)

        opCandidates = [op for op in selection if hasattr(op, "Path") and "Job" not in op.Name]
        operations = []
        if opCandidates and job.Operations.Group != opCandidates:
            msgBox = QtGui.QMessageBox()
            msgBox.setWindowTitle("Post Process")
            msgBox.setText("<p align='center'>What needs to be exported?</p>")
            msgBox.setInformativeText(
                "<p align='center'>Check to make sure that you won't break anything by leaving out operations</p>"
            )
            msgBox.findChild(QtGui.QGridLayout).setColumnMinimumWidth(1, 250)
            btn1 = msgBox.addButton("Only selected", QtGui.QMessageBox.ButtonRole.YesRole)
            btn2 = msgBox.addButton("All", QtGui.QMessageBox.ButtonRole.NoRole)
            msgBox.setDefaultButton(btn2)
            msgBox.exec()

            if msgBox.clickedButton() == btn1:
                print(
                    f"Post process only selected operations: {', '.join([op.Name for op in opCandidates])}"
                )
                operations = opCandidates

        postprocessor_name = _resolve_post_processor_name(job)
        Path.Log.debug(f"Post Processor: {postprocessor_name}")

        if not postprocessor_name:
            FreeCAD.ActiveDocument.abortTransaction()
            return

        # get a postprocessor
        postprocessor = PostProcessorFactory.get_post_processor(
            {"job": job, "operations": operations}, postprocessor_name
        )

        post_data = postprocessor.export()
        # None is returned if there was an error during argument processing
        # otherwise the "usual" post_data data structure is returned.
        if not post_data:
            FreeCAD.ActiveDocument.abortTransaction()
            return

        policy = Path.Preferences.defaultOutputPolicy()
        generator = FilenameGenerator(job=job)
        generated_filename = generator.generate_filenames()

        for item in post_data:
            subpart, gcode = item

            # get a name for the file
            subpart = "" if subpart == "allitems" else subpart
            Path.Log.debug(subpart)
            generator.set_subpartname(subpart)
            fname = next(generated_filename)

            if gcode is not None:
                # write the results to the file
                self._write_file(fname, gcode, policy)

        FreeCAD.ActiveDocument.commitTransaction()
        FreeCAD.ActiveDocument.recompute()


if FreeCAD.GuiUp:
    # register the FreeCAD command
    FreeCADGui.addCommand("CAM_Post", CommandPathPost())
    FreeCADGui.addCommand("CAM_PostSelected", CommandPathPostSelected())

FreeCAD.Console.PrintLog("Loading PathPost… done\n")