1427 lines
55 KiB
Python
1427 lines
55 KiB
Python
# -*- coding: utf-8 -*-
|
|
# ***************************************************************************
|
|
# * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> *
|
|
# * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> *
|
|
# * Copyright (c) 2020 Schildkroet *
|
|
# * *
|
|
# * 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 Path
|
|
import Path.Base.Drillable as Drillable
|
|
import Path.Op.Area as PathAreaOp
|
|
import Path.Op.Base as PathOp
|
|
import PathScripts.PathUtils as PathUtils
|
|
import math
|
|
import numpy
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
|
|
# lazily loaded modules
|
|
from lazy_loader.lazy_loader import LazyLoader
|
|
|
|
Part = LazyLoader("Part", globals(), "Part")
|
|
DraftGeomUtils = LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils")
|
|
|
|
translate = FreeCAD.Qt.translate
|
|
|
|
__title__ = "CAM Profile Operation"
|
|
__author__ = "sliptonic (Brad Collette)"
|
|
__url__ = "https://www.freecad.org"
|
|
__doc__ = "Create a profile toolpath based on entire model, selected faces or selected edges."
|
|
__contributors__ = "Schildkroet"
|
|
|
|
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())
|
|
|
|
|
|
class ObjectProfile(PathAreaOp.ObjectOp):
|
|
"""Proxy object for Profile operations based on faces."""
|
|
|
|
def areaOpFeatures(self, obj):
|
|
"""areaOpFeatures(obj) ... returns operation-specific features"""
|
|
return PathOp.FeatureBaseFaces | PathOp.FeatureBaseEdges
|
|
|
|
def initAreaOp(self, obj):
|
|
"""initAreaOp(obj) ... creates all profile specific properties."""
|
|
self.propertiesReady = False
|
|
self.initAreaOpProperties(obj)
|
|
|
|
obj.setEditorMode("MiterLimit", 2)
|
|
obj.setEditorMode("JoinType", 2)
|
|
|
|
def initAreaOpProperties(self, obj, warn=False):
|
|
"""initAreaOpProperties(obj) ... create operation specific properties"""
|
|
self.addNewProps = []
|
|
|
|
for propertytype, propertyname, grp, tt in self.areaOpProperties():
|
|
if not hasattr(obj, propertyname):
|
|
obj.addProperty(propertytype, propertyname, grp, tt)
|
|
self.addNewProps.append(propertyname)
|
|
|
|
if len(self.addNewProps) > 0:
|
|
# Set enumeration lists for enumeration properties
|
|
ENUMS = self.areaOpPropertyEnumerations()
|
|
for n in ENUMS:
|
|
if n[0] in self.addNewProps:
|
|
setattr(obj, n[0], n[1])
|
|
if warn:
|
|
newPropMsg = "New property added to"
|
|
newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + ". "
|
|
newPropMsg += "Check its default value." + "\n"
|
|
FreeCAD.Console.PrintWarning(newPropMsg)
|
|
|
|
self.propertiesReady = True
|
|
|
|
def areaOpProperties(self):
|
|
"""areaOpProperties(obj) ... returns a tuples.
|
|
Each tuple contains property declaration information in the
|
|
form of (prototype, name, section, tooltip)."""
|
|
return [
|
|
(
|
|
"App::PropertyEnumeration",
|
|
"Direction",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property",
|
|
"The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)",
|
|
),
|
|
),
|
|
(
|
|
"App::PropertyEnumeration",
|
|
"HandleMultipleFeatures",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property",
|
|
"Choose how to process multiple Base Geometry features.",
|
|
),
|
|
),
|
|
(
|
|
"App::PropertyEnumeration",
|
|
"JoinType",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property",
|
|
"Controls how tool moves around corners. Default=Round",
|
|
),
|
|
),
|
|
(
|
|
"App::PropertyFloat",
|
|
"MiterLimit",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property", "Maximum distance before a miter joint is truncated"
|
|
),
|
|
),
|
|
(
|
|
"App::PropertyDistance",
|
|
"OffsetExtra",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property",
|
|
"Extra value to stay away from final profile- good for roughing toolpath",
|
|
),
|
|
),
|
|
(
|
|
"App::PropertyBool",
|
|
"processHoles",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP("App::Property", "Profile holes as well as the outline"),
|
|
),
|
|
(
|
|
"App::PropertyBool",
|
|
"processPerimeter",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP("App::Property", "Profile the outline"),
|
|
),
|
|
(
|
|
"App::PropertyBool",
|
|
"processCircles",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP("App::Property", "Profile round holes"),
|
|
),
|
|
(
|
|
"App::PropertyEnumeration",
|
|
"Side",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP("App::Property", "Side of edge that tool should cut"),
|
|
),
|
|
(
|
|
"App::PropertyBool",
|
|
"UseComp",
|
|
"Profile",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property", "Make True, if using Cutter Radius Compensation"
|
|
),
|
|
),
|
|
]
|
|
|
|
@classmethod
|
|
def areaOpPropertyEnumerations(self, dataType="data"):
|
|
"""opPropertyEnumerations(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
|
|
"""
|
|
|
|
# Enumeration lists for App::PropertyEnumeration properties
|
|
enums = {
|
|
"Direction": [
|
|
(translate("PathProfile", "CW"), "CW"),
|
|
(translate("PathProfile", "CCW"), "CCW"),
|
|
], # this is the direction that the profile runs
|
|
"HandleMultipleFeatures": [
|
|
(translate("PathProfile", "Collectively"), "Collectively"),
|
|
(translate("PathProfile", "Individually"), "Individually"),
|
|
],
|
|
"JoinType": [
|
|
(translate("PathProfile", "Round"), "Round"),
|
|
(translate("PathProfile", "Square"), "Square"),
|
|
(translate("PathProfile", "Miter"), "Miter"),
|
|
], # this is the direction that the Profile runs
|
|
"Side": [
|
|
(translate("PathProfile", "Outside"), "Outside"),
|
|
(translate("PathProfile", "Inside"), "Inside"),
|
|
], # side of profile that cutter is on in relation to direction of profile
|
|
}
|
|
|
|
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[k] = [tup[idx] for tup in v]
|
|
data.append((v, [tup[idx] for tup in enums[v]]))
|
|
Path.Log.debug(data)
|
|
|
|
return data
|
|
|
|
def areaOpPropertyDefaults(self, obj, job):
|
|
"""areaOpPropertyDefaults(obj, job) ... returns a dictionary of default values
|
|
for the operation's properties."""
|
|
return {
|
|
"Direction": "CW",
|
|
"HandleMultipleFeatures": "Collectively",
|
|
"JoinType": "Round",
|
|
"MiterLimit": 0.1,
|
|
"OffsetExtra": 0.0,
|
|
"Side": "Outside",
|
|
"UseComp": True,
|
|
"processCircles": False,
|
|
"processHoles": False,
|
|
"processPerimeter": True,
|
|
}
|
|
|
|
def areaOpApplyPropertyDefaults(self, obj, job, propList):
|
|
# Set standard property defaults
|
|
PROP_DFLTS = self.areaOpPropertyDefaults(obj, job)
|
|
for n in PROP_DFLTS:
|
|
if n in propList:
|
|
prop = getattr(obj, n)
|
|
val = PROP_DFLTS[n]
|
|
setVal = False
|
|
if hasattr(prop, "Value"):
|
|
if isinstance(val, int) or isinstance(val, float):
|
|
setVal = True
|
|
if setVal:
|
|
# propVal = getattr(prop, 'Value')
|
|
# Need to check if `val` below should be `propVal` commented out above
|
|
setattr(prop, "Value", val)
|
|
else:
|
|
setattr(obj, n, val)
|
|
|
|
def areaOpSetDefaultValues(self, obj, job):
|
|
if self.addNewProps and self.addNewProps.__len__() > 0:
|
|
self.areaOpApplyPropertyDefaults(obj, job, self.addNewProps)
|
|
|
|
def setOpEditorProperties(self, obj):
|
|
"""setOpEditorProperties(obj, porp) ... Process operation-specific changes to properties visibility."""
|
|
fc = 2
|
|
# ml = 0 if obj.JoinType == 'Miter' else 2
|
|
side = 0 if obj.UseComp else 2
|
|
opType = self._getOperationType(obj)
|
|
|
|
if opType == "Contour":
|
|
side = 2
|
|
elif opType == "Face":
|
|
fc = 0
|
|
elif opType == "Edge":
|
|
pass
|
|
|
|
obj.setEditorMode("JoinType", 2)
|
|
obj.setEditorMode("MiterLimit", 2) # ml
|
|
obj.setEditorMode("Side", side)
|
|
obj.setEditorMode("HandleMultipleFeatures", fc)
|
|
obj.setEditorMode("processCircles", fc)
|
|
obj.setEditorMode("processHoles", fc)
|
|
obj.setEditorMode("processPerimeter", fc)
|
|
|
|
def _getOperationType(self, obj):
|
|
if len(obj.Base) == 0:
|
|
return "Contour"
|
|
|
|
# return first geometry type selected
|
|
(_, subsList) = obj.Base[0]
|
|
return subsList[0][:4]
|
|
|
|
def areaOpOnDocumentRestored(self, obj):
|
|
self.propertiesReady = False
|
|
|
|
self.initAreaOpProperties(obj, warn=True)
|
|
self.areaOpSetDefaultValues(obj, PathUtils.findParentJob(obj))
|
|
self.setOpEditorProperties(obj)
|
|
|
|
def areaOpOnChanged(self, obj, prop):
|
|
"""areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties."""
|
|
if prop in ["UseComp", "JoinType", "Base"]:
|
|
if hasattr(self, "propertiesReady") and self.propertiesReady:
|
|
self.setOpEditorProperties(obj)
|
|
|
|
def areaOpAreaParams(self, obj, isHole):
|
|
"""areaOpAreaParams(obj, isHole) ... returns dictionary with area parameters.
|
|
Do not overwrite."""
|
|
params = {}
|
|
params["Fill"] = 0
|
|
params["Coplanar"] = 0
|
|
params["SectionCount"] = -1
|
|
|
|
offset = obj.OffsetExtra.Value # 0.0
|
|
if obj.UseComp:
|
|
offset = self.radius + obj.OffsetExtra.Value
|
|
if obj.Side == "Inside":
|
|
offset = 0 - offset
|
|
if isHole:
|
|
offset = 0 - offset
|
|
params["Offset"] = offset
|
|
|
|
jointype = ["Round", "Square", "Miter"]
|
|
params["JoinType"] = jointype.index(obj.JoinType)
|
|
|
|
if obj.JoinType == "Miter":
|
|
params["MiterLimit"] = obj.MiterLimit
|
|
|
|
if obj.SplitArcs:
|
|
params["Explode"] = True
|
|
params["FitArcs"] = False
|
|
|
|
return params
|
|
|
|
def areaOpPathParams(self, obj, isHole):
|
|
"""areaOpPathParams(obj, isHole) ... returns dictionary with path parameters.
|
|
Do not overwrite."""
|
|
params = {}
|
|
|
|
# Reverse the direction for holes
|
|
if isHole:
|
|
direction = "CW" if obj.Direction == "CCW" else "CCW"
|
|
else:
|
|
direction = obj.Direction
|
|
|
|
if direction == "CCW":
|
|
params["orientation"] = 0
|
|
else:
|
|
params["orientation"] = 1
|
|
|
|
offset = obj.OffsetExtra.Value
|
|
if obj.UseComp:
|
|
offset = self.radius + obj.OffsetExtra.Value
|
|
if offset == 0.0:
|
|
if direction == "CCW":
|
|
params["orientation"] = 1
|
|
else:
|
|
params["orientation"] = 0
|
|
|
|
return params
|
|
|
|
def areaOpUseProjection(self, obj):
|
|
"""areaOpUseProjection(obj) ... returns True"""
|
|
return True
|
|
|
|
def opUpdateDepths(self, obj):
|
|
if hasattr(obj, "Base") and obj.Base.__len__() == 0:
|
|
obj.OpStartDepth = obj.OpStockZMax
|
|
obj.OpFinalDepth = obj.OpStockZMin
|
|
|
|
def areaOpShapes(self, obj):
|
|
"""areaOpShapes(obj) ... returns envelope for all base shapes or wires"""
|
|
|
|
shapes = []
|
|
remainingObjBaseFeatures = []
|
|
self.isDebug = True if Path.Log.getLevel(Path.Log.thisModule()) == 4 else False
|
|
self.inaccessibleMsg = translate(
|
|
"PathProfile",
|
|
"The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.",
|
|
)
|
|
self.offsetExtra = obj.OffsetExtra.Value
|
|
|
|
if self.isDebug:
|
|
for grpNm in ["tmpDebugGrp", "tmpDebugGrp001"]:
|
|
if hasattr(FreeCAD.ActiveDocument, grpNm):
|
|
for go in FreeCAD.ActiveDocument.getObject(grpNm).Group:
|
|
FreeCAD.ActiveDocument.removeObject(go.Name)
|
|
FreeCAD.ActiveDocument.removeObject(grpNm)
|
|
self.tmpGrp = FreeCAD.ActiveDocument.addObject(
|
|
"App::DocumentObjectGroup", "tmpDebugGrp"
|
|
)
|
|
tmpGrpNm = self.tmpGrp.Name
|
|
self.JOB = PathUtils.findParentJob(obj)
|
|
|
|
if obj.UseComp:
|
|
self.useComp = True
|
|
self.ofstRadius = self.radius + self.offsetExtra
|
|
self.commandlist.append(
|
|
Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")")
|
|
)
|
|
else:
|
|
self.useComp = False
|
|
self.ofstRadius = self.offsetExtra
|
|
self.commandlist.append(Path.Command("(Uncompensated Tool Path)"))
|
|
|
|
# Pre-process Base Geometry to process edges
|
|
if (
|
|
obj.Base and len(obj.Base) > 0
|
|
): # The user has selected subobjects from the base. Process each.
|
|
shapes.extend(self._processEdges(obj, remainingObjBaseFeatures))
|
|
Path.Log.track("returned {} shapes".format(len(shapes)))
|
|
|
|
Path.Log.track(remainingObjBaseFeatures)
|
|
if obj.Base and len(obj.Base) > 0 and not remainingObjBaseFeatures:
|
|
# Edges were already processed, or whole model targeted.
|
|
Path.Log.track("remainingObjBaseFeatures is False")
|
|
elif (
|
|
remainingObjBaseFeatures and len(remainingObjBaseFeatures) > 0
|
|
): # Process remaining features after edges processed above.
|
|
for base, subsList in remainingObjBaseFeatures:
|
|
holes = []
|
|
faces = []
|
|
faceDepths = []
|
|
|
|
for sub in subsList:
|
|
shape = getattr(base.Shape, sub)
|
|
# only process faces here
|
|
if isinstance(shape, Part.Face):
|
|
faces.append(shape)
|
|
if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face
|
|
Path.Log.debug(abs(shape.normalAt(0, 0).z))
|
|
for wire in shape.Wires:
|
|
if wire.hashCode() == shape.OuterWire.hashCode():
|
|
continue
|
|
holes.append((base.Shape, wire))
|
|
|
|
# Add face depth to list
|
|
faceDepths.append(shape.BoundBox.ZMin)
|
|
else:
|
|
Path.Log.track()
|
|
ignoreSub = base.Name + "." + sub
|
|
msg = "Found a selected object which is not a face. Ignoring:"
|
|
Path.Log.warning(msg + " {}".format(ignoreSub))
|
|
|
|
for baseShape, wire in holes:
|
|
cont = False
|
|
f = Part.makeFace(wire, "Part::FaceMakerSimple")
|
|
drillable = Drillable.isDrillable(baseShape, f, vector=None)
|
|
Path.Log.debug(drillable)
|
|
|
|
if obj.processCircles:
|
|
if drillable:
|
|
cont = True
|
|
if obj.processHoles:
|
|
if not drillable:
|
|
cont = True
|
|
|
|
if cont:
|
|
shapeEnv = PathUtils.getEnvelope(
|
|
baseShape, subshape=f, depthparams=self.depthparams
|
|
)
|
|
|
|
if shapeEnv:
|
|
self._addDebugObject("HoleShapeEnvelope", shapeEnv)
|
|
tup = shapeEnv, True, "pathProfile"
|
|
shapes.append(tup)
|
|
|
|
if faces and obj.processPerimeter:
|
|
if obj.HandleMultipleFeatures == "Collectively":
|
|
custDepthparams = self.depthparams
|
|
cont = True
|
|
profileshape = Part.makeCompound(faces)
|
|
|
|
try:
|
|
shapeEnv = PathUtils.getEnvelope(
|
|
profileshape, depthparams=custDepthparams
|
|
)
|
|
except Exception as ee:
|
|
# PathUtils.getEnvelope() failed to return an object.
|
|
msg = translate("PathProfile", "Unable to create path for face(s).")
|
|
Path.Log.error(msg + "\n{}".format(ee))
|
|
cont = False
|
|
|
|
if cont:
|
|
self._addDebugObject("CollectCutShapeEnv", shapeEnv)
|
|
tup = shapeEnv, False, "pathProfile"
|
|
shapes.append(tup)
|
|
|
|
elif obj.HandleMultipleFeatures == "Individually":
|
|
for shape in faces:
|
|
custDepthparams = self.depthparams
|
|
self._addDebugObject("Indiv_Shp", shape)
|
|
shapeEnv = PathUtils.getEnvelope(shape, depthparams=custDepthparams)
|
|
if shapeEnv:
|
|
self._addDebugObject("IndivCutShapeEnv", shapeEnv)
|
|
tup = shapeEnv, False, "pathProfile"
|
|
shapes.append(tup)
|
|
|
|
else: # Try to build targets from the job models
|
|
# No base geometry selected, so treating operation like a exterior contour operation
|
|
Path.Log.track()
|
|
self.opUpdateDepths(obj)
|
|
|
|
if 1 == len(self.model) and hasattr(self.model[0], "Proxy"):
|
|
Path.Log.debug("Single model processed.")
|
|
shapes.extend(self._processEachModel(obj))
|
|
else:
|
|
shapes.extend(self._processEachModel(obj))
|
|
|
|
self.removalshapes = shapes
|
|
Path.Log.debug("%d shapes" % len(shapes))
|
|
|
|
# Delete the temporary objects
|
|
if self.isDebug:
|
|
if FreeCAD.GuiUp:
|
|
import FreeCADGui
|
|
|
|
FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False
|
|
self.tmpGrp.purgeTouched()
|
|
|
|
# for shape in shapes:
|
|
# Part.show(shape[0])
|
|
# print(shape)
|
|
return shapes
|
|
|
|
# Method to handle each model as a whole, when no faces are selected
|
|
def _processEachModel(self, obj):
|
|
shapeTups = []
|
|
for base in self.model:
|
|
if hasattr(base, "Shape"):
|
|
env = PathUtils.getEnvelope(
|
|
partshape=base.Shape, subshape=None, depthparams=self.depthparams
|
|
)
|
|
if env:
|
|
shapeTups.append((env, False))
|
|
return shapeTups
|
|
|
|
# Edges pre-processing
|
|
def _processEdges(self, obj, remainingObjBaseFeatures):
|
|
Path.Log.track("remainingObjBaseFeatures: {}".format(remainingObjBaseFeatures))
|
|
shapes = []
|
|
basewires = []
|
|
ezMin = None
|
|
self.cutOut = self.tool.Diameter
|
|
|
|
for base, subsList in obj.Base:
|
|
keepFaces = []
|
|
edgelist = []
|
|
for sub in subsList:
|
|
shape = getattr(base.Shape, sub)
|
|
# extract and process edges
|
|
if isinstance(shape, Part.Edge):
|
|
edgelist.append(getattr(base.Shape, sub))
|
|
# save faces for regular processing
|
|
elif isinstance(shape, Part.Face):
|
|
keepFaces.append(sub)
|
|
if len(edgelist) > 0:
|
|
basewires.append((base, DraftGeomUtils.findWires(edgelist)))
|
|
if ezMin is None or base.Shape.BoundBox.ZMin < ezMin:
|
|
ezMin = base.Shape.BoundBox.ZMin
|
|
|
|
if len(keepFaces) > 0: # save faces for returning and processing
|
|
remainingObjBaseFeatures.append((base, keepFaces))
|
|
|
|
Path.Log.track(basewires)
|
|
for base, wires in basewires:
|
|
for wire in wires:
|
|
if wire.isClosed():
|
|
# Attempt to profile a closed wire
|
|
|
|
# f = Part.makeFace(wire, 'Part::FaceMakerSimple')
|
|
# if planar error, Comment out previous line, uncomment the next two
|
|
(origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value)
|
|
f = flatWire.Wires[0]
|
|
if f:
|
|
shapeEnv = PathUtils.getEnvelope(Part.Face(f), depthparams=self.depthparams)
|
|
if shapeEnv:
|
|
tup = shapeEnv, False, "pathProfile"
|
|
shapes.append(tup)
|
|
else:
|
|
Path.Log.error(self.inaccessibleMsg)
|
|
else:
|
|
# Attempt open-edges profile
|
|
if self.JOB.GeometryTolerance.Value == 0.0:
|
|
msg = self.JOB.Label + ".GeometryTolerance = 0.0. "
|
|
msg += "Please set to an acceptable value greater than zero."
|
|
Path.Log.error(msg)
|
|
else:
|
|
flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value)
|
|
zDiff = math.fabs(wire.BoundBox.ZMin - obj.FinalDepth.Value)
|
|
if flattened and zDiff >= self.JOB.GeometryTolerance.Value:
|
|
cutWireObjs = False
|
|
openEdges = []
|
|
passOffsets = [self.ofstRadius]
|
|
(origWire, flatWire) = flattened
|
|
|
|
self._addDebugObject("FlatWire", flatWire)
|
|
|
|
for po in passOffsets:
|
|
self.ofstRadius = po
|
|
cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire)
|
|
if cutShp:
|
|
cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp)
|
|
|
|
if cutWireObjs:
|
|
for cW in cutWireObjs:
|
|
openEdges.append(cW)
|
|
else:
|
|
Path.Log.error(self.inaccessibleMsg)
|
|
|
|
if openEdges:
|
|
tup = openEdges, False, "OpenEdge"
|
|
shapes.append(tup)
|
|
else:
|
|
if zDiff < self.JOB.GeometryTolerance.Value:
|
|
msg = translate(
|
|
"PathProfile",
|
|
"Check edge selection and Final Depth requirements for profiling open edge(s).",
|
|
)
|
|
Path.Log.error(msg)
|
|
else:
|
|
Path.Log.error(self.inaccessibleMsg)
|
|
|
|
return shapes
|
|
|
|
def _flattenWire(self, obj, wire, trgtDep):
|
|
"""_flattenWire(obj, wire)... Return a flattened version of the wire"""
|
|
Path.Log.debug("_flattenWire()")
|
|
wBB = wire.BoundBox
|
|
|
|
if wBB.ZLength > 0.0:
|
|
Path.Log.debug("Wire is not horizontally co-planar. Flattening it.")
|
|
|
|
# Extrude non-horizontal wire
|
|
extFwdLen = (wBB.ZLength + 2.0) * 2.0
|
|
mbbEXT = wire.extrude(FreeCAD.Vector(0, 0, extFwdLen))
|
|
|
|
# Create cross-section of shape and translate
|
|
sliceZ = wire.BoundBox.ZMin + (extFwdLen / 2)
|
|
crsectFaceShp = self._makeCrossSection(mbbEXT, sliceZ, trgtDep)
|
|
if crsectFaceShp is not False:
|
|
return (wire, crsectFaceShp)
|
|
else:
|
|
return False
|
|
else:
|
|
srtWire = Part.Wire(Part.__sortEdges__(wire.Edges))
|
|
srtWire.translate(FreeCAD.Vector(0, 0, trgtDep - srtWire.BoundBox.ZMin))
|
|
|
|
return (wire, srtWire)
|
|
|
|
# Open-edges methods
|
|
def _getCutAreaCrossSection(self, obj, base, origWire, flatWire):
|
|
Path.Log.debug("_getCutAreaCrossSection()")
|
|
# FCAD = FreeCAD.ActiveDocument
|
|
tolerance = self.JOB.GeometryTolerance.Value
|
|
toolDiam = 2 * self.radius # self.radius defined in PathAreaOp or PathProfileBase modules
|
|
minBfr = toolDiam * 1.25
|
|
bbBfr = (self.ofstRadius * 2) * 1.25
|
|
if bbBfr < minBfr:
|
|
bbBfr = minBfr
|
|
# fwBB = flatWire.BoundBox
|
|
wBB = origWire.BoundBox
|
|
minArea = (self.ofstRadius - tolerance) ** 2 * math.pi
|
|
|
|
useWire = origWire.Wires[0]
|
|
numOrigEdges = len(useWire.Edges)
|
|
sdv = wBB.ZMax
|
|
fdv = obj.FinalDepth.Value
|
|
extLenFwd = sdv - fdv
|
|
if extLenFwd <= 0.0:
|
|
msg = "For open edges, verify Final Depth for this operation."
|
|
FreeCAD.Console.PrintError(msg + "\n")
|
|
# return False
|
|
extLenFwd = 0.1
|
|
WIRE = flatWire.Wires[0]
|
|
numEdges = len(WIRE.Edges)
|
|
|
|
# Identify first/last edges and first/last vertex on wire
|
|
begE = WIRE.Edges[0] # beginning edge
|
|
endE = WIRE.Edges[numEdges - 1] # ending edge
|
|
blen = begE.Length
|
|
elen = endE.Length
|
|
Vb = begE.Vertexes[0] # first vertex of wire
|
|
Ve = endE.Vertexes[1] # last vertex of wire
|
|
pb = FreeCAD.Vector(Vb.X, Vb.Y, fdv)
|
|
pe = FreeCAD.Vector(Ve.X, Ve.Y, fdv)
|
|
|
|
# Obtain beginning point perpendicular points
|
|
if blen > 0.1:
|
|
bcp = begE.valueAt(begE.getParameterByLength(0.1)) # point returned 0.1 mm along edge
|
|
else:
|
|
bcp = FreeCAD.Vector(begE.Vertexes[1].X, begE.Vertexes[1].Y, fdv)
|
|
if elen > 0.1:
|
|
ecp = endE.valueAt(
|
|
endE.getParameterByLength(elen - 0.1)
|
|
) # point returned 0.1 mm along edge
|
|
else:
|
|
ecp = FreeCAD.Vector(endE.Vertexes[1].X, endE.Vertexes[1].Y, fdv)
|
|
|
|
# Create intersection tags for determining which side of wire to cut
|
|
(begInt, begExt, iTAG, eTAG) = self._makeIntersectionTags(useWire, numOrigEdges, fdv)
|
|
if not begInt or not begExt:
|
|
return False
|
|
self.iTAG = iTAG
|
|
self.eTAG = eTAG
|
|
|
|
# Create extended wire boundbox, and extrude
|
|
extBndbox = self._makeExtendedBoundBox(wBB, bbBfr, fdv)
|
|
extBndboxEXT = extBndbox.extrude(FreeCAD.Vector(0, 0, extLenFwd))
|
|
|
|
# Cut model(selected edges) from extended edges boundbox
|
|
cutArea = extBndboxEXT.cut(base.Shape)
|
|
self._addDebugObject("CutArea", cutArea)
|
|
|
|
# Get top and bottom faces of cut area (CA), and combine faces when necessary
|
|
topFc = []
|
|
botFc = []
|
|
bbZMax = cutArea.BoundBox.ZMax
|
|
bbZMin = cutArea.BoundBox.ZMin
|
|
for f in range(0, len(cutArea.Faces)):
|
|
FcBB = cutArea.Faces[f].BoundBox
|
|
if abs(FcBB.ZMax - bbZMax) < tolerance and abs(FcBB.ZMin - bbZMax) < tolerance:
|
|
topFc.append(f)
|
|
if abs(FcBB.ZMax - bbZMin) < tolerance and abs(FcBB.ZMin - bbZMin) < tolerance:
|
|
botFc.append(f)
|
|
if len(topFc) == 0:
|
|
Path.Log.error("Failed to identify top faces of cut area.")
|
|
return False
|
|
topComp = Part.makeCompound([cutArea.Faces[f] for f in topFc])
|
|
topComp.translate(
|
|
FreeCAD.Vector(0, 0, fdv - topComp.BoundBox.ZMin)
|
|
) # Translate face to final depth
|
|
if len(botFc) > 1:
|
|
# Path.Log.debug('len(botFc) > 1')
|
|
bndboxFace = Part.Face(extBndbox.Wires[0])
|
|
tmpFace = Part.Face(extBndbox.Wires[0])
|
|
for f in botFc:
|
|
Q = tmpFace.cut(cutArea.Faces[f])
|
|
tmpFace = Q
|
|
botComp = bndboxFace.cut(tmpFace)
|
|
else:
|
|
botComp = Part.makeCompound(
|
|
[cutArea.Faces[f] for f in botFc]
|
|
) # Part.makeCompound([CA.Shape.Faces[f] for f in botFc])
|
|
botComp.translate(
|
|
FreeCAD.Vector(0, 0, fdv - botComp.BoundBox.ZMin)
|
|
) # Translate face to final depth
|
|
|
|
# Make common of the two
|
|
comFC = topComp.common(botComp)
|
|
|
|
# Determine with which set of intersection tags the model intersects
|
|
(cmnIntArea, cmnExtArea) = self._checkTagIntersection(iTAG, eTAG, "QRY", comFC)
|
|
if cmnExtArea > cmnIntArea:
|
|
Path.Log.debug("Cutting on Ext side.")
|
|
self.cutSide = "E"
|
|
self.cutSideTags = eTAG
|
|
tagCOM = begExt.CenterOfMass
|
|
else:
|
|
Path.Log.debug("Cutting on Int side.")
|
|
self.cutSide = "I"
|
|
self.cutSideTags = iTAG
|
|
tagCOM = begInt.CenterOfMass
|
|
|
|
# Make two beginning style(oriented) 'L' shape stops
|
|
begStop = self._makeStop("BEG", bcp, pb, "BegStop")
|
|
altBegStop = self._makeStop("END", bcp, pb, "BegStop")
|
|
|
|
# Identify to which style 'L' stop the beginning intersection tag is closest,
|
|
# and create partner end 'L' stop geometry, and save for application later
|
|
lenBS_extETag = begStop.CenterOfMass.sub(tagCOM).Length
|
|
lenABS_extETag = altBegStop.CenterOfMass.sub(tagCOM).Length
|
|
if lenBS_extETag < lenABS_extETag:
|
|
endStop = self._makeStop("END", ecp, pe, "EndStop")
|
|
pathStops = Part.makeCompound([begStop, endStop])
|
|
else:
|
|
altEndStop = self._makeStop("BEG", ecp, pe, "EndStop")
|
|
pathStops = Part.makeCompound([altBegStop, altEndStop])
|
|
pathStops.translate(FreeCAD.Vector(0, 0, fdv - pathStops.BoundBox.ZMin))
|
|
|
|
# Identify closed wire in cross-section that corresponds to user-selected edge(s)
|
|
workShp = comFC
|
|
wire = origWire
|
|
WS = workShp.Wires
|
|
lenWS = len(WS)
|
|
wi = 0
|
|
if lenWS < 3:
|
|
# fcShp = workShp
|
|
pass
|
|
else:
|
|
wi = None
|
|
for wvt in wire.Vertexes:
|
|
for w in range(0, lenWS):
|
|
twr = WS[w]
|
|
for v in range(0, len(twr.Vertexes)):
|
|
V = twr.Vertexes[v]
|
|
if abs(V.X - wvt.X) < tolerance:
|
|
if abs(V.Y - wvt.Y) < tolerance:
|
|
# Same vertex found. This wire to be used for offset
|
|
wi = w
|
|
break
|
|
# Efor
|
|
|
|
if wi is None:
|
|
Path.Log.error(
|
|
"The cut area cross-section wire does not coincide with selected edge. Wires[] index is None."
|
|
)
|
|
return False
|
|
else:
|
|
Path.Log.debug("Cross-section Wires[] index is {}.".format(wi))
|
|
|
|
nWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi].Edges))
|
|
fcShp = Part.Face(nWire)
|
|
fcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin))
|
|
# Eif
|
|
|
|
# verify that wire chosen is not inside the physical model
|
|
if wi > 0: # and isInterior is False:
|
|
Path.Log.debug("Multiple wires in cut area. First choice is not 0. Testing.")
|
|
testArea = fcShp.cut(base.Shape)
|
|
|
|
isReady = self._checkTagIntersection(iTAG, eTAG, self.cutSide, testArea)
|
|
Path.Log.debug("isReady {}.".format(isReady))
|
|
|
|
if isReady is False:
|
|
Path.Log.debug("Using wire index {}.".format(wi - 1))
|
|
pWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi - 1].Edges))
|
|
pfcShp = Part.Face(pWire)
|
|
pfcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin))
|
|
workShp = pfcShp.cut(fcShp)
|
|
|
|
if testArea.Area < minArea:
|
|
Path.Log.debug("offset area is less than minArea of {}.".format(minArea))
|
|
Path.Log.debug("Using wire index {}.".format(wi - 1))
|
|
pWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi - 1].Edges))
|
|
pfcShp = Part.Face(pWire)
|
|
pfcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin))
|
|
workShp = pfcShp.cut(fcShp)
|
|
# Eif
|
|
|
|
# Add path stops at ends of wire
|
|
cutShp = workShp.cut(pathStops)
|
|
self._addDebugObject("CutShape", cutShp)
|
|
|
|
return cutShp
|
|
|
|
def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj):
|
|
Path.Log.debug("_checkTagIntersection()")
|
|
# Identify intersection of Common area and Interior Tags
|
|
intCmn = tstObj.common(iTAG)
|
|
|
|
# Identify intersection of Common area and Exterior Tags
|
|
extCmn = tstObj.common(eTAG)
|
|
|
|
# Calculate common intersection (solid model side, or the non-cut side) area with tags, to determine physical cut side
|
|
cmnIntArea = intCmn.Area
|
|
cmnExtArea = extCmn.Area
|
|
if cutSide == "QRY":
|
|
return (cmnIntArea, cmnExtArea)
|
|
|
|
if cmnExtArea > cmnIntArea:
|
|
Path.Log.debug("Cutting on Ext side.")
|
|
if cutSide == "E":
|
|
return True
|
|
else:
|
|
Path.Log.debug("Cutting on Int side.")
|
|
if cutSide == "I":
|
|
return True
|
|
return False
|
|
|
|
def _extractPathWire(self, obj, base, flatWire, cutShp):
|
|
Path.Log.debug("_extractPathWire()")
|
|
|
|
subLoops = []
|
|
rtnWIRES = []
|
|
osWrIdxs = []
|
|
subDistFactor = 1.0 # Raise to include sub wires at greater distance from original
|
|
fdv = obj.FinalDepth.Value
|
|
wire = flatWire
|
|
lstVrtIdx = len(wire.Vertexes) - 1
|
|
lstVrt = wire.Vertexes[lstVrtIdx]
|
|
frstVrt = wire.Vertexes[0]
|
|
cent0 = FreeCAD.Vector(frstVrt.X, frstVrt.Y, fdv)
|
|
cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv)
|
|
|
|
# Calculate offset shape, containing cut region
|
|
ofstShp = self._getOffsetArea(obj, cutShp, False)
|
|
|
|
# CHECK for ZERO area of offset shape
|
|
try:
|
|
if hasattr(ofstShp, "Area"):
|
|
osArea = ofstShp.Area
|
|
if osArea: # Make LGTM parser happy
|
|
pass
|
|
else:
|
|
Path.Log.error("No area to offset shape returned.")
|
|
return []
|
|
except Exception as ee:
|
|
Path.Log.error("No area to offset shape returned.\n{}".format(ee))
|
|
return []
|
|
|
|
self._addDebugObject("OffsetShape", ofstShp)
|
|
|
|
numOSWires = len(ofstShp.Wires)
|
|
for w in range(0, numOSWires):
|
|
osWrIdxs.append(w)
|
|
|
|
# Identify two vertexes for dividing offset loop
|
|
NEAR0 = self._findNearestVertex(ofstShp, cent0)
|
|
# min0i = 0
|
|
min0 = NEAR0[0][4]
|
|
for n in range(0, len(NEAR0)):
|
|
N = NEAR0[n]
|
|
if N[4] < min0:
|
|
min0 = N[4]
|
|
# min0i = n
|
|
(w0, vi0, pnt0, _, _) = NEAR0[0] # min0i
|
|
near0Shp = Part.makeLine(cent0, pnt0)
|
|
self._addDebugObject("Near0", near0Shp)
|
|
|
|
NEAR1 = self._findNearestVertex(ofstShp, cent1)
|
|
# min1i = 0
|
|
min1 = NEAR1[0][4]
|
|
for n in range(0, len(NEAR1)):
|
|
N = NEAR1[n]
|
|
if N[4] < min1:
|
|
min1 = N[4]
|
|
# min1i = n
|
|
(w1, vi1, pnt1, _, _) = NEAR1[0] # min1i
|
|
near1Shp = Part.makeLine(cent1, pnt1)
|
|
self._addDebugObject("Near1", near1Shp)
|
|
|
|
if w0 != w1:
|
|
Path.Log.warning(
|
|
"Offset wire endpoint indexes are not equal - w0, w1: {}, {}".format(w0, w1)
|
|
)
|
|
|
|
# Debugging
|
|
"""
|
|
if self.isDebug:
|
|
Path.Log.debug('min0i is {}.'.format(min0i))
|
|
Path.Log.debug('min1i is {}.'.format(min1i))
|
|
Path.Log.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0]))
|
|
Path.Log.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1]))
|
|
Path.Log.debug('NEAR0 is {}.'.format(NEAR0))
|
|
Path.Log.debug('NEAR1 is {}.'.format(NEAR1))
|
|
"""
|
|
|
|
mainWire = ofstShp.Wires[w0]
|
|
|
|
# Check for additional closed loops in offset wire by checking distance to iTAG or eTAG elements
|
|
if numOSWires > 1:
|
|
# check all wires for proximity(children) to intersection tags
|
|
tagsComList = []
|
|
for T in self.cutSideTags.Faces:
|
|
tcom = T.CenterOfMass
|
|
tv = FreeCAD.Vector(tcom.x, tcom.y, 0.0)
|
|
tagsComList.append(tv)
|
|
subDist = self.ofstRadius * subDistFactor
|
|
for w in osWrIdxs:
|
|
if w != w0:
|
|
cutSub = False
|
|
VTXS = ofstShp.Wires[w].Vertexes
|
|
for V in VTXS:
|
|
v = FreeCAD.Vector(V.X, V.Y, 0.0)
|
|
for t in tagsComList:
|
|
if t.sub(v).Length < subDist:
|
|
cutSub = True
|
|
break
|
|
if cutSub is True:
|
|
break
|
|
if cutSub is True:
|
|
sub = Part.Wire(Part.__sortEdges__(ofstShp.Wires[w].Edges))
|
|
subLoops.append(sub)
|
|
# Eif
|
|
|
|
# Break offset loop into two wires - one of which is the desired profile path wire.
|
|
try:
|
|
(edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(
|
|
mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1]
|
|
)
|
|
except Exception as ee:
|
|
Path.Log.error("Failed to identify offset edge.\n{}".format(ee))
|
|
return False
|
|
edgs0 = []
|
|
edgs1 = []
|
|
for e in edgeIdxs0:
|
|
edgs0.append(mainWire.Edges[e])
|
|
for e in edgeIdxs1:
|
|
edgs1.append(mainWire.Edges[e])
|
|
part0 = Part.Wire(Part.__sortEdges__(edgs0))
|
|
part1 = Part.Wire(Part.__sortEdges__(edgs1))
|
|
|
|
# Determine which part is nearest original edge(s)
|
|
distToPart0 = self._distMidToMid(wire.Wires[0], part0.Wires[0])
|
|
distToPart1 = self._distMidToMid(wire.Wires[0], part1.Wires[0])
|
|
if distToPart0 < distToPart1:
|
|
rtnWIRES.append(part0)
|
|
else:
|
|
rtnWIRES.append(part1)
|
|
rtnWIRES.extend(subLoops)
|
|
|
|
return rtnWIRES
|
|
|
|
def _getOffsetArea(self, obj, fcShape, isHole):
|
|
"""Get an offset area for a shape. Wrapper around
|
|
PathUtils.getOffsetArea."""
|
|
Path.Log.debug("_getOffsetArea()")
|
|
|
|
JOB = PathUtils.findParentJob(obj)
|
|
tolerance = JOB.GeometryTolerance.Value
|
|
offset = self.ofstRadius
|
|
|
|
if isHole is False:
|
|
offset = 0 - offset
|
|
|
|
return PathUtils.getOffsetArea(fcShape, offset, plane=fcShape, tolerance=tolerance)
|
|
|
|
def _findNearestVertex(self, shape, point):
|
|
Path.Log.debug("_findNearestVertex()")
|
|
PT = FreeCAD.Vector(point.x, point.y, 0.0)
|
|
|
|
def sortDist(tup):
|
|
return tup[4]
|
|
|
|
PNTS = []
|
|
for w in range(0, len(shape.Wires)):
|
|
WR = shape.Wires[w]
|
|
V = WR.Vertexes[0]
|
|
P = FreeCAD.Vector(V.X, V.Y, 0.0)
|
|
dist = P.sub(PT).Length
|
|
vi = 0
|
|
pnt = P
|
|
vrt = V
|
|
for v in range(0, len(WR.Vertexes)):
|
|
V = WR.Vertexes[v]
|
|
P = FreeCAD.Vector(V.X, V.Y, 0.0)
|
|
d = P.sub(PT).Length
|
|
if d < dist:
|
|
dist = d
|
|
vi = v
|
|
pnt = P
|
|
vrt = V
|
|
PNTS.append((w, vi, pnt, vrt, dist))
|
|
PNTS.sort(key=sortDist)
|
|
return PNTS
|
|
|
|
def _separateWireAtVertexes(self, wire, VV1, VV2):
|
|
Path.Log.debug("_separateWireAtVertexes()")
|
|
tolerance = self.JOB.GeometryTolerance.Value
|
|
grps = [[], []]
|
|
wireIdxs = [[], []]
|
|
V1 = FreeCAD.Vector(VV1.X, VV1.Y, VV1.Z)
|
|
V2 = FreeCAD.Vector(VV2.X, VV2.Y, VV2.Z)
|
|
|
|
edgeCount = len(wire.Edges)
|
|
FLGS = []
|
|
for e in range(0, edgeCount):
|
|
FLGS.append(0)
|
|
|
|
chk4 = False
|
|
for e in range(0, edgeCount):
|
|
v = 0
|
|
E = wire.Edges[e]
|
|
fv0 = FreeCAD.Vector(E.Vertexes[0].X, E.Vertexes[0].Y, E.Vertexes[0].Z)
|
|
fv1 = FreeCAD.Vector(E.Vertexes[1].X, E.Vertexes[1].Y, E.Vertexes[1].Z)
|
|
|
|
if fv0.sub(V1).Length < tolerance:
|
|
v = 1
|
|
if fv1.sub(V2).Length < tolerance:
|
|
v += 3
|
|
chk4 = True
|
|
elif fv1.sub(V1).Length < tolerance:
|
|
v = 1
|
|
if fv0.sub(V2).Length < tolerance:
|
|
v += 3
|
|
chk4 = True
|
|
|
|
if fv0.sub(V2).Length < tolerance:
|
|
v = 3
|
|
if fv1.sub(V1).Length < tolerance:
|
|
v += 1
|
|
chk4 = True
|
|
elif fv1.sub(V2).Length < tolerance:
|
|
v = 3
|
|
if fv0.sub(V1).Length < tolerance:
|
|
v += 1
|
|
chk4 = True
|
|
FLGS[e] += v
|
|
# Efor
|
|
|
|
# Path.Log.debug('_separateWireAtVertexes() FLGS: {}'.format(FLGS))
|
|
|
|
PRE = []
|
|
POST = []
|
|
IDXS = []
|
|
IDX1 = []
|
|
IDX2 = []
|
|
for e in range(0, edgeCount):
|
|
f = FLGS[e]
|
|
PRE.append(f)
|
|
POST.append(f)
|
|
IDXS.append(e)
|
|
IDX1.append(e)
|
|
IDX2.append(e)
|
|
|
|
PRE.extend(FLGS)
|
|
PRE.extend(POST)
|
|
lenFULL = len(PRE)
|
|
IDXS.extend(IDX1)
|
|
IDXS.extend(IDX2)
|
|
|
|
if chk4 is True:
|
|
# find beginning 1 edge
|
|
begIdx = None
|
|
for e in range(0, lenFULL):
|
|
f = PRE[e]
|
|
i = IDXS[e]
|
|
if f == 4:
|
|
begIdx = e
|
|
grps[0].append(f)
|
|
wireIdxs[0].append(i)
|
|
break
|
|
# find first 3 edge
|
|
for e in range(begIdx + 1, edgeCount + begIdx):
|
|
f = PRE[e]
|
|
i = IDXS[e]
|
|
grps[1].append(f)
|
|
wireIdxs[1].append(i)
|
|
else:
|
|
# find beginning 1 edge
|
|
begIdx = None
|
|
begFlg = False
|
|
for e in range(0, lenFULL):
|
|
f = PRE[e]
|
|
if f == 1:
|
|
if not begFlg:
|
|
begFlg = True
|
|
else:
|
|
begIdx = e
|
|
break
|
|
# find first 3 edge and group all first wire edges
|
|
endIdx = None
|
|
for e in range(begIdx, edgeCount + begIdx):
|
|
f = PRE[e]
|
|
i = IDXS[e]
|
|
if f == 3:
|
|
grps[0].append(f)
|
|
wireIdxs[0].append(i)
|
|
endIdx = e
|
|
break
|
|
else:
|
|
grps[0].append(f)
|
|
wireIdxs[0].append(i)
|
|
# Collect remaining edges
|
|
for e in range(endIdx + 1, lenFULL):
|
|
f = PRE[e]
|
|
i = IDXS[e]
|
|
if f == 1:
|
|
grps[1].append(f)
|
|
wireIdxs[1].append(i)
|
|
break
|
|
else:
|
|
wireIdxs[1].append(i)
|
|
grps[1].append(f)
|
|
# Efor
|
|
# Eif
|
|
|
|
# Debugging
|
|
"""
|
|
if self.isDebug:
|
|
Path.Log.debug('grps[0]: {}'.format(grps[0]))
|
|
Path.Log.debug('grps[1]: {}'.format(grps[1]))
|
|
Path.Log.debug('wireIdxs[0]: {}'.format(wireIdxs[0]))
|
|
Path.Log.debug('wireIdxs[1]: {}'.format(wireIdxs[1]))
|
|
Path.Log.debug('PRE: {}'.format(PRE))
|
|
Path.Log.debug('IDXS: {}'.format(IDXS))
|
|
"""
|
|
return (wireIdxs[0], wireIdxs[1])
|
|
|
|
def _makeCrossSection(self, shape, sliceZ, zHghtTrgt=False):
|
|
"""_makeCrossSection(shape, sliceZ, zHghtTrgt=None)...
|
|
Creates cross-section objectc from shape. Translates cross-section to zHghtTrgt if available.
|
|
Makes face shape from cross-section object. Returns face shape at zHghtTrgt."""
|
|
Path.Log.debug("_makeCrossSection()")
|
|
# Create cross-section of shape and translate
|
|
wires = []
|
|
slcs = shape.slice(FreeCAD.Vector(0, 0, 1), sliceZ)
|
|
if len(slcs) > 0:
|
|
for i in slcs:
|
|
wires.append(i)
|
|
comp = Part.Compound(wires)
|
|
if zHghtTrgt is not False:
|
|
comp.translate(FreeCAD.Vector(0, 0, zHghtTrgt - comp.BoundBox.ZMin))
|
|
return comp
|
|
|
|
return False
|
|
|
|
def _makeExtendedBoundBox(self, wBB, bbBfr, zDep):
|
|
Path.Log.debug("_makeExtendedBoundBox()")
|
|
p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep)
|
|
p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep)
|
|
p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep)
|
|
p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep)
|
|
|
|
L1 = Part.makeLine(p1, p2)
|
|
L2 = Part.makeLine(p2, p3)
|
|
L3 = Part.makeLine(p3, p4)
|
|
L4 = Part.makeLine(p4, p1)
|
|
|
|
return Part.Face(Part.Wire([L1, L2, L3, L4]))
|
|
|
|
def _makeIntersectionTags(self, useWire, numOrigEdges, fdv):
|
|
Path.Log.debug("_makeIntersectionTags()")
|
|
# Create circular probe tags around perimiter of wire
|
|
extTags = []
|
|
intTags = []
|
|
tagRad = self.radius / 2
|
|
tagCnt = 0
|
|
begInt = False
|
|
begExt = False
|
|
for e in range(0, numOrigEdges):
|
|
E = useWire.Edges[e]
|
|
LE = E.Length
|
|
if LE > (self.radius * 2):
|
|
nt = math.ceil(LE / (tagRad * math.pi)) # (tagRad * 2 * math.pi) is circumference
|
|
else:
|
|
nt = 4 # desired + 1
|
|
mid = LE / nt
|
|
spc = self.radius / 10
|
|
for i in range(0, int(nt)):
|
|
if i == 0:
|
|
if e == 0:
|
|
if LE > 0.2:
|
|
aspc = 0.1
|
|
else:
|
|
aspc = LE * 0.75
|
|
cp1 = E.valueAt(E.getParameterByLength(0))
|
|
cp2 = E.valueAt(E.getParameterByLength(aspc))
|
|
(intTObj, extTObj) = self._makeOffsetCircleTag(
|
|
cp1, cp2, tagRad, fdv, "BeginEdge[{}]_".format(e)
|
|
)
|
|
if intTObj and extTObj:
|
|
begInt = intTObj
|
|
begExt = extTObj
|
|
else:
|
|
d = i * mid
|
|
negTestLen = d - spc
|
|
if negTestLen < 0:
|
|
negTestLen = d - (LE * 0.25)
|
|
posTestLen = d + spc
|
|
if posTestLen > LE:
|
|
posTestLen = d + (LE * 0.25)
|
|
cp1 = E.valueAt(E.getParameterByLength(negTestLen))
|
|
cp2 = E.valueAt(E.getParameterByLength(posTestLen))
|
|
(intTObj, extTObj) = self._makeOffsetCircleTag(
|
|
cp1, cp2, tagRad, fdv, "Edge[{}]_".format(e)
|
|
)
|
|
if intTObj and extTObj:
|
|
tagCnt += nt
|
|
intTags.append(intTObj)
|
|
extTags.append(extTObj)
|
|
# tagArea = math.pi * tagRad**2 * tagCnt
|
|
iTAG = Part.makeCompound(intTags)
|
|
eTAG = Part.makeCompound(extTags)
|
|
|
|
return (begInt, begExt, iTAG, eTAG)
|
|
|
|
def _makeOffsetCircleTag(self, p1, p2, cutterRad, depth, lbl, reverse=False):
|
|
# Path.Log.debug('_makeOffsetCircleTag()')
|
|
pb = FreeCAD.Vector(p1.x, p1.y, 0.0)
|
|
pe = FreeCAD.Vector(p2.x, p2.y, 0.0)
|
|
|
|
toMid = pe.sub(pb).multiply(0.5)
|
|
lenToMid = toMid.Length
|
|
if lenToMid == 0.0:
|
|
# Probably a vertical line segment
|
|
return (False, False)
|
|
|
|
cutFactor = (
|
|
cutterRad / 2.1
|
|
) / lenToMid # = 2 is tangent to wire; > 2 allows tag to overlap wire; < 2 pulls tag away from wire
|
|
perpE = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(-1 * cutFactor) # exterior tag
|
|
extPnt = pb.add(toMid.add(perpE))
|
|
|
|
# make exterior tag
|
|
eCntr = extPnt.add(FreeCAD.Vector(0, 0, depth))
|
|
ecw = Part.Wire(Part.makeCircle((cutterRad / 2), eCntr).Edges[0])
|
|
extTag = Part.Face(ecw)
|
|
|
|
# make interior tag
|
|
perpI = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(cutFactor) # interior tag
|
|
intPnt = pb.add(toMid.add(perpI))
|
|
iCntr = intPnt.add(FreeCAD.Vector(0, 0, depth))
|
|
icw = Part.Wire(Part.makeCircle((cutterRad / 2), iCntr).Edges[0])
|
|
intTag = Part.Face(icw)
|
|
|
|
return (intTag, extTag)
|
|
|
|
def _makeStop(self, sType, pA, pB, lbl):
|
|
# Path.Log.debug('_makeStop()')
|
|
ofstRad = self.ofstRadius
|
|
extra = self.radius / 5.0
|
|
lng = 0.05
|
|
med = lng / 2.0
|
|
shrt = lng / 5.0
|
|
|
|
E = FreeCAD.Vector(pB.x, pB.y, 0) # endpoint
|
|
C = FreeCAD.Vector(pA.x, pA.y, 0) # checkpoint
|
|
|
|
if self.useComp is True or (self.useComp is False and self.offsetExtra != 0):
|
|
# 'L' stop shape and edge map
|
|
# --1--
|
|
# | |
|
|
# 2 6
|
|
# | |
|
|
# | ----5----|
|
|
# | 4
|
|
# -----3-------|
|
|
# positive dist in _makePerp2DVector() is CCW rotation
|
|
p1 = E
|
|
if sType == "BEG":
|
|
p2 = self._makePerp2DVector(C, E, -1 * shrt) # E1
|
|
p3 = self._makePerp2DVector(p1, p2, ofstRad + lng + extra) # E2
|
|
p4 = self._makePerp2DVector(p2, p3, shrt + ofstRad + extra) # E3
|
|
p5 = self._makePerp2DVector(p3, p4, lng + extra) # E4
|
|
p6 = self._makePerp2DVector(p4, p5, ofstRad + extra) # E5
|
|
elif sType == "END":
|
|
p2 = self._makePerp2DVector(C, E, shrt) # E1
|
|
p3 = self._makePerp2DVector(p1, p2, -1 * (ofstRad + lng + extra)) # E2
|
|
p4 = self._makePerp2DVector(p2, p3, -1 * (shrt + ofstRad + extra)) # E3
|
|
p5 = self._makePerp2DVector(p3, p4, -1 * (lng + extra)) # E4
|
|
p6 = self._makePerp2DVector(p4, p5, -1 * (ofstRad + extra)) # E5
|
|
p7 = E # E6
|
|
L1 = Part.makeLine(p1, p2)
|
|
L2 = Part.makeLine(p2, p3)
|
|
L3 = Part.makeLine(p3, p4)
|
|
L4 = Part.makeLine(p4, p5)
|
|
L5 = Part.makeLine(p5, p6)
|
|
L6 = Part.makeLine(p6, p7)
|
|
wire = Part.Wire([L1, L2, L3, L4, L5, L6])
|
|
else:
|
|
# 'L' stop shape and edge map
|
|
# :
|
|
# |----2-------|
|
|
# 3 1
|
|
# |-----4------|
|
|
# positive dist in _makePerp2DVector() is CCW rotation
|
|
p1 = E
|
|
if sType == "BEG":
|
|
p2 = self._makePerp2DVector(C, E, -1 * (shrt + abs(self.offsetExtra))) # left, shrt
|
|
p3 = self._makePerp2DVector(p1, p2, shrt + abs(self.offsetExtra))
|
|
p4 = self._makePerp2DVector(p2, p3, (med + abs(self.offsetExtra))) # FIRST POINT
|
|
p5 = self._makePerp2DVector(p3, p4, shrt + abs(self.offsetExtra)) # E1 SECOND
|
|
elif sType == "END":
|
|
p2 = self._makePerp2DVector(C, E, (shrt + abs(self.offsetExtra))) # left, shrt
|
|
p3 = self._makePerp2DVector(p1, p2, -1 * (shrt + abs(self.offsetExtra)))
|
|
p4 = self._makePerp2DVector(
|
|
p2, p3, -1 * (med + abs(self.offsetExtra))
|
|
) # FIRST POINT
|
|
p5 = self._makePerp2DVector(
|
|
p3, p4, -1 * (shrt + abs(self.offsetExtra))
|
|
) # E1 SECOND
|
|
p6 = p1 # E4
|
|
L1 = Part.makeLine(p1, p2)
|
|
L2 = Part.makeLine(p2, p3)
|
|
L3 = Part.makeLine(p3, p4)
|
|
L4 = Part.makeLine(p4, p5)
|
|
L5 = Part.makeLine(p5, p6)
|
|
wire = Part.Wire([L1, L2, L3, L4, L5])
|
|
# Eif
|
|
face = Part.Face(wire)
|
|
self._addDebugObject(lbl, face)
|
|
|
|
return face
|
|
|
|
def _makePerp2DVector(self, v1, v2, dist):
|
|
p1 = FreeCAD.Vector(v1.x, v1.y, 0.0)
|
|
p2 = FreeCAD.Vector(v2.x, v2.y, 0.0)
|
|
toEnd = p2.sub(p1)
|
|
factor = dist / toEnd.Length
|
|
perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0).multiply(factor)
|
|
return p1.add(toEnd.add(perp))
|
|
|
|
def _distMidToMid(self, wireA, wireB):
|
|
mpA = self._findWireMidpoint(wireA)
|
|
mpB = self._findWireMidpoint(wireB)
|
|
return mpA.sub(mpB).Length
|
|
|
|
def _findWireMidpoint(self, wire):
|
|
midPnt = None
|
|
dist = 0.0
|
|
wL = wire.Length
|
|
midW = wL / 2
|
|
|
|
for E in Part.sortEdges(wire.Edges)[0]:
|
|
elen = E.Length
|
|
d_ = dist + elen
|
|
if dist < midW and midW <= d_:
|
|
dtm = midW - dist
|
|
midPnt = E.valueAt(E.getParameterByLength(dtm))
|
|
break
|
|
else:
|
|
dist += elen
|
|
return midPnt
|
|
|
|
# Method to add temporary debug object
|
|
def _addDebugObject(self, objName, objShape):
|
|
if self.isDebug:
|
|
newDocObj = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmp_" + objName)
|
|
newDocObj.Shape = objShape
|
|
newDocObj.purgeTouched()
|
|
self.tmpGrp.addObject(newDocObj)
|
|
|
|
|
|
def SetupProperties():
|
|
setup = PathAreaOp.SetupProperties()
|
|
setup.extend([tup[1] for tup in ObjectProfile.areaOpProperties(False)])
|
|
return setup
|
|
|
|
|
|
def Create(name, obj=None, parentJob=None):
|
|
"""Create(name) ... Creates and returns a Profile based on faces operation."""
|
|
if obj is None:
|
|
obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
|
|
obj.Proxy = ObjectProfile(obj, name, parentJob)
|
|
return obj
|