* [CAM] extract tool controller ui elements into their own file * [CAM] make the changes in the tool controller UI only apply when ok is clicked * [CAM] Add tool controller edit panel to the Profile operation * [CAM] Add copy button to in-operation tool controller editor * [CAM] clean up changes * [CAM] Add tool controller edit UI to all operations Notes on changes that were not a simple copy/paste job from the changes I made for Profile: - Deburr: changed TC/coolant rows from 1 and 2 to 0 and 1 - Probe: didn't work at all initially due to bug in main where ShapeName changed to ShapeType. I added a utility for reading either a ShapeType or a ShapeName (check for both properties, convert ShapeType to lower case) and applied it to probe and camotics - Drilling: moved Keep Tool Down checkbox up from row 8 to row 2 (all intermediate rows were missing) and added the edit checkbox in row 3 below it - VBit, Probe (or anything else that requires a specific tool type): in Base.py setupToolController(), I added a check to see if the currently selected tool is an invalid type, and if so and there is a valid tool, then change to that one. This fixes two UI bugs. Plausibly pre-existing, if there is one valid tool and an invalid tool is selected, it's impossible to switch to the valid one because you can't generate a combo box change event for the new tool. Definitely new: if an invalid tool is selected and there are no valid tools, the combo box will be empty but the new tool controller edit utility will let you edit the current TC anyway. - Thread Milling: replaced the Tool Controller GroupBox with the standard QFrame layout, and added the checkbox. Note that this operation doesn't have a UI element for coolant -- likely a bug, but I didn't look into it - Surface: Changed from form layout to grid layout. Deleted an old SurfaceEdit.ui file -- it was replaced with PageOpSurfaceEdit.ui in 2017 but not deleted (commit 77af19e7489e1fc637a68cdad220e5dd430dc2b9) - Waterline: Changed from form layout to grid layout * [CAM] Bug fixes setupUi() wasn't called on the tool controller editor, preventing changes in its UI from being written back to the object immediately. This caused weird behavior where if you edited a field twice it would reset the second time it was focused Added a hook to automatically update the TC combo box when the TC name (or anything else about it, since that was easier) changes * Fix usage of QSignalBlockers * [CAM] Block scroll events on tool number and spindle direction when not focused Specifically, if you mouse over either of these UI elements and use the scroll wheel, it used to focus the element and change its value. This commit makes it do neither of those things, for these specific elements, as a measure against users accidentally changing these values. * disable tcNumber edit field in operations panel * Add "New tool controller" option to TC combo box When selected, it opens (toggles, technically) the tool bit dock and returns to the previous selection. Adding a new tool controller using the dock (already, before this commit) automatically switches the operation's tool controller to the new one * Add "Copy" option to tool controller combo box * Copy TC function only in combo box, no button * [CAM] update in-operation "new tool controller" function to use a dialog * [CAM] make the tool selector always a dialog and never a dock * remove spacer from ToolControllerEdit.ui to fix exces white space * [CAM] change tool dialog default sizing/spacing * [CAM] fix bug where copying tool controller doesn't copy all values
338 lines
11 KiB
Python
338 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
# ***************************************************************************
|
|
# * Copyright (c) 2020 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 *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
from threading import Thread, Lock
|
|
import FreeCAD
|
|
import FreeCADGui
|
|
import Mesh
|
|
import Path
|
|
import PathScripts
|
|
import PathScripts.PathUtils as PathUtils
|
|
import Path.Post.Command as PathPost
|
|
import camotics
|
|
import io
|
|
import json
|
|
import queue
|
|
import subprocess
|
|
|
|
from PySide import QtCore, QtGui
|
|
|
|
__title__ = "Camotics Simulator"
|
|
__author__ = "sliptonic (Brad Collette)"
|
|
__url__ = "https://www.freecad.org"
|
|
__doc__ = "Task panel for Camotics Simulation"
|
|
|
|
if False:
|
|
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
|
|
|
|
|
|
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):
|
|
Path.Log.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()
|
|
Path.Log.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(PathUtils.getToolShapeName(t.Tool), "Cylindrical"),
|
|
length=t.Tool.Length.Value,
|
|
diameter=t.Tool.Diameter.Value,
|
|
)
|
|
|
|
postlist = PathPost.buildPostList(self.job)
|
|
Path.Log.track(postlist)
|
|
# self.filenames = [PathPost.resolveFileName(self.job)]
|
|
|
|
success = True
|
|
|
|
finalgcode = ""
|
|
for idx, section in enumerate(postlist):
|
|
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)
|
|
Path.Log.track(result, gcode, name)
|
|
|
|
if result is None:
|
|
success = False
|
|
else:
|
|
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()])
|
|
Path.Log.debug("sim time: {}".format(tot))
|
|
self.taskForm.setRunTime(tot)
|
|
|
|
def execute(self, timeIndex):
|
|
Path.Log.track()
|
|
self.SIM.start(self.callback, time=timeIndex, done=self.isDone)
|
|
|
|
def accept(self):
|
|
pass
|
|
|
|
def cancel(self):
|
|
pass
|
|
|
|
def buildproject(self): # , files=[]):
|
|
Path.Log.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(
|
|
PathUtils.getToolShapeName(t.Tool), "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": "CAM_Camotics",
|
|
"MenuText": QT_TRANSLATE_NOOP("CAM_Camotics", "CAMotics"),
|
|
"Accel": "P, C",
|
|
"ToolTip": QT_TRANSLATE_NOOP("CAM_Camotics", "Simulates 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, Path.Main.Job.ObjectJob)
|
|
except:
|
|
return False
|
|
|
|
def Activated(self):
|
|
pathSimulation = CamoticsSimulation()
|
|
pathSimulation.Activate()
|
|
|
|
|
|
if FreeCAD.GuiUp:
|
|
FreeCADGui.addCommand("CAM_Camotics", CommandCamoticsSimulate())
|
|
|
|
|
|
FreeCAD.Console.PrintLog("Loading PathCamoticsSimulateGui… done\n")
|