diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 70609ce0d3..32bd70f156 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -224,6 +224,7 @@ SET(PathTests_SRCS PathTests/PathTestUtils.py PathTests/test_adaptive.fcstd PathTests/test_centroid_00.ngc + PathTests/test_filenaming.fcstd PathTests/test_geomop.fcstd PathTests/test_holes00.fcstd PathTests/test_linuxcnc_00.ngc diff --git a/src/Mod/Path/Gui/Resources/Path.qrc b/src/Mod/Path/Gui/Resources/Path.qrc index 66acdc91e6..9ef71d6643 100644 --- a/src/Mod/Path/Gui/Resources/Path.qrc +++ b/src/Mod/Path/Gui/Resources/Path.qrc @@ -14,6 +14,7 @@ icons/Path_BStep.svg icons/Path_BStop.svg icons/Path_BaseGeometry.svg + icons/Path_Camotics.svg icons/Path_Comment.svg icons/Path_Compound.svg icons/Path_Contour.svg @@ -130,6 +131,7 @@ panels/ToolBitSelector.ui panels/ToolEditor.ui panels/ToolLibraryEditor.ui + panels/TaskPathCamoticsSim.ui panels/TaskPathSimulator.ui panels/ZCorrectEdit.ui preferences/Advanced.ui diff --git a/src/Mod/Path/Gui/Resources/icons/Path_Camotics.svg b/src/Mod/Path/Gui/Resources/icons/Path_Camotics.svg new file mode 100644 index 0000000000..e9d4b73037 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/icons/Path_Camotics.svg @@ -0,0 +1,644 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + Path_Drilling + 2015-07-04 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Path/Gui/Resources/icons/Path_Drilling.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + [agryson] Alexander Gryson + + + + + + + + + C + + + + diff --git a/src/Mod/Path/Gui/Resources/panels/TaskPathCamoticsSim.ui b/src/Mod/Path/Gui/Resources/panels/TaskPathCamoticsSim.ui new file mode 100644 index 0000000000..3fca6c0c93 --- /dev/null +++ b/src/Mod/Path/Gui/Resources/panels/TaskPathCamoticsSim.ui @@ -0,0 +1,92 @@ + + + TaskPathSimulator + + + + 0 + 0 + 377 + 361 + + + + Path Simulator + + + + + + 23 + + + + + + + + + TextLabel + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + + + + + + + + + Launch Camotics + + + + + + + Make Camotics File + + + + + + + + + + + SimStop() + SimPlay() + SimPause() + SimStep() + SimFF() + + diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 43897fc787..f023397743 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -88,6 +88,7 @@ class PathWorkbench(Workbench): from PySide.QtCore import QT_TRANSLATE_NOOP import PathCommands + import subprocess PathGuiInit.Startup() @@ -158,7 +159,13 @@ class PathWorkbench(Workbench): if PathPreferences.advancedOCLFeaturesEnabled(): try: - import ocl + subprocess.call(["camsim", "-v"]) + toolcmdlist.append("Path_Camotics") + except FileNotFoundError: + pass + + try: + import ocl # pylint: disable=unused-variable from PathScripts import PathSurfaceGui from PathScripts import PathWaterlineGui diff --git a/src/Mod/Path/PathScripts/PathCamoticsGui.py b/src/Mod/Path/PathScripts/PathCamoticsGui.py new file mode 100644 index 0000000000..4d5f4a8b34 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathCamoticsGui.py @@ -0,0 +1,464 @@ +# -*- coding: utf-8 -*- +# *************************************************************************** +# * Copyright (c) 2020 sliptonic * +# * * +# * 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 FreeCADGui +import PathScripts.PathLog as PathLog +from PySide.QtCore import QT_TRANSLATE_NOOP + +# from pivy import coin +# from itertools import cycle +# import FreeCADGui as Gui +import json + +# import tempfile +import os +import Mesh + +# import string +# import random +import camotics +import PathScripts.PathPost as PathPost +import io + +# import time +import PathScripts +import queue +from threading import Thread, Lock +import subprocess + +from PySide import QtCore, QtGui + +__title__ = "Camotics Simulator" +__author__ = "sliptonic (Brad Collette)" +__url__ = "https://www.freecadweb.org" +__doc__ = "Task panel for Camotics Simulation" + +if False: + PathLog.setLevel(PathLog.Level.DEBUG, PathLog.thisModule()) + PathLog.trackModule(PathLog.thisModule()) +else: + PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + +translate = FreeCAD.Qt.translate + + +class CAMoticsUI: + def __init__(self, simulation): + # this will create a Qt widget from our ui file + self.form = FreeCADGui.PySideUic.loadUi(":/panels/TaskPathCamoticsSim.ui") + self.simulation = simulation + self.initializeUI() + self.lock = False + + def initializeUI(self): + self.form.timeSlider.sliderReleased.connect( + lambda: self.simulation.execute(self.form.timeSlider.value()) + ) + self.form.progressBar.reset() + self.form.timeSlider.setEnabled = False + self.form.btnLaunchCamotics.clicked.connect(self.launchCamotics) + self.form.btnMakeFile.clicked.connect(self.makeCamoticsFile) + self.simulation.progressUpdate.connect(self.calculating) + self.simulation.statusChange.connect(self.updateStatus) + self.form.txtStatus.setText(translate("Path", "Drag Slider to Simulate")) + + def launchCamotics(self): + filename = self.makeCamoticsFile() + subprocess.Popen(["camotics", filename]) + + def makeCamoticsFile(self): + PathLog.track() + filename = QtGui.QFileDialog.getSaveFileName( + self.form, + translate("Path", "Save Project As"), + "", + translate("Path", "Camotics Project (*.camotics)"), + )[0] + if filename: + if not filename.endswith(".camotics"): + filename += ".camotics" + + text = self.simulation.buildproject() + try: + with open(filename, "w") as outputfile: + outputfile.write(text) + except IOError: + QtGui.QMessageBox.information( + self, translate("Path", "Unable to open file: {}".format(filename)) + ) + + return filename + + def accept(self): + self.simulation.accept() + FreeCADGui.Control.closeDialog() + + def reject(self): + self.simulation.cancel() + if self.simulation.simmesh is not None: + FreeCAD.ActiveDocument.removeObject(self.simulation.simmesh.Name) + FreeCADGui.Control.closeDialog() + + def setRunTime(self, duration): + self.form.timeSlider.setMinimum(0) + self.form.timeSlider.setMaximum(duration) + + def calculating(self, progress=0.0): + self.form.timeSlider.setEnabled = progress == 1.0 + self.form.progressBar.setValue(int(progress * 100)) + + def updateStatus(self, status): + self.form.txtStatus.setText(status) + + +class CamoticsSimulation(QtCore.QObject): + + SIM = camotics.Simulation() + q = queue.Queue() + progressUpdate = QtCore.Signal(object) + statusChange = QtCore.Signal(object) + simmesh = None + filenames = [] + + SHAPEMAP = { + "ballend": "Ballnose", + "endmill": "Cylindrical", + "v-bit": "Conical", + "chamfer": "Snubnose", + } + + def worker(self, lock): + while True: + item = self.q.get() + PathLog.debug("worker processing: {}".format(item)) + with lock: + if item["TYPE"] == "STATUS": + self.statusChange.emit(item["VALUE"]) + if item["VALUE"] == "DONE": + self.SIM.wait() + surface = self.SIM.get_surface("binary") + self.SIM.wait() + self.addMesh(surface) + elif item["TYPE"] == "PROGRESS": + self.progressUpdate.emit(item["VALUE"]) + self.q.task_done() + + def __init__(self): + super().__init__() # needed for QT signals + lock = Lock() + Thread(target=self.worker, daemon=True, args=(lock,)).start() + + def callback(self, status, progress): + self.q.put({"TYPE": "PROGRESS", "VALUE": progress}) + self.q.put({"TYPE": "STATUS", "VALUE": status}) + + def isDone(self, success): + self.q.put({"TYPE": "STATUS", "VALUE": "DONE"}) + + def addMesh(self, surface): + """takes a binary stl and adds a Mesh to the current document""" + + if self.simmesh is None: + self.simmesh = FreeCAD.ActiveDocument.addObject("Mesh::Feature", "Camotics") + buffer = io.BytesIO() + buffer.write(surface) + buffer.seek(0) + mesh = Mesh.Mesh() + mesh.read(buffer, "STL") + self.simmesh.Mesh = mesh + # Mesh.show(mesh) + + def Activate(self): + self.taskForm = CAMoticsUI(self) + FreeCADGui.Control.showDialog(self.taskForm) + self.job = FreeCADGui.Selection.getSelectionEx()[0].Object + self.SIM.set_metric() + self.SIM.set_resolution("high") + + bb = self.job.Stock.Shape.BoundBox + self.SIM.set_workpiece( + min=(bb.XMin, bb.YMin, bb.ZMin), max=(bb.XMax, bb.YMax, bb.ZMax) + ) + + for t in self.job.Tools.Group: + self.SIM.set_tool( + t.ToolNumber, + metric=True, + shape=self.SHAPEMAP.get(t.Tool.ShapeName, "Cylindrical"), + length=t.Tool.Length.Value, + diameter=t.Tool.Diameter.Value, + ) + + postlist = PathPost.buildPostList(self.job) + PathLog.track(postlist) + # self.filenames = [PathPost.resolveFileName(self.job)] + + success = True + + finalgcode = "" + if self.job.SplitOutput: + PathLog.track(postlist) + for idx, section in enumerate(postlist): + # split = os.path.splitext(self.filename) + # partname = split[0] + "_{}".format(index) + split[1] + partname = section[0] + sublist = section[1] + + result, gcode, name = PathPost.CommandPathPost().exportObjectsWith( + sublist, + partname, + self.job, + idx, + extraargs="--no-show-editor", + ) + self.filenames.append(name) + PathLog.track(result, gcode, name) + + if result is None: + success = False + else: + finalgcode += gcode + + else: + finalpostlist = [item for slist in postlist for item in slist] + PathLog.track(postlist) + result, gcode, name = PathPost.CommandPathPost().exportObjectsWith( + finalpostlist, + "allitems", + self.job, + 0, + extraargs="--no-show-editor", + ) + self.filenames.append(name) + success = result is not None + finalgcode = gcode + + if not success: + return + + self.SIM.compute_path(finalgcode) + self.SIM.wait() + + tot = sum([step["time"] for step in self.SIM.get_path()]) + PathLog.debug("sim time: {}".format(tot)) + self.taskForm.setRunTime(tot) + + def execute(self, timeIndex): + PathLog.track() + self.SIM.start(self.callback, time=timeIndex, done=self.isDone) + + def accept(self): + pass + + def cancel(self): + pass + + # def makeCoinMesh(self, surface): + # # this doesn't work yet + # sg = Gui.ActiveDocument.ActiveView.getSceneGraph(); + # color = coin.SoBaseColor() + # color.rgb = (1, 0, 1) + # coords = coin.SoTransform() + # node = coin.SoSeparator() + # node.addChild(color) + # node.addChild(coords) + + # end = [-1] + # vertices = list(zip(*[iter(surface['vertices'])] * 3)) + # #polygons = list(zip(*[iter(vertices)] * 3, cycle(end))) + # polygons = list(zip(*[iter(range(len(vertices)))] * 3, cycle(end))) + + # print(vertices) + # print(polygons) + + # data=coin.SoCoordinate3() + # face=coin.SoIndexedFaceSet() + # node.addChild(data) + # node.addChild(face) + + # i = 0 + # for v in vertices: + # data.point.set1Value(i, v[0], v[1], v[2]) + # i += 1 + # i = 0 + # for p in polygons: + # try: + # face.coordIndex.set1Value(i, p) + # i += 1 + # except Exception as e: + # print(e) + # print(i) + # print(p) + + # sg.addChild(node) + + # def Activated(self): + + # s = self.SIM + # print('activated') + # print (s.is_running()) + + # if s.is_running(): + # print('interrupted') + # s.interrupt() + # s.wait() + # else: + # try: + # surface = s.get_surface('python') + # except Exception as e: + # print(e) + # pp = CommandPathPost() + # job = FreeCADGui.Selection.getSelectionEx()[0].Object + + # s = camotics.Simulation() + # s.set_metric() + # s.set_resolution('high') + + # bb = job.Stock.Shape.BoundBox + # s.set_workpiece(min = (bb.XMin, bb.YMin, bb.ZMin), max = (bb.XMax, bb.YMax, bb.ZMax)) + + # shapemap = {'ballend': 'Ballnose', + # 'endmill': 'Cylindrical', + # 'v-bit' : 'Conical', + # 'chamfer': 'Snubnose'} + + # for t in job.Tools.Group: + # s.set_tool(t.ToolNumber, + # metric = True, + # shape = shapemap.get(t.Tool.ShapeName, 'Cylindrical'), + # length = t.Tool.Length.Value, + # diameter = t.Tool.Diameter.Value) + + # gcode = job.Path.toGCode() #temporary solution!!!!! + # s.compute_path(gcode) + # s.wait() + + # print(s.get_path()) + + # tot = sum([step['time'] for step in s.get_path()]) + + # print(tot) + + # for t in range(1, int(tot), int(tot/10)): + # print(t) + # s.start(callback, time=t) + # while s.is_running(): + # time.sleep(0.1) + + # s.wait() + + # surface = s.get_surface('binary') + # self.addMesh(surface) + + def buildproject(self): # , files=[]): + PathLog.track() + + job = self.job + + tooltemplate = { + "units": "metric", + "shape": "cylindrical", + "length": 10, + "diameter": 3.125, + "description": "", + } + + workpiecetemplate = { + "automatic": False, + "margin": 0, + "bounds": {"min": [0, 0, 0], "max": [0, 0, 0]}, + } + + camoticstemplate = { + "units": "metric", + "resolution-mode": "medium", + "resolution": 1, + "tools": {}, + "workpiece": {}, + "files": [], + } + + unitstring = ( + "imperial" if FreeCAD.Units.getSchema() in [2, 3, 5, 7] else "metric" + ) + + camoticstemplate["units"] = unitstring + camoticstemplate["resolution-mode"] = "medium" + camoticstemplate["resolution"] = 1 + + toollist = {} + for t in job.Tools.Group: + toolitem = tooltemplate.copy() + toolitem["units"] = unitstring + if hasattr(t.Tool, "Camotics"): + toolitem["shape"] = t.Tool.Camotics + else: + toolitem["shape"] = self.SHAPEMAP.get(t.Tool.ShapeName, "Cylindrical") + + toolitem["length"] = t.Tool.Length.Value + toolitem["diameter"] = t.Tool.Diameter.Value + toolitem["description"] = t.Label + toollist[t.ToolNumber] = toolitem + + camoticstemplate["tools"] = toollist + + bb = job.Stock.Shape.BoundBox + + workpiecetemplate["bounds"]["min"] = [bb.XMin, bb.YMin, bb.ZMin] + workpiecetemplate["bounds"]["max"] = [bb.XMax, bb.YMax, bb.ZMax] + camoticstemplate["workpiece"] = workpiecetemplate + + camoticstemplate["files"] = self.filenames # files + + return json.dumps(camoticstemplate, indent=2) + + +class CommandCamoticsSimulate: + def GetResources(self): + return { + "Pixmap": "Path_Camotics", + "MenuText": QT_TRANSLATE_NOOP("Path_Camotics", "Camotics"), + "Accel": "P, C", + "ToolTip": QT_TRANSLATE_NOOP("Path_Camotics", "Simulate using Camotics"), + "CmdType": "ForEdit", + } + + def IsActive(self): + if bool(FreeCADGui.Selection.getSelection()) is False: + return False + try: + job = FreeCADGui.Selection.getSelectionEx()[0].Object + return isinstance(job.Proxy, PathScripts.PathJob.ObjectJob) + except: + return False + + def Activated(self): + pathSimulation = CamoticsSimulation() + pathSimulation.Activate() + + +if FreeCAD.GuiUp: + FreeCADGui.addCommand("Path_Camotics", CommandCamoticsSimulate()) + + +FreeCAD.Console.PrintLog("Loading PathCamoticsSimulateGui ... done\n") diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py index 593648e229..02846c5d70 100644 --- a/src/Mod/Path/PathScripts/PathGuiInit.py +++ b/src/Mod/Path/PathScripts/PathGuiInit.py @@ -49,6 +49,7 @@ def Startup(): from PathScripts import PathDressupPathBoundaryGui from PathScripts import PathDressupRampEntry from PathScripts import PathDressupTagGui + from PathScripts import PathDressupLeadInOut from PathScripts import PathDressupZCorrect from PathScripts import PathDrillingGui from PathScripts import PathEngraveGui @@ -67,6 +68,7 @@ def Startup(): from PathScripts import PathSetupSheetGui from PathScripts import PathSimpleCopy from PathScripts import PathSimulatorGui + from PathScripts import PathCamoticsGui from PathScripts import PathSlotGui from PathScripts import PathStop from PathScripts import PathThreadMillingGui diff --git a/src/Mod/Path/PathScripts/PathJob.py b/src/Mod/Path/PathScripts/PathJob.py index f5f134ab15..d2b64c1405 100644 --- a/src/Mod/Path/PathScripts/PathJob.py +++ b/src/Mod/Path/PathScripts/PathJob.py @@ -674,6 +674,8 @@ class ObjectJob: if getattr(obj, "Operations", None): # obj.Path = obj.Operations.Path self.getCycleTime() + if hasattr(obj, "PathChanged"): + obj.PathChanged = True def getCycleTime(self): seconds = 0 diff --git a/src/Mod/Path/PathTests/TestPathPost.py b/src/Mod/Path/PathTests/TestPathPost.py index bdf2a8e19f..67f4323164 100644 --- a/src/Mod/Path/PathTests/TestPathPost.py +++ b/src/Mod/Path/PathTests/TestPathPost.py @@ -164,10 +164,6 @@ def dumpgroup(group): print(f"--->{j.Name}") print("====================") - self.assertRaises( - gcode_pre.PathNoActiveDocumentException, - gcode_pre._isImportEnvironmentReady, - ) class TestBuildPostList(unittest.TestCase): """ @@ -192,6 +188,13 @@ class TestBuildPostList(unittest.TestCase): doc = FreeCAD.open(testfile) job = doc.getObjectsByLabel("MainJob")[0] + def setUp(self): + pass + + def tearDown(self): + pass + + def test000(self): # check that the test file is structured correctly @@ -269,8 +272,6 @@ class TestBuildPostList(unittest.TestCase): self.assertTrue(len(firstoplist) == 6) self.assertTrue(firstoutputitem[0] == "G54") - importFile = FreeCAD.getHomePath() + "Mod/Path/PathTests/test_centroid_00.ngc" - gcodeByToolNumberList = gcode_pre._identifygcodeByToolNumberList(importFile) class TestOutputNameSubstitution(unittest.TestCase): @@ -323,8 +324,10 @@ class TestOutputNameSubstitution(unittest.TestCase): job = doc.getObjectsByLabel("MainJob")[0] macro = FreeCAD.getUserMacroDir() + def test000(self): # Test basic name generation with empty string + FreeCAD.setActiveDocument(self.doc.Label) teststring = "" self.job.PostProcessorOutputFile = teststring self.job.SplitOutput = False diff --git a/src/Mod/Path/TestPathApp.py b/src/Mod/Path/TestPathApp.py index b8752f998c..e4d397a126 100644 --- a/src/Mod/Path/TestPathApp.py +++ b/src/Mod/Path/TestPathApp.py @@ -38,10 +38,10 @@ from PathTests.TestPathHelixGenerator import TestPathHelixGenerator from PathTests.TestPathLog import TestPathLog from PathTests.TestPathOpTools import TestPathOpTools -# from PathTests.TestPathPost import PathPostTestCases -from PathTests.TestPathPost import OutputOrderingTestCases from PathTests.TestPathPost import TestPathPostUtils -from PathTests.TestPathPost import TestPathPostImport +from PathTests.TestPathPost import TestBuildPostList +from PathTests.TestPathPost import TestOutputNameSubstitution + from PathTests.TestPathPreferences import TestPathPreferences from PathTests.TestPathPropertyBag import TestPathPropertyBag from PathTests.TestPathRotationGenerator import TestPathRotationGenerator @@ -72,8 +72,10 @@ False if TestPathHelpers.__name__ else True # False if TestPathHelix.__name__ else True False if TestPathLog.__name__ else True False if TestPathOpTools.__name__ else True -False if TestPathPostImport.__name__ else True +# False if TestPathPostImport.__name__ else True # False if TestPathPost.__name__ else True +False if TestBuildPostList.__name__ else True +False if TestOutputNameSubstitution.__name__ else True False if TestPathPostUtils.__name__ else True False if TestPathPreferences.__name__ else True False if TestPathPropertyBag.__name__ else True