629 lines
23 KiB
Python
629 lines
23 KiB
Python
# -*- coding: utf-8 -*-
|
|
# ***************************************************************************
|
|
# * Copyright (c) 2019 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
|
|
import FreeCAD
|
|
import Part
|
|
import Path
|
|
import math
|
|
|
|
# lazily loaded modules
|
|
from lazy_loader.lazy_loader import LazyLoader
|
|
|
|
PathUtils = LazyLoader("PathScripts.PathUtils", globals(), "PathScripts.PathUtils")
|
|
|
|
|
|
__title__ = "CAM Features Extensions"
|
|
__author__ = "sliptonic (Brad Collette)"
|
|
__url__ = "https://www.freecad.org"
|
|
__doc__ = "Class and implementation of face extensions features."
|
|
|
|
|
|
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
|
|
|
|
|
|
def endPoints(edgeOrWire):
|
|
"""endPoints(edgeOrWire) ... return the first and last point of the wire or the edge, assuming the argument is not a closed wire."""
|
|
if Part.Wire == type(edgeOrWire):
|
|
# edges = edgeOrWire.Edges
|
|
pts = [e.valueAt(e.FirstParameter) for e in edgeOrWire.Edges]
|
|
pts.extend([e.valueAt(e.LastParameter) for e in edgeOrWire.Edges])
|
|
unique = []
|
|
for p in pts:
|
|
cnt = len([p2 for p2 in pts if Path.Geom.pointsCoincide(p, p2)])
|
|
if 1 == cnt:
|
|
unique.append(p)
|
|
|
|
return unique
|
|
|
|
pfirst = edgeOrWire.valueAt(edgeOrWire.FirstParameter)
|
|
plast = edgeOrWire.valueAt(edgeOrWire.LastParameter)
|
|
if Path.Geom.pointsCoincide(pfirst, plast):
|
|
return None
|
|
|
|
return [pfirst, plast]
|
|
|
|
|
|
def includesPoint(p, pts):
|
|
"""includesPoint(p, pts) ... answer True if the collection of pts includes the point p"""
|
|
for pt in pts:
|
|
if Path.Geom.pointsCoincide(p, pt):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def selectOffsetWire(feature, wires):
|
|
"""selectOffsetWire(feature, wires) ... returns the Wire in wires which is does not intersect with feature"""
|
|
closest = None
|
|
for w in wires:
|
|
dist = feature.distToShape(w)[0]
|
|
if closest is None or dist > closest[0]:
|
|
closest = (dist, w)
|
|
|
|
if closest is not None:
|
|
return closest[1]
|
|
|
|
return None
|
|
|
|
|
|
def extendWire(feature, wire, length):
|
|
"""extendWire(wire, length) ... return a closed Wire which extends wire by length"""
|
|
Path.Log.track(length)
|
|
|
|
if not length or length == 0:
|
|
return None
|
|
|
|
try:
|
|
off2D = wire.makeOffset2D(length)
|
|
except FreeCAD.Base.FreeCADError as ee:
|
|
Path.Log.debug(ee)
|
|
return None
|
|
endPts = endPoints(wire) # Assumes wire is NOT closed
|
|
if endPts:
|
|
edges = [
|
|
e
|
|
for e in off2D.Edges
|
|
if Part.Circle != type(e.Curve) or not includesPoint(e.Curve.Center, endPts)
|
|
]
|
|
wires = [Part.Wire(e) for e in Part.sortEdges(edges)]
|
|
offset = selectOffsetWire(feature, wires)
|
|
ePts = endPoints(offset)
|
|
if ePts and len(ePts) > 1:
|
|
l0 = (ePts[0] - endPts[0]).Length
|
|
l1 = (ePts[1] - endPts[0]).Length
|
|
edges = wire.Edges
|
|
if l0 < l1:
|
|
edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[0])))
|
|
edges.extend(offset.Edges)
|
|
edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[1])))
|
|
else:
|
|
edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[0])))
|
|
edges.extend(offset.Edges)
|
|
edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[1])))
|
|
|
|
return Part.Wire(edges)
|
|
|
|
return None
|
|
|
|
|
|
def createExtension(obj, extObj, extFeature, extSub):
|
|
return Extension(
|
|
obj,
|
|
extObj,
|
|
extFeature,
|
|
extSub,
|
|
obj.ExtensionLengthDefault,
|
|
Extension.DirectionNormal,
|
|
)
|
|
|
|
|
|
def readObjExtensionFeature(obj):
|
|
"""readObjExtensionFeature(obj)...
|
|
Return three item string tuples (base name, feature, subfeature) extracted from obj.ExtensionFeature
|
|
"""
|
|
extensions = []
|
|
|
|
for extObj, features in obj.ExtensionFeature:
|
|
for sub in features:
|
|
extFeature, extSub = sub.split(":")
|
|
extensions.append((extObj.Name, extFeature, extSub))
|
|
return extensions
|
|
|
|
|
|
def getExtensions(obj):
|
|
Path.Log.debug("getExtenstions()")
|
|
extensions = []
|
|
i = 0
|
|
|
|
for extObj, features in obj.ExtensionFeature:
|
|
for sub in features:
|
|
extFeature, extSub = sub.split(":")
|
|
extensions.append(createExtension(obj, extObj, extFeature, extSub))
|
|
i = i + 1
|
|
return extensions
|
|
|
|
|
|
def setExtensions(obj, extensions):
|
|
Path.Log.track(obj.Label, len(extensions))
|
|
obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions]
|
|
|
|
|
|
def getStandardAngle(x, y):
|
|
"""getStandardAngle(x, y)...
|
|
Return standard degree angle given x and y values of vector."""
|
|
angle = math.degrees(math.atan2(y, x))
|
|
if angle < 0.0:
|
|
return angle + 360.0
|
|
return angle
|
|
|
|
|
|
def arcAdjustmentAngle(arc1, arc2):
|
|
"""arcAdjustmentAngle(arc1, arc2)...
|
|
Return adjustment angle to apply to arc2 in order to align it with arc1.
|
|
Arcs must have same center point."""
|
|
center = arc1.Curve.Center
|
|
cntr2 = arc2.Curve.Center
|
|
|
|
# Verify centers of arcs are same
|
|
if center.sub(cntr2).Length > 0.0000001:
|
|
return None
|
|
|
|
# Calculate midpoint of arc1, and standard angle from center to that midpoint
|
|
midPntArc1 = arc1.valueAt(
|
|
arc1.FirstParameter + (arc1.LastParameter - arc1.FirstParameter) / 2.0
|
|
)
|
|
midPntVect1 = midPntArc1.sub(center)
|
|
ang1 = getStandardAngle(midPntVect1.x, midPntVect1.y)
|
|
|
|
# Calculate midpoint of arc2, and standard angle from center to that midpoint
|
|
midPntArc2 = arc2.valueAt(
|
|
arc2.FirstParameter + (arc2.LastParameter - arc2.FirstParameter) / 2.0
|
|
)
|
|
midPntVect2 = midPntArc2.sub(center)
|
|
ang2 = getStandardAngle(midPntVect2.x, midPntVect2.y)
|
|
|
|
# Return adjustment angle to apply to arc2 in order to align with arc1
|
|
return ang1 - ang2
|
|
|
|
|
|
class Extension(object):
|
|
DirectionNormal = 0
|
|
DirectionX = 1
|
|
DirectionY = 2
|
|
|
|
def __init__(self, op, obj, feature, sub, length, direction):
|
|
Path.Log.debug(
|
|
"Extension(%s, %s, %s, %.2f, %s" % (obj.Label, feature, sub, length, direction)
|
|
)
|
|
self.op = op
|
|
self.obj = obj
|
|
self.feature = feature
|
|
self.sub = sub
|
|
self.length = length
|
|
self.direction = direction
|
|
self.extFaces = None
|
|
self.isDebug = True if Path.Log.getLevel(Path.Log.thisModule()) == 4 else False
|
|
|
|
self.avoid = False
|
|
if sub.startswith("Avoid_"):
|
|
self.avoid = True
|
|
|
|
self.wire = None
|
|
|
|
def getSubLink(self):
|
|
return "%s:%s" % (self.feature, self.sub)
|
|
|
|
def _extendEdge(self, feature, e0, direction):
|
|
Path.Log.track(feature, e0, direction)
|
|
if isinstance(e0.Curve, Part.Line) or isinstance(e0.Curve, Part.LineSegment):
|
|
e2 = e0.copy()
|
|
off = self.length.Value * direction
|
|
e2.translate(off)
|
|
e2 = Path.Geom.flipEdge(e2)
|
|
e1 = Part.Edge(
|
|
Part.LineSegment(e0.valueAt(e0.LastParameter), e2.valueAt(e2.FirstParameter))
|
|
)
|
|
e3 = Part.Edge(
|
|
Part.LineSegment(e2.valueAt(e2.LastParameter), e0.valueAt(e0.FirstParameter))
|
|
)
|
|
wire = Part.Wire([e0, e1, e2, e3])
|
|
self.wire = wire
|
|
return wire
|
|
|
|
return extendWire(feature, Part.Wire([e0]), self.length.Value)
|
|
|
|
def _getEdgeNumbers(self):
|
|
if "Wire" in self.sub:
|
|
numbers = [nr for nr in self.sub[5:-1].split(",")]
|
|
else:
|
|
numbers = [self.sub[4:]]
|
|
|
|
Path.Log.debug("_getEdgeNumbers() -> %s" % numbers)
|
|
return numbers
|
|
|
|
def _getEdgeNames(self):
|
|
return ["Edge%s" % nr for nr in self._getEdgeNumbers()]
|
|
|
|
def _getEdges(self):
|
|
return [self.obj.Shape.getElement(sub) for sub in self._getEdgeNames()]
|
|
|
|
def _getDirectedNormal(self, p0, normal):
|
|
poffPlus = p0 + 0.01 * normal
|
|
poffMinus = p0 - 0.01 * normal
|
|
if not self.obj.Shape.isInside(poffPlus, 0.005, True):
|
|
return normal
|
|
|
|
if not self.obj.Shape.isInside(poffMinus, 0.005, True):
|
|
return normal.negative()
|
|
|
|
return None
|
|
|
|
def _getDirection(self, wire):
|
|
e0 = wire.Edges[0]
|
|
midparam = e0.FirstParameter + 0.5 * (e0.LastParameter - e0.FirstParameter)
|
|
tangent = e0.tangentAt(midparam)
|
|
Path.Log.track("tangent", tangent, self.feature, self.sub)
|
|
normal = tangent.cross(FreeCAD.Vector(0, 0, 1))
|
|
if Path.Geom.pointsCoincide(normal, FreeCAD.Vector(0, 0, 0)):
|
|
return None
|
|
|
|
return self._getDirectedNormal(e0.valueAt(midparam), normal.normalize())
|
|
|
|
def getExtensionFaces(self, extensionWire):
|
|
"""getExtensionFace(extensionWire)...
|
|
A public helper method to retrieve the requested extension as a face,
|
|
rather than a wire because some extensions require a face shape
|
|
for definition that allows for two wires for boundary definition.
|
|
"""
|
|
|
|
if self.extFaces:
|
|
return self.extFaces
|
|
|
|
return [Part.Face(extensionWire)]
|
|
|
|
def getWire(self):
|
|
"""getWire()... Public method to retrieve the extension area, pertaining to the feature
|
|
and sub element provided at class instantiation, as a closed wire. If no closed wire
|
|
is possible, a `None` value is returned."""
|
|
|
|
return self._getRegularWire()
|
|
|
|
def _getRegularWire(self):
|
|
"""_getRegularWire()... Private method to retrieve the extension area, pertaining to the feature
|
|
and sub element provided at class instantiation, as a closed wire. If no closed wire
|
|
is possible, a `None` value is returned."""
|
|
Path.Log.track()
|
|
|
|
length = self.length.Value
|
|
if Path.Geom.isRoughly(0, length) or not self.sub:
|
|
Path.Log.debug("no extension, length=%.2f, sub=%s" % (length, self.sub))
|
|
return None
|
|
|
|
feature = self.obj.Shape.getElement(self.feature)
|
|
edges = self._getEdges()
|
|
sub = Part.Wire(Part.sortEdges(edges)[0])
|
|
|
|
if 1 == len(edges):
|
|
Path.Log.debug("Extending single edge wire")
|
|
edge = edges[0]
|
|
if Part.Circle == type(edge.Curve):
|
|
Path.Log.debug("is Part.Circle")
|
|
circle = edge.Curve
|
|
# for a circle we have to figure out if it's a hole or a cylinder
|
|
p0 = edge.valueAt(edge.FirstParameter)
|
|
normal = (edge.Curve.Center - p0).normalize()
|
|
direction = self._getDirectedNormal(p0, normal)
|
|
if direction is None:
|
|
return None
|
|
|
|
if Path.Geom.pointsCoincide(normal, direction):
|
|
r = circle.Radius - length
|
|
else:
|
|
r = circle.Radius + length
|
|
|
|
# assuming the offset produces a valid circle - go for it
|
|
if r > 0:
|
|
Path.Log.debug("radius > 0 - extend outward")
|
|
e3 = Part.makeCircle(
|
|
r,
|
|
circle.Center,
|
|
circle.Axis,
|
|
edge.FirstParameter * 180 / math.pi,
|
|
edge.LastParameter * 180 / math.pi,
|
|
)
|
|
|
|
# Determine if rotational alignment is necessary for new arc
|
|
rotationAdjustment = arcAdjustmentAngle(edge, e3)
|
|
if not Path.Geom.isRoughly(rotationAdjustment, 0.0):
|
|
e3.rotate(
|
|
edge.Curve.Center,
|
|
FreeCAD.Vector(0.0, 0.0, 1.0),
|
|
rotationAdjustment,
|
|
)
|
|
|
|
if endPoints(edge):
|
|
Path.Log.debug("Make section of donut")
|
|
# need to construct the arc slice
|
|
e0 = Part.makeLine(
|
|
edge.valueAt(edge.FirstParameter),
|
|
e3.valueAt(e3.FirstParameter),
|
|
)
|
|
e2 = Part.makeLine(
|
|
edge.valueAt(edge.LastParameter),
|
|
e3.valueAt(e3.LastParameter),
|
|
)
|
|
|
|
wire = Part.Wire([e0, edge, e2, e3])
|
|
|
|
# Determine if calculated extension collides with model (wrong direction)
|
|
face = Part.Face(wire)
|
|
if face.common(feature).Area < face.Area * 0.10:
|
|
return wire # Calculated extension is correct
|
|
else:
|
|
return None # Extension collides with model
|
|
|
|
extWire = Part.Wire([e3])
|
|
self.extFaces = [self._makeCircularExtFace(edge, extWire)]
|
|
return extWire
|
|
|
|
Path.Log.debug("radius < 0 - extend inward")
|
|
# the extension is bigger than the hole - so let's just cover the whole hole
|
|
if endPoints(edge):
|
|
# if the resulting arc is smaller than the radius, create a pie slice
|
|
Path.Log.track()
|
|
center = circle.Center
|
|
e0 = Part.makeLine(center, edge.valueAt(edge.FirstParameter))
|
|
e2 = Part.makeLine(edge.valueAt(edge.LastParameter), center)
|
|
return Part.Wire([e0, edge, e2])
|
|
|
|
Path.Log.track()
|
|
return Part.Wire([edge])
|
|
|
|
else:
|
|
Path.Log.debug("else is NOT Part.Circle")
|
|
Path.Log.track(self.feature, self.sub, type(edge.Curve), endPoints(edge))
|
|
direction = self._getDirection(sub)
|
|
if direction is None:
|
|
return None
|
|
|
|
return self._extendEdge(feature, edges[0], direction)
|
|
|
|
elif sub.isClosed():
|
|
Path.Log.debug("Extending multi-edge closed wire")
|
|
subFace = Part.Face(sub)
|
|
featFace = Part.Face(feature.Wires[0])
|
|
isOutside = True
|
|
if not Path.Geom.isRoughly(featFace.Area, subFace.Area):
|
|
length = -1.0 * length
|
|
isOutside = False
|
|
|
|
try:
|
|
off2D = sub.makeOffset2D(length)
|
|
except FreeCAD.Base.FreeCADError as ee:
|
|
Path.Log.debug(ee)
|
|
return None
|
|
|
|
if isOutside:
|
|
self.extFaces = [Part.Face(off2D).cut(featFace)]
|
|
else:
|
|
self.extFaces = [subFace.cut(Part.Face(off2D))]
|
|
return off2D
|
|
|
|
Path.Log.debug("Extending multi-edge open wire")
|
|
extendedWire = extendWire(feature, sub, length)
|
|
if extendedWire is None:
|
|
return extendedWire
|
|
|
|
# Trim wire face using model
|
|
extFace = Part.Face(extendedWire)
|
|
trimmedWire = extFace.cut(self.obj.Shape).Wires[0]
|
|
return trimmedWire.copy()
|
|
|
|
def _makeCircularExtFace(self, edge, extWire):
|
|
"""_makeCircularExtensionFace(edge, extWire)...
|
|
Create proper circular extension face shape. Incoming edge is expected to be a circle.
|
|
"""
|
|
# Add original outer wire to cut faces if necessary
|
|
edgeFace = Part.Face(Part.Wire([edge]))
|
|
edgeFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - edgeFace.BoundBox.ZMin))
|
|
extWireFace = Part.Face(extWire)
|
|
extWireFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - extWireFace.BoundBox.ZMin))
|
|
|
|
if extWireFace.Area >= edgeFace.Area:
|
|
extensionFace = extWireFace.cut(edgeFace)
|
|
else:
|
|
extensionFace = edgeFace.cut(extWireFace)
|
|
extensionFace.translate(FreeCAD.Vector(0.0, 0.0, edge.BoundBox.ZMin))
|
|
|
|
return extensionFace
|
|
|
|
|
|
# Eclass
|
|
|
|
|
|
def initialize_properties(obj):
|
|
"""initialize_properties(obj)... Adds feature properties to object argument"""
|
|
if not hasattr(obj, "ExtensionLengthDefault"):
|
|
obj.addProperty(
|
|
"App::PropertyDistance",
|
|
"ExtensionLengthDefault",
|
|
"Extension",
|
|
QT_TRANSLATE_NOOP("App::Property", "Default length of extensions."),
|
|
)
|
|
if not hasattr(obj, "ExtensionFeature"):
|
|
obj.addProperty(
|
|
"App::PropertyLinkSubListGlobal",
|
|
"ExtensionFeature",
|
|
"Extension",
|
|
QT_TRANSLATE_NOOP("App::Property", "List of features to extend."),
|
|
)
|
|
if not hasattr(obj, "ExtensionCorners"):
|
|
obj.addProperty(
|
|
"App::PropertyBool",
|
|
"ExtensionCorners",
|
|
"Extension",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property",
|
|
"When enabled connected extension edges are combined to wires.",
|
|
),
|
|
)
|
|
obj.ExtensionCorners = True
|
|
|
|
obj.setEditorMode("ExtensionFeature", 2)
|
|
|
|
|
|
def set_default_property_values(obj, job):
|
|
"""set_default_property_values(obj, job) ... set default values for feature properties"""
|
|
obj.ExtensionCorners = True
|
|
obj.setExpression("ExtensionLengthDefault", "OpToolDiameter / 2.0")
|
|
|
|
|
|
def SetupProperties():
|
|
"""SetupProperties()... Returns list of feature property names"""
|
|
setup = ["ExtensionLengthDefault", "ExtensionFeature", "ExtensionCorners"]
|
|
return setup
|
|
|
|
|
|
# Extend outline face generation function
|
|
def getExtendOutlineFace(base_shape, face, extension, remHoles=False, offset_tolerance=1e-4):
|
|
"""getExtendOutlineFace(obj, base_shape, face, extension, remHoles) ...
|
|
Creates an extended face for the pocket, taking into consideration lateral
|
|
collision with the greater base shape.
|
|
Arguments are:
|
|
parent base shape of face,
|
|
target face,
|
|
extension magnitude,
|
|
remove holes boolean,
|
|
offset tolerance = 1e-4 default
|
|
The default value of 1e-4 for offset tolerance is the same default value
|
|
at getOffsetArea() function definition.
|
|
Return is an all access face extending the specified extension value from the source face.
|
|
"""
|
|
|
|
# Make offset face per user-specified extension distance so as to allow full clearing of face where possible.
|
|
offset_face = PathUtils.getOffsetArea(
|
|
face, extension, removeHoles=remHoles, plane=face, tolerance=offset_tolerance
|
|
)
|
|
if not offset_face:
|
|
Path.Log.error("Failed to offset a selected face.")
|
|
return None
|
|
|
|
# Apply collision detection by limiting extended face using base shape
|
|
depth = 0.2
|
|
offset_ext = offset_face.extrude(FreeCAD.Vector(0.0, 0.0, depth))
|
|
face_del = offset_face.extrude(FreeCAD.Vector(0.0, 0.0, -1.0 * depth))
|
|
clear = base_shape.cut(face_del)
|
|
available = offset_ext.cut(clear)
|
|
available.removeSplitter()
|
|
|
|
# Debug
|
|
# Part.show(available)
|
|
# FreeCAD.ActiveDocument.ActiveObject.Label = "available"
|
|
|
|
# Identify bottom face of available volume
|
|
zmin = available.BoundBox.ZMax
|
|
bottom_faces = list()
|
|
for f in available.Faces:
|
|
bbx = f.BoundBox
|
|
zNorm = abs(f.normalAt(0.0, 0.0).z)
|
|
if (
|
|
Path.Geom.isRoughly(zNorm, 1.0)
|
|
and Path.Geom.isRoughly(bbx.ZMax - bbx.ZMin, 0.0)
|
|
and Path.Geom.isRoughly(bbx.ZMin, face.BoundBox.ZMin)
|
|
):
|
|
if bbx.ZMin < zmin:
|
|
bottom_faces.append(f)
|
|
|
|
if bottom_faces:
|
|
extended = None
|
|
for bf in bottom_faces:
|
|
# Drop travel face to same height as source face
|
|
diff = face.BoundBox.ZMax - bf.BoundBox.ZMax
|
|
bf.translate(FreeCAD.Vector(0.0, 0.0, diff))
|
|
cmn = bf.common(face)
|
|
if hasattr(cmn, "Area") and cmn.Area > 0.0:
|
|
extended = bf
|
|
|
|
return extended
|
|
|
|
Path.Log.error("No bottom face for extend outline.")
|
|
return None
|
|
|
|
|
|
# Waterline extension face generation function
|
|
def getWaterlineFace(base_shape, face):
|
|
"""getWaterlineFace(base_shape, face) ...
|
|
Creates a waterline extension face for the target face,
|
|
taking into consideration the greater base shape.
|
|
Arguments are: parent base shape and target face.
|
|
Return is a waterline face at height of the target face.
|
|
"""
|
|
faceHeight = face.BoundBox.ZMin
|
|
|
|
# Get envelope of model to height of face, then fuse with model and refine the shape
|
|
baseBB = base_shape.BoundBox
|
|
depthparams = PathUtils.depth_params(
|
|
clearance_height=faceHeight,
|
|
safe_height=faceHeight,
|
|
start_depth=faceHeight,
|
|
step_down=math.floor(faceHeight - baseBB.ZMin + 2.0),
|
|
z_finish_step=0.0,
|
|
final_depth=baseBB.ZMin,
|
|
user_depths=None,
|
|
)
|
|
env = PathUtils.getEnvelope(partshape=base_shape, subshape=None, depthparams=depthparams)
|
|
# Get top face(s) of envelope at face height
|
|
rawList = list()
|
|
for f in env.Faces:
|
|
if Path.Geom.isRoughly(f.BoundBox.ZMin, faceHeight):
|
|
rawList.append(f)
|
|
# make compound and extrude downward
|
|
rawComp = Part.makeCompound(rawList)
|
|
rawCompExtNeg = rawComp.extrude(FreeCAD.Vector(0.0, 0.0, baseBB.ZMin - faceHeight - 1.0))
|
|
# Cut off bottom of base shape at face height
|
|
topSolid = base_shape.cut(rawCompExtNeg)
|
|
|
|
# Get intersection with base shape
|
|
# The commented version returns waterlines that only intersects horizontal faces at same height as target face
|
|
# cmn = base_shape.common(rawComp)
|
|
# waterlineShape = cmn.cut(topSolid)
|
|
# return waterlineShape
|
|
|
|
# This version returns more of a true waterline flowing from target face
|
|
waterlineShape = rawComp.cut(topSolid)
|
|
faces = list()
|
|
for f in waterlineShape.Faces:
|
|
cmn = face.common(f)
|
|
if hasattr(cmn, "Area") and cmn.Area > 0.0:
|
|
faces.append(f)
|
|
if faces:
|
|
return Part.makeCompound(faces)
|
|
|
|
return None
|