CAM: Improve SelectLoop (#23275)

This commit is contained in:
tarman3
2025-09-12 17:42:20 +03:00
committed by GitHub
parent fff2e2daf9
commit 0336185ec1
2 changed files with 143 additions and 41 deletions

View File

@@ -21,12 +21,13 @@
# ***************************************************************************
import FreeCAD
import Part
import Path
import PathScripts
import traceback
from PathScripts.PathUtils import loopdetect
from PathScripts.PathUtils import horizontalEdgeLoop
from PathScripts.PathUtils import tangentEdgeLoop
from PathScripts.PathUtils import horizontalFaceLoop
from PathScripts.PathUtils import addToJob
from PathScripts.PathUtils import findParentJob
@@ -59,7 +60,8 @@ class _CommandSelectLoop:
"MenuText": QT_TRANSLATE_NOOP("CAM_SelectLoop", "Finish Selecting Loop"),
"Accel": "P, L",
"ToolTip": QT_TRANSLATE_NOOP(
"CAM_SelectLoop", "Completes the selection of edges that form a loop"
"CAM_SelectLoop",
"Completes the selection of edges that form a loop\n Select one edge to search loop edges in horizontal plane\n Select two edges to search loop edges in wires of the shape\n Select one or more vertical faces to search loop faces which form the walls",
),
"CmdType": "ForEdit",
}
@@ -97,7 +99,7 @@ class _CommandSelectLoop:
obj = sel.Object
sub = sel.SubObjects
names = sel.SubElementNames
loopwire = None
loop = None
# Face selection
if "Face" in names[0]:
@@ -107,21 +109,27 @@ class _CommandSelectLoop:
FreeCADGui.Selection.addSelection(obj, loop)
return
# One edge selected
elif len(sub) == 1:
loopwire = horizontalEdgeLoop(obj, sub[0])
elif "Edge" in names[0]:
if len(sub) == 1:
# One edge selected
loop = horizontalEdgeLoop(obj, sub[0], verbose=True)
# Two edges selected
elif len(sub) >= 2:
loopwire = loopdetect(obj, sub[0], sub[1])
if len(sub) >= 2:
# Several edges selected
loop = loopdetect(obj, sub[0], sub[1])
if hasattr(loopwire, "Edges") and loopwire.Edges:
elist = obj.Shape.Edges
if not loop:
# Try to find tangent non planar loop
loop = tangentEdgeLoop(obj, sub[0])
if isinstance(loop, list) and len(loop) > 0 and isinstance(loop[0], Part.Edge):
# Select edges from list
objEdges = obj.Shape.Edges
FreeCADGui.Selection.clearSelection()
for i in loopwire.Edges:
for e in elist:
if e.hashCode() == i.hashCode():
FreeCADGui.Selection.addSelection(obj, f"Edge{elist.index(e) + 1}")
for el in loop:
for eo in objEdges:
if eo.hashCode() == el.hashCode():
FreeCADGui.Selection.addSelection(obj, f"Edge{objEdges.index(eo) + 1}")
return
# Final fallback

View File

@@ -90,12 +90,9 @@ def segments(poly):
def loopdetect(obj, edge1, edge2):
"""
Returns a loop wire that includes the two edges.
"""Returns a loop of edges that includes the two edges.
Useful for detecting boundaries of negative space features ie 'holes'
If a unique loop is not found, returns None
edge1 = edge
edge2 = edge
"""
Path.Log.track()
@@ -110,20 +107,97 @@ def loopdetect(obj, edge1, edge2):
if len(loop) != 1:
return None
loopwire = next(x for x in loop)[1]
return loopwire
return loopwire.Edges
def horizontalEdgeLoop(obj, edge):
"""horizontalEdgeLoop(obj, edge) ... returns a wire in the horizontal plane, if that is the only horizontal wire the given edge is a part of."""
h = edge.hashCode()
wires = [w for w in obj.Shape.Wires if any(e.hashCode() == h for e in w.Edges)]
def horizontalEdgeLoop(obj, edge, verbose=False):
"""Returns a loop of edges in the horizontal plane that includes one edge"""
isHorizontal = Path.Geom.isHorizontal
isRoughly = Path.Geom.isRoughly
if not isHorizontal(edge) and verbose:
# stop if selected edge is not horizontal
return None
# Trying to find edges in loop wires from shape
ehash = edge.hashCode()
wires = [w for w in obj.Shape.Wires if any(e.hashCode() == ehash for e in w.Edges)]
loops = [
w
for w in wires
if all(Path.Geom.isHorizontal(e) for e in w.Edges) and Path.Geom.isHorizontal(Part.Face(w))
w for w in wires if all(isHorizontal(e) for e in w.Edges) and isHorizontal(Part.Face(w))
]
if len(loops) == 1:
return loops[0]
if len(loops) > 0:
return loops[0].Edges
# Trying to find edges in loop without wires from shape
# get edges in horizontal plane with selected edge
candidates = [
e
for e in obj.Shape.Edges
if isHorizontal(e) and isRoughly(e.BoundBox.ZMin, edge.BoundBox.ZMin)
]
# get cluster of edges from which closed wire can be created
# this cluster should contain selected edge
for cluster in Part.getSortedClusters(candidates):
wire = Part.Wire(cluster)
if wire.isClosed() and any(e.hashCode() == ehash for e in cluster):
# cluster is found
return cluster
return None
def tangentEdgeLoop(obj, edge):
"""Returns a tangent loop of edges"""
isCoincide = Path.Geom.pointsCoincide
loop = [edge]
hashes = [edge.hashCode()]
startPoint = edge.Vertexes[0].Point
lastEdge = edge
lastIndex = -1
repeatCount = 0
while repeatCount < len(obj.Shape.Edges):
repeatCount += 1
lastPoint = lastEdge.Vertexes[lastIndex].Point
lastTangent = lastEdge.tangentAt(lastEdge.ParameterRange[lastIndex])
if isCoincide(lastEdge.Vertexes[lastIndex].Point, startPoint):
# stop because return to start point and loop is closed
break
for e in obj.Shape.Edges:
if e.hashCode() in hashes:
# this edge is already in loop
continue
if isCoincide(lastPoint, e.Vertexes[0].Point):
index = 0
elif isCoincide(lastPoint, e.Vertexes[-1].Point):
index = -1
else:
continue
tangent = e.tangentAt(e.ParameterRange[index])
if isCoincide(tangent, lastTangent, 0.05):
# found next tangency edge
loop.append(e)
hashes.append(e.hashCode())
lastEdge = e
lastIndex = -1 if index == 0 else 0
break
else:
# stop because next tangency edge was not found
break
if loop:
return loop
return None
@@ -131,27 +205,45 @@ def horizontalFaceLoop(obj, face, faceList=None):
"""horizontalFaceLoop(obj, face, faceList=None) ... returns a list of face names which form the walls of a vertical hole face is a part of.
All face names listed in faceList must be part of the hole for the solution to be returned."""
wires = [horizontalEdgeLoop(obj, e) for e in face.Edges]
# Not sure if sorting by Area is a premature optimization - but it seems
# the loop we're looking for is typically the biggest of the them all.
wires = sorted([w for w in wires if w], key=lambda w: Part.Face(w).Area)
isVertical = Path.Geom.isVertical
isRoughly = Path.Geom.isRoughly
for wire in wires:
hashes = [e.hashCode() for e in wire.Edges]
if not all(isVertical(obj.Shape.getElement(f)) for f in faceList):
# stop if selected faces is not vertical
Path.Log.warning(
translate(
"CAM",
"Selected faces should be vertical",
)
)
return None
# find all faces that share a an edge with the wire and are vertical
cluster = [horizontalEdgeLoop(obj, e) for e in face.Edges]
# use sorting by Area as simple optimization
clusterSorted = sorted(
[edges for edges in cluster if edges],
key=lambda edges: Part.Face(Part.Wire(Part.sortEdges(edges)[0])).Area,
)
for edges in clusterSorted:
hashes = [e.hashCode() for e in edges]
# find all faces that share an edges and are vertical
faces = [
"Face%d" % (i + 1)
for i, f in enumerate(obj.Shape.Faces)
if any(e.hashCode() in hashes for e in f.Edges) and Path.Geom.isVertical(f)
if any(e.hashCode() in hashes for e in f.Edges) and isVertical(f)
]
if faceList and not all(f in faces for f in faceList):
# not all selected faces in list of candidates faces
continue
# verify they form a valid hole by getting the outline and comparing
# the resulting XY footprint with that of the faces
comp = Part.makeCompound([obj.Shape.getElement(f) for f in faces])
outline = TechDraw.findShapeOutline(comp, 1, Vector(0, 0, 1))
# findShapeOutline always returns closed wires, by removing the
@@ -161,18 +253,20 @@ def horizontalFaceLoop(obj, face, faceList=None):
if any(Path.Geom.edgesMatch(edge, e) for e in uniqueEdges):
continue
uniqueEdges.append(edge)
w = Part.Wire(uniqueEdges)
# if the faces really form the walls of a hole then the resulting
# wire is still closed and it still has the same footprint
bb1 = comp.BoundBox
bb2 = w.BoundBox
prec = 1 # used low precision because findShapeOutline() is dirty
if (
w.isClosed()
and Path.Geom.isRoughly(bb1.XMin, bb2.XMin)
and Path.Geom.isRoughly(bb1.XMax, bb2.XMax)
and Path.Geom.isRoughly(bb1.YMin, bb2.YMin)
and Path.Geom.isRoughly(bb1.YMax, bb2.YMax)
and isRoughly(bb1.XMin, bb2.XMin, prec)
and isRoughly(bb1.XMax, bb2.XMax, prec)
and isRoughly(bb1.YMin, bb2.YMin, prec)
and isRoughly(bb1.YMax, bb2.YMax, prec)
):
return faces
return None