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 @@
+
+
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