freecad-cam/Mod/CAM/Path/Main/Job.py
2026-02-01 01:59:24 +01:00

822 lines
30 KiB
Python

# -*- coding: utf-8 -*-
# ***************************************************************************
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
# * *
# * 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 Path.Post.Processor import PostProcessorFactory # PostProcessor,
from PySide import QtCore
from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Path
import Path.Base.SetupSheet as PathSetupSheet
import Path.Base.Util as PathUtil
import Path.Main.Stock as PathStock
import Path.Tool.Controller as PathToolController
import json
import time
# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader
Draft = LazyLoader("Draft", globals(), "Draft")
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 JobTemplate:
"""Attribute and sub element strings for template export/import."""
Description = "Desc"
GeometryTolerance = "Tolerance"
Job = "Job"
PostProcessor = "Post"
PostProcessorArgs = "PostArgs"
PostProcessorOutputFile = "Output"
Fixtures = "Fixtures"
OrderOutputBy = "OrderOutputBy"
SplitOutput = "SplitOutput"
SetupSheet = "SetupSheet"
Stock = "Stock"
# TCs are grouped under Tools in a job, the template refers to them directly though
ToolController = "ToolController"
Version = "Version"
def isResourceClone(obj, propLink, resourceName):
if hasattr(propLink, "PathResource") and (
resourceName is None or resourceName == propLink.PathResource
):
return True
return False
def createResourceClone(obj, orig, name, icon):
clone = Draft.clone(orig)
clone.Label = "%s-%s" % (name, orig.Label)
clone.addProperty("App::PropertyString", "PathResource")
clone.PathResource = name
if clone.ViewObject:
import Path.Base.Gui.IconViewProvider
Path.Base.Gui.IconViewProvider.Attach(clone.ViewObject, icon)
clone.ViewObject.Visibility = False
clone.ViewObject.Transparency = 80
obj.Document.recompute() # necessary to create the clone shape
return clone
def createModelResourceClone(obj, orig):
return createResourceClone(obj, orig, "Model", "BaseGeometry")
class NotificationClass(QtCore.QObject):
updateTC = QtCore.Signal(object, object)
Notification = NotificationClass()
class ObjectJob:
def __init__(self, obj, models, templateFile=None):
self.obj = obj
self.tooltip = None
self.tooltipArgs = None
obj.Proxy = self
obj.addProperty(
"App::PropertyFile",
"PostProcessorOutputFile",
"Output",
QT_TRANSLATE_NOOP("App::Property", "The G-code output file for this project"),
)
obj.addProperty(
"App::PropertyEnumeration",
"PostProcessor",
"Output",
QT_TRANSLATE_NOOP("App::Property", "Select the Post Processor"),
)
obj.addProperty(
"App::PropertyString",
"PostProcessorArgs",
"Output",
QT_TRANSLATE_NOOP(
"App::Property",
"Arguments for the Post Processor (specific to the script)",
),
)
obj.addProperty(
"App::PropertyString",
"LastPostProcessDate",
"Output",
QT_TRANSLATE_NOOP("App::Property", "Last Time the Job was post processed"),
)
obj.setEditorMode("LastPostProcessDate", 2) # Hide
obj.addProperty(
"App::PropertyString",
"LastPostProcessOutput",
"Output",
QT_TRANSLATE_NOOP("App::Property", "Last Time the Job was post processed"),
)
obj.setEditorMode("LastPostProcessOutput", 2) # Hide
obj.addProperty(
"App::PropertyString",
"Description",
"Path",
QT_TRANSLATE_NOOP("App::Property", "An optional description for this job"),
)
obj.addProperty(
"App::PropertyString",
"CycleTime",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Job Cycle Time Estimation"),
)
obj.setEditorMode("CycleTime", 1) # read-only
obj.addProperty(
"App::PropertyDistance",
"GeometryTolerance",
"Geometry",
QT_TRANSLATE_NOOP(
"App::Property",
"For computing Paths; smaller increases accuracy, but slows down computation",
),
)
obj.addProperty(
"App::PropertyLink",
"Stock",
"Base",
QT_TRANSLATE_NOOP("App::Property", "Solid object to be used as stock."),
)
obj.addProperty(
"App::PropertyLink",
"Operations",
"Base",
QT_TRANSLATE_NOOP(
"App::Property",
"Compound path of all operations in the order they are processed.",
),
)
obj.addProperty(
"App::PropertyEnumeration",
"JobType",
"Base",
QT_TRANSLATE_NOOP("App::Property", "Select the Type of Job"),
)
obj.setEditorMode("JobType", 2) # Hide
obj.addProperty(
"App::PropertyBool",
"SplitOutput",
"Output",
QT_TRANSLATE_NOOP("App::Property", "Split output into multiple G-code files"),
)
obj.addProperty(
"App::PropertyEnumeration",
"OrderOutputBy",
"WCS",
QT_TRANSLATE_NOOP("App::Property", "If multiple WCS, order the output this way"),
)
obj.addProperty(
"App::PropertyStringList",
"Fixtures",
"WCS",
QT_TRANSLATE_NOOP("App::Property", "The Work Coordinate Systems for the Job"),
)
obj.Fixtures = ["G54"]
for n in self.propertyEnumerations():
setattr(obj, n[0], n[1])
obj.PostProcessorOutputFile = Path.Preferences.defaultOutputFile()
obj.PostProcessor = postProcessors = Path.Preferences.allEnabledPostProcessors()
defaultPostProcessor = Path.Preferences.defaultPostProcessor()
# Check to see if default post processor hasn't been 'lost' (This can happen when Macro dir has changed)
if defaultPostProcessor in postProcessors:
obj.PostProcessor = defaultPostProcessor
else:
obj.PostProcessor = postProcessors[0]
obj.PostProcessorArgs = Path.Preferences.defaultPostProcessorArgs()
obj.GeometryTolerance = Path.Preferences.defaultGeometryTolerance()
self.setupOperations(obj)
self.setupSetupSheet(obj)
self.setupBaseModel(obj, models)
self.setupToolTable(obj)
self.setFromTemplateFile(obj, templateFile)
self.setupStock(obj)
@classmethod
def propertyEnumerations(self, dataType="data"):
"""propertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
Args:
dataType = 'data', 'raw', 'translated'
Notes:
'data' is list of internal string literals used in code
'raw' is list of (translated_text, data_string) tuples
'translated' is list of translated string literals
"""
enums = {
"OrderOutputBy": [
(translate("CAM_Job", "Fixture"), "Fixture"),
(translate("CAM_Job", "Tool"), "Tool"),
(translate("CAM_Job", "Operation"), "Operation"),
],
"JobType": [
(translate("CAM_Job", "2D"), "2D"),
(translate("CAM_Job", "2.5D"), "2.5D"),
(translate("CAM_Job", "Lathe"), "Lathe"),
(translate("CAM_Job", "Multiaxis"), "Multiaxis"),
],
}
if dataType == "raw":
return enums
data = list()
idx = 0 if dataType == "translated" else 1
Path.Log.debug(enums)
for k, v in enumerate(enums):
data.append((v, [tup[idx] for tup in enums[v]]))
Path.Log.debug(data)
return data
def setupOperations(self, obj):
"""setupOperations(obj)... setup the Operations group for the Job object."""
# ops = FreeCAD.ActiveDocument.addObject(
# "Path::FeatureCompoundPython", "Operations"
# )
ops = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Operations")
if ops.ViewObject:
# ops.ViewObject.Proxy = 0
ops.ViewObject.Visibility = True
obj.Operations = ops
obj.setEditorMode("Operations", 2) # hide
obj.setEditorMode("Placement", 2)
def setupSetupSheet(self, obj):
if not getattr(obj, "SetupSheet", None):
if not hasattr(obj, "SetupSheet"):
obj.addProperty(
"App::PropertyLink",
"SetupSheet",
"Base",
QT_TRANSLATE_NOOP(
"App::Property", "SetupSheet holding the settings for this job"
),
)
obj.SetupSheet = PathSetupSheet.Create()
if obj.SetupSheet.ViewObject:
import Path.Base.Gui.IconViewProvider
Path.Base.Gui.IconViewProvider.Attach(obj.SetupSheet.ViewObject, "SetupSheet")
obj.SetupSheet.Label = "SetupSheet"
self.setupSheet = obj.SetupSheet.Proxy
def setupBaseModel(self, obj, models=None):
Path.Log.track(obj.Label, models)
addModels = False
if not hasattr(obj, "Model"):
obj.addProperty(
"App::PropertyLink",
"Model",
"Base",
QT_TRANSLATE_NOOP("App::Property", "The base objects for all operations"),
)
addModels = True
elif obj.Model is None:
addModels = True
if addModels:
model = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Model")
if model.ViewObject:
model.ViewObject.Visibility = False
if models:
model.addObjects([createModelResourceClone(obj, base) for base in models])
obj.Model = model
obj.Model.Label = "Model"
if hasattr(obj, "Base"):
Path.Log.info("Converting Job.Base to new Job.Model for {}".format(obj.Label))
obj.Model.addObject(obj.Base)
obj.Base = None
obj.removeProperty("Base")
def setupToolTable(self, obj):
addTable = False
if not hasattr(obj, "Tools"):
obj.addProperty(
"App::PropertyLink",
"Tools",
"Base",
QT_TRANSLATE_NOOP(
"App::Property", "Collection of all tool controllers for the job"
),
)
addTable = True
elif obj.Tools is None:
addTable = True
if addTable:
toolTable = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Tools")
toolTable.Label = "Tools"
if toolTable.ViewObject:
toolTable.ViewObject.Visibility = False
if hasattr(obj, "ToolController"):
toolTable.addObjects(obj.ToolController)
obj.removeProperty("ToolController")
obj.Tools = toolTable
def setupStock(self, obj):
"""setupStock(obj)... setup the Stock for the Job object."""
if not obj.Stock:
stockTemplate = Path.Preferences.defaultStockTemplate()
if stockTemplate:
obj.Stock = PathStock.CreateFromTemplate(obj, json.loads(stockTemplate))
if not obj.Stock:
obj.Stock = PathStock.CreateFromBase(obj)
if obj.Stock.ViewObject:
obj.Stock.ViewObject.Visibility = False
def removeBase(self, obj, base, removeFromModel):
if isResourceClone(obj, base, None):
PathUtil.clearExpressionEngine(base)
if removeFromModel:
obj.Model.removeObject(base)
obj.Document.removeObject(base.Name)
def modelBoundBox(self, obj):
return PathStock.shapeBoundBox(obj.Model.Group)
def onDelete(self, obj, arg2=None):
"""Called by the view provider, there doesn't seem to be a callback on the obj itself."""
Path.Log.track(obj.Label, arg2)
doc = obj.Document
if getattr(obj, "Operations", None):
# the first to tear down are the ops, they depend on other resources
Path.Log.debug("taking down ops: %s" % [o.Name for o in self.allOperations()])
while obj.Operations.Group:
op = obj.Operations.Group[0]
if (
not op.ViewObject
or not hasattr(op.ViewObject.Proxy, "onDelete")
or op.ViewObject.Proxy.onDelete(op.ViewObject, ())
):
PathUtil.clearExpressionEngine(op)
doc.removeObject(op.Name)
obj.Operations.Group = []
doc.removeObject(obj.Operations.Name)
obj.Operations = None
# stock could depend on Model, so delete it first
if getattr(obj, "Stock", None):
Path.Log.debug("taking down stock")
PathUtil.clearExpressionEngine(obj.Stock)
doc.removeObject(obj.Stock.Name)
obj.Stock = None
# base doesn't depend on anything inside job
if getattr(obj, "Model", None):
for base in obj.Model.Group:
Path.Log.debug("taking down base %s" % base.Label)
self.removeBase(obj, base, False)
obj.Model.Group = []
doc.removeObject(obj.Model.Name)
obj.Model = None
# Tool controllers might refer to either legacy tool or toolbit
if getattr(obj, "Tools", None):
Path.Log.debug("taking down tool controller")
for tc in obj.Tools.Group:
if hasattr(tc.Tool, "Proxy"):
PathUtil.clearExpressionEngine(tc.Tool)
doc.removeObject(tc.Tool.Name)
PathUtil.clearExpressionEngine(tc)
tc.Proxy.onDelete(tc)
doc.removeObject(tc.Name)
obj.Tools.Group = []
doc.removeObject(obj.Tools.Name)
obj.Tools = None
# SetupSheet
if getattr(obj, "SetupSheet", None):
PathUtil.clearExpressionEngine(obj.SetupSheet)
doc.removeObject(obj.SetupSheet.Name)
obj.SetupSheet = None
return True
def fixupOperations(self, obj):
if getattr(obj.Operations, "ViewObject", None):
try:
obj.Operations.ViewObject.DisplayMode
except Exception:
name = obj.Operations.Name
label = obj.Operations.Label
ops = FreeCAD.ActiveDocument.addObject("Path::FeatureCompoundPython", "Operations")
ops.ViewObject.Proxy = 0
ops.Group = obj.Operations.Group
obj.Operations.Group = []
obj.Operations = ops
FreeCAD.ActiveDocument.removeObject(name)
if label == "Unnamed":
ops.Label = "Operations"
else:
ops.Label = label
def onDocumentRestored(self, obj):
self.setupBaseModel(obj)
self.fixupOperations(obj)
self.setupSetupSheet(obj)
self.setupToolTable(obj)
self.integrityCheck(obj)
obj.setEditorMode("Operations", 2) # hide
obj.setEditorMode("Placement", 2)
if hasattr(obj, "Path"):
obj.Path = Path.Path()
if not hasattr(obj, "CycleTime"):
obj.addProperty(
"App::PropertyString",
"CycleTime",
"Path",
QT_TRANSLATE_NOOP("App::Property", "Operations Cycle Time Estimation"),
)
obj.setEditorMode("CycleTime", 1) # read-only
if not hasattr(obj, "Fixtures"):
obj.addProperty(
"App::PropertyStringList",
"Fixtures",
"WCS",
QT_TRANSLATE_NOOP("App::Property", "The Work Coordinate Systems for the Job"),
)
obj.Fixtures = ["G54"]
if not hasattr(obj, "OrderOutputBy"):
obj.addProperty(
"App::PropertyEnumeration",
"OrderOutputBy",
"WCS",
QT_TRANSLATE_NOOP("App::Property", "If multiple WCS, order the output this way"),
)
obj.OrderOutputBy = ["Fixture", "Tool", "Operation"]
if not hasattr(obj, "SplitOutput"):
obj.addProperty(
"App::PropertyBool",
"SplitOutput",
"Output",
QT_TRANSLATE_NOOP("App::Property", "Split output into multiple G-code files"),
)
obj.SplitOutput = False
if not hasattr(obj, "JobType"):
obj.addProperty(
"App::PropertyEnumeration",
"JobType",
"Base",
QT_TRANSLATE_NOOP("App::Property", "Select the type of Job"),
)
obj.setEditorMode("JobType", 2) # Hide
for n in self.propertyEnumerations():
setattr(obj, n[0], n[1])
def onChanged(self, obj, prop):
if prop == "PostProcessor" and obj.PostProcessor:
processor = PostProcessorFactory.get_post_processor(obj, obj.PostProcessor)
if processor:
self.tooltip = processor.tooltip
self.tooltipArgs = processor.tooltipArgs
def baseObject(self, obj, base):
"""Return the base object, not its clone."""
if isResourceClone(obj, base, "Model") or isResourceClone(obj, base, "Base"):
return base.Objects[0]
return base
def baseObjects(self, obj):
"""Return the base objects, not their clones."""
return [self.baseObject(obj, base) for base in obj.Model.Group]
def resourceClone(self, obj, base):
"""resourceClone(obj, base) ... Return the resource clone for base if it exists."""
if isResourceClone(obj, base, None):
return base
for b in obj.Model.Group:
if base == b.Objects[0]:
return b
return None
def setFromTemplateFile(self, obj, template):
"""setFromTemplateFile(obj, template) ... extract the properties from the given template file and assign to receiver.
This will also create any TCs stored in the template."""
tcs = []
if template:
with open(str(template), "rb") as fp:
attrs = json.load(fp)
if attrs.get(JobTemplate.Version) and 1 == int(attrs[JobTemplate.Version]):
attrs = self.setupSheet.decodeTemplateAttributes(attrs)
if attrs.get(JobTemplate.SetupSheet):
self.setupSheet.setFromTemplate(attrs[JobTemplate.SetupSheet])
if attrs.get(JobTemplate.GeometryTolerance):
obj.GeometryTolerance = float(attrs.get(JobTemplate.GeometryTolerance))
if attrs.get(JobTemplate.PostProcessor):
obj.PostProcessor = attrs.get(JobTemplate.PostProcessor)
if attrs.get(JobTemplate.PostProcessorArgs):
obj.PostProcessorArgs = attrs.get(JobTemplate.PostProcessorArgs)
else:
obj.PostProcessorArgs = ""
if attrs.get(JobTemplate.PostProcessorOutputFile):
obj.PostProcessorOutputFile = attrs.get(JobTemplate.PostProcessorOutputFile)
if attrs.get(JobTemplate.Description):
obj.Description = attrs.get(JobTemplate.Description)
if attrs.get(JobTemplate.ToolController):
for tc in attrs.get(JobTemplate.ToolController):
ctrl = PathToolController.FromTemplate(tc)
if ctrl:
tcs.append(ctrl)
else:
Path.Log.debug(f"skipping TC {tc['name']}")
if attrs.get(JobTemplate.Stock):
obj.Stock = PathStock.CreateFromTemplate(obj, attrs.get(JobTemplate.Stock))
if attrs.get(JobTemplate.Fixtures):
obj.Fixtures = [x for y in attrs.get(JobTemplate.Fixtures) for x in y]
if attrs.get(JobTemplate.OrderOutputBy):
obj.OrderOutputBy = attrs.get(JobTemplate.OrderOutputBy)
if attrs.get(JobTemplate.SplitOutput):
obj.SplitOutput = attrs.get(JobTemplate.SplitOutput)
Path.Log.debug("setting tool controllers (%d)" % len(tcs))
if tcs:
obj.Tools.Group = tcs
else:
Path.Log.error(
"Unsupported PathJob template version {}".format(attrs.get(JobTemplate.Version))
)
if not tcs:
self.addToolController(PathToolController.Create())
def templateAttrs(self, obj):
"""templateAttrs(obj) ... answer a dictionary with all properties of the receiver that should be stored in a template file."""
attrs = {}
attrs[JobTemplate.Version] = 1
if obj.PostProcessor:
attrs[JobTemplate.PostProcessor] = obj.PostProcessor
attrs[JobTemplate.PostProcessorArgs] = obj.PostProcessorArgs
attrs[JobTemplate.Fixtures] = [{f: True} for f in obj.Fixtures]
attrs[JobTemplate.OrderOutputBy] = obj.OrderOutputBy
attrs[JobTemplate.SplitOutput] = obj.SplitOutput
if obj.PostProcessorOutputFile:
attrs[JobTemplate.PostProcessorOutputFile] = obj.PostProcessorOutputFile
attrs[JobTemplate.GeometryTolerance] = str(obj.GeometryTolerance.Value)
if obj.Description:
attrs[JobTemplate.Description] = obj.Description
return attrs
def dumps(self):
return None
def loads(self, state):
for obj in FreeCAD.ActiveDocument.Objects:
if hasattr(obj, "Proxy") and obj.Proxy == self:
self.obj = obj
break
return None
def execute(self, obj):
if getattr(obj, "Operations", None):
# obj.Path = obj.Operations.Path
self.getCycleTime()
if hasattr(obj, "PathChanged"):
obj.PathChanged = True
def getCycleTime(self):
seconds = 0
if len(self.obj.Operations.Group):
for op in self.obj.Operations.Group:
# Skip inactive operations
if PathUtil.opProperty(op, "Active") is False:
continue
# Skip operations that don't have a cycletime attribute
if PathUtil.opProperty(op, "CycleTime") is None:
continue
formattedCycleTime = PathUtil.opProperty(op, "CycleTime")
opCycleTime = 0
try:
# Convert the formatted time from HH:MM:SS to just seconds
opCycleTime = sum(
x * int(t)
for x, t in zip([1, 60, 3600], reversed(formattedCycleTime.split(":")))
)
except Exception:
continue
if opCycleTime > 0:
seconds = seconds + opCycleTime
cycleTimeString = time.strftime("%H:%M:%S", time.gmtime(seconds))
self.obj.CycleTime = cycleTimeString
def addOperation(self, op, before=None, removeBefore=False):
group = self.obj.Operations.Group
if op not in group:
if before:
try:
group.insert(group.index(before), op)
if removeBefore:
group.remove(before)
except Exception as e:
Path.Log.error(e)
group.append(op)
else:
group.append(op)
self.obj.Operations.Group = group
# op.Path.Center = self.obj.Operations.Path.Center
def nextToolNumber(self):
# returns the next available toolnumber in the job
group = self.obj.Tools.Group
if len(group) > 0:
return sorted([t.ToolNumber for t in group])[-1] + 1
else:
return 1
def addToolController(self, tc):
group = self.obj.Tools.Group
Path.Log.debug("addToolController(%s): %s" % (tc.Label, [t.Label for t in group]))
if tc.Name not in [str(t.Name) for t in group]:
tc.setExpression(
"VertRapid",
"%s.%s"
% (
self.setupSheet.expressionReference(),
PathSetupSheet.Template.VertRapid,
),
)
tc.setExpression(
"HorizRapid",
"%s.%s"
% (
self.setupSheet.expressionReference(),
PathSetupSheet.Template.HorizRapid,
),
)
self.obj.Tools.addObject(tc)
Notification.updateTC.emit(self.obj, tc)
def allOperations(self):
ops = []
def collectBaseOps(op):
if hasattr(op, "TypeId"):
if op.TypeId == "Path::FeaturePython":
ops.append(op)
if hasattr(op, "Base"):
collectBaseOps(op.Base)
if op.TypeId == "Path::FeatureCompoundPython":
ops.append(op)
for sub in op.Group:
collectBaseOps(sub)
if getattr(self.obj, "Operations", None) and getattr(self.obj.Operations, "Group", None):
for op in self.obj.Operations.Group:
collectBaseOps(op)
return ops
def setCenterOfRotation(self, center):
if center != self.obj.Path.Center:
self.obj.Path.Center = center
self.obj.Operations.Path.Center = center
for op in self.allOperations():
op.Path.Center = center
def integrityCheck(self, job):
"""integrityCheck(job)... Return True if job has all expected children objects. Attempts to restore any missing children."""
suffix = ""
if len(job.Name) > 3:
suffix = job.Name[3:]
def errorMessage(grp, job):
Path.Log.error("{} corrupt in {} job.".format(grp, job.Name))
if not job.Operations:
self.setupOperations(job)
job.Operations.Label = "Operations" + suffix
if not job.Operations:
errorMessage("Operations", job)
return False
if not job.SetupSheet:
self.setupSetupSheet(job)
job.SetupSheet.Label = "SetupSheet" + suffix
if not job.SetupSheet:
errorMessage("SetupSheet", job)
return False
if not job.Model:
self.setupBaseModel(job)
job.Model.Label = "Model" + suffix
if not job.Model:
errorMessage("Model", job)
return False
if not job.Stock:
self.setupStock(job)
job.Stock.Label = "Stock" + suffix
if not job.Stock:
errorMessage("Stock", job)
return False
if not job.Tools:
self.setupToolTable(job)
job.Tools.Label = "Tools" + suffix
if not job.Tools:
errorMessage("Tools", job)
return False
return True
@classmethod
def baseCandidates(cls):
"""Answer all objects in the current document which could serve as a Base for a job."""
return sorted(
[obj for obj in FreeCAD.ActiveDocument.Objects if cls.isBaseCandidate(obj)],
key=lambda o: o.Label,
)
@classmethod
def isBaseCandidate(cls, obj):
"""Answer true if the given object can be used as a Base for a job."""
return PathUtil.isValidBaseObject(obj)
def Instances():
"""Instances() ... Return all Jobs in the current active document."""
if FreeCAD.ActiveDocument:
return [
job
for job in FreeCAD.ActiveDocument.Objects
if hasattr(job, "Proxy") and isinstance(job.Proxy, ObjectJob)
]
return []
def Create(name, base, templateFile=None):
"""Create(name, base, templateFile=None) ... creates a new job and all it's resources.
If a template file is specified the new job is initialized with the values from the template."""
if isinstance(base[0], str):
models = []
for baseName in base:
models.append(FreeCAD.ActiveDocument.getObject(baseName))
else:
models = base
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
obj.addExtension("App::GroupExtensionPython")
obj.Proxy = ObjectJob(obj, models, templateFile)
return obj