diff --git a/src/Mod/Path/Gui/Resources/panels/DogboneEdit.ui b/src/Mod/Path/Gui/Resources/panels/DogboneEdit.ui
index 6b7f57f88..d628b7e3d 100644
--- a/src/Mod/Path/Gui/Resources/panels/DogboneEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/DogboneEdit.ui
@@ -6,7 +6,7 @@
0
0
- 352
+ 376
387
@@ -27,8 +27,8 @@
0
0
- 334
- 340
+ 358
+ 333
diff --git a/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui b/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui
index 465b0ad6e..d8289aca7 100644
--- a/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui
+++ b/src/Mod/Path/Gui/Resources/panels/HoldingTagsEdit.ui
@@ -6,8 +6,8 @@
0
0
- 352
- 387
+ 363
+ 530
@@ -27,8 +27,8 @@
0
0
- 334
- 311
+ 345
+ 476
@@ -36,199 +36,118 @@
-
-
-
-
- 0
- 0
-
-
-
- true
-
-
- 80
-
-
- false
-
-
-
- X
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
-
-
-
- Y
-
-
-
-
- Width
-
-
-
-
- Height
-
-
-
-
- Angle
-
-
-
-
- -
-
-
-
-
-
+
-
+
- Delete
+ Width
- -
-
-
- Disable
+
-
+
+
+ <html><head/><body><p>Specify the resulting width of tags at the base.</p><p>The initial default width is based on the longest edge found in the base path.</p></body></html>
- -
-
+
-
+
- Add
+ Height
+
+
+
+ -
+
+
+ <html><head/><body><p>Height of holding tags.</p></body></html>
+
+
+
+ -
+
+
+ Angle
+
+
+
+ -
+
+
+ <html><head/><body><p>Angle of ascend and descend of the tool for the holding tag cutout.</p><p><br/></p><p>If the angle is too flat for the given width to reach the specified height the resulting tag will have a triangular shape and not as high as specified.</p></body></html>
-
-
-
-
-
- 0
- 0
- 334
- 311
-
-
-
- Generate
-
-
- -
-
-
- Width
-
-
-
- -
-
+
-
+
- <html><head/><body><p>Width of each tag.</p></body></html>
+ <html><head/><body><p>List of current tags.</p><p>Edit coordinates to move tag or invoker snapper tool.</p></body></html>
- -
-
-
- Height
-
-
-
- -
-
-
- <html><head/><body><p>The height of the holding tag measured from the bottom of the path. By default this is set to the (estimated) height of the path.</p></body></html>
-
-
-
- -
-
-
- true
-
-
- Angle
-
-
-
- -
-
-
- true
-
-
- <html><head/><body><p>Angle of tag walls.</p></body></html>
-
-
-
- -
-
-
- QDialogButtonBox::Apply|QDialogButtonBox::Ok
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Layout
-
-
+
-
+
+
-
-
+
+
+ false
+
- Count
+ Delete
-
-
-
- <html><head/><body><p>Enter the number of tags you wish to have.</p><p><br/></p><p>Note that sometimes it's necessary to enter a larger than desired count number and disable the ones tags you don't want in order to get the holding tag layout you want.</p></body></html>
+
+
+ Add...
+
+
+
+
+
+
+ -
+
+
+ Auto Generate
+
+
+
-
+
+
+ -
+
+
+ false
+
+
+ Replace All
-
- Spacing
+ Number of Tags
+
+
+ Qt::AlignCenter
- -
-
-
- -
-
-
- Auto Apply
-
-
-
diff --git a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py
index 31d474f44..3be16f87e 100644
--- a/src/Mod/Path/PathScripts/PathDressupHoldingTags.py
+++ b/src/Mod/Path/PathScripts/PathDressupHoldingTags.py
@@ -29,6 +29,9 @@ import Part
import copy
import math
+import cProfile
+import time
+
from PathScripts import PathUtils
from PathScripts.PathGeom import *
from PySide import QtCore, QtGui
@@ -57,7 +60,11 @@ def debugEdge(edge, prefix, force = False):
pf = edge.valueAt(edge.FirstParameter)
pl = edge.valueAt(edge.LastParameter)
if force or debugDressup:
- print("%s %s((%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f))" % (prefix, type(edge.Curve), pf.x, pf.y, pf.z, pl.x, pl.y, pl.z))
+ if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment:
+ print("%s %s((%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f))" % (prefix, type(edge.Curve), pf.x, pf.y, pf.z, pl.x, pl.y, pl.z))
+ else:
+ pm = edge.valueAt((edge.FirstParameter+edge.LastParameter)/2)
+ print("%s %s((%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f))" % (prefix, type(edge.Curve), pf.x, pf.y, pf.z, pm.x, pm.y, pm.z, pl.x, pl.y, pl.z))
def debugMarker(vector, label, color = None, radius = 0.5):
if debugDressup:
@@ -104,18 +111,18 @@ class Tag:
def toString(self):
return str((self.x, self.y, self.width, self.height, self.angle, self.enabled))
- def __init__(self, x, y, width, height, angle, enabled=True, z=None):
- debugPrint("Tag(%.2f, %.2f, %.2f, %.2f, %.2f, %d, %s)" % (x, y, width, height, angle/math.pi, enabled, z))
+ def __init__(self, x, y, width, height, angle, enabled=True):
+ debugPrint("Tag(%.2f, %.2f, %.2f, %.2f, %.2f, %d)" % (x, y, width, height, angle/math.pi, enabled))
self.x = x
self.y = y
- self.z = z
self.width = math.fabs(width)
self.height = math.fabs(height)
self.actualHeight = self.height
self.angle = math.fabs(angle)
self.enabled = enabled
- if z is not None:
- self.createSolidsAt(z)
+
+ def fullWidth(self):
+ return 2 * self.toolRadius + self.width
def originAt(self, z):
return FreeCAD.Vector(self.x, self.y, z)
@@ -126,14 +133,16 @@ class Tag:
def top(self):
return self.z + self.actualHeight
- def createSolidsAt(self, z):
+ def createSolidsAt(self, z, R):
self.z = z
- r1 = self.width / 2
+ self.toolRadius = R
+ r1 = self.fullWidth() / 2
self.r1 = r1
self.r2 = r1
height = self.height
if self.angle == 90 and height > 0:
self.solid = Part.makeCylinder(r1, height)
+ print("Part.makeCone(%f, %f)" % (r1, height))
elif self.angle > 0.0 and height > 0.0:
tangens = math.tan(math.radians(self.angle))
dr = height / tangens
@@ -144,10 +153,17 @@ class Tag:
height = r1 * tangens
self.actualHeight = height
self.r2 = r2
+ print("Part.makeCone(%f, %f, %f)" % (r1, r2, height))
self.solid = Part.makeCone(r1, r2, height)
else:
# degenerated case - no tag
+ print("Part.makeSphere(%f / 10000)" % (r1))
self.solid = Part.makeSphere(r1 / 10000)
+ if not R == 0: # testing is easier if the solid is not rotated
+ angle = -PathGeom.getAngle(self.originAt(0)) * 180 / math.pi
+ print("solid.rotate(%f)" % angle)
+ self.solid.rotate(FreeCAD.Vector(0,0,0), FreeCAD.Vector(0,0,1), angle)
+ print("solid.translate(%s)" % self.originAt(z))
self.solid.translate(self.originAt(z))
def filterIntersections(self, pts, face):
@@ -206,8 +222,9 @@ class Tag:
return None
def intersects(self, edge, param):
- if edge.valueAt(edge.FirstParameter).z < self.top() or edge.valueAt(edge.LastParameter).z < self.top():
- return self.nextIntersectionClosestTo(edge, self.solid, edge.valueAt(param))
+ if self.enabled:
+ if edge.valueAt(edge.FirstParameter).z < self.top() or edge.valueAt(edge.LastParameter).z < self.top():
+ return self.nextIntersectionClosestTo(edge, self.solid, edge.valueAt(param))
return None
class MapWireToTag:
@@ -231,105 +248,131 @@ class MapWireToTag:
self.edges = []
self.entry = i
self.complete = False
- self.wire = None
+ self.haveProblem = False
def addEdge(self, edge):
debugEdge(edge, '..........')
- if self.wire:
- self.wire.add(edge)
- else:
- self.wire = Part.Wire(edge)
+ self.edges.append(edge)
def needToFlipEdge(self, edge, p):
if PathGeom.pointsCoincide(edge.valueAt(edge.LastParameter), p):
return True, edge.valueAt(edge.FirstParameter)
return False, edge.valueAt(edge.LastParameter)
- def isEntryOrExitStrut(self, p1, p2):
- p = PathGeom.xy(p1)
- pEntry0 = PathGeom.xy(self.entry)
- pExit0 = PathGeom.xy(self.exit)
- # it can only be an entry strut if the strut coincides with the entry point and is above it
- if PathGeom.pointsCoincide(p, pEntry0) and p1.z >= self.entry.z and p2.z >= self.entry.z:
- return True
- if PathGeom.pointsCoincide(p, pExit0) and p1.z >= self.exit.z and p2.z >= self.exit.z:
- return True
- return False
+ def isEntryOrExitStrut(self, e):
+ p1 = e.valueAt(e.FirstParameter)
+ p2 = e.valueAt(e.LastParameter)
+ if PathGeom.pointsCoincide(p1, self.entry) and p2.z >= self.entry.z:
+ return 1
+ if PathGeom.pointsCoincide(p2, self.entry) and p1.z >= self.entry.z:
+ return 1
+ if PathGeom.pointsCoincide(p1, self.exit) and p2.z >= self.exit.z:
+ return 2
+ if PathGeom.pointsCoincide(p2, self.exit) and p1.z >= self.exit.z:
+ return 2
+ return 0
-
- def cleanupEdges(self, edges):
- # first remove all internal struts
- debugEdge(Part.Edge(Part.LineSegment(self.entry, self.exit)), '------> cleanupEdges')
- inputEdges = copy.copy(edges)
- plinths = []
+ def cleanupEdges(self, edges, baseEdge):
+ # want to remove all edges from the wire itself, and all internal struts
+ print("+cleanupEdges")
+ print(" base:")
+ debugEdge(baseEdge, ' ', True)
+ print(" edges:")
for e in edges:
- debugEdge(e, '........ cleanup')
- p1 = e.valueAt(e.FirstParameter)
- p2 = e.valueAt(e.LastParameter)
- if PathGeom.pointsCoincide(PathGeom.xy(p1), PathGeom.xy(p2)):
- #it's a strut
- if not self.isEntryOrExitStrut(p1, p2):
- debugEdge(e, '......... X0 %d/%d' % (PathGeom.edgeConnectsTo(e, self.entry), PathGeom.edgeConnectsTo(e, self.exit)))
- inputEdges.remove(e)
- if p1.z > p2.z:
- plinths.append(p2)
- else:
- plinths.append(p1)
- # remove all edges that are connected to the plinths of the (former) internal struts
- for e in copy.copy(inputEdges):
- for p in plinths:
- if PathGeom.edgeConnectsTo(e, p):
- debugEdge(e, '......... X1')
- inputEdges.remove(e)
- break
- # if there are any edges beside a direct edge remaining, the direct edge between
- # entry and exit is redundant
- if len(inputEdges) > 1:
- for e in copy.copy(inputEdges):
- if PathGeom.edgeConnectsTo(e, self.entry) and PathGeom.edgeConnectsTo(e, self.exit):
- debugEdge(e, '......... X2')
- inputEdges.remove(e)
+ debugEdge(e, ' ', True)
+ print(":")
- # the remaining edges form a walk around the tag
- # they need to be ordered and potentially flipped though
+ haveEntry = False
+ for e in copy.copy(edges):
+ if PathGeom.edgesMatch(e, baseEdge):
+ debugEdge(e, '......... X0')
+ edges.remove(e)
+ elif self.isStrut(e):
+ typ = self.isEntryOrExitStrut(e)
+ debugEdge(e, '......... |%d' % typ)
+ if 0 == typ: # neither entry nor exit
+ debugEdge(e, '......... X1')
+ edges.remove(e)
+ elif 1 == typ:
+ haveEntry = True
+
+ print("entry(%.2f, %.2f, %.2f), exit(%.2f, %.2f, %.2f)" % (self.entry.x, self.entry.y, self.entry.z, self.exit.x, self.exit.y, self.exit.z))
+ # the remaininng edges for the path from xy(baseEdge) along the tags surface
outputEdges = []
- p = self.entry
- lastP = p
- while inputEdges:
- for e in inputEdges:
+ p0 = baseEdge.valueAt(baseEdge.FirstParameter)
+ ignoreZ = False
+ if not haveEntry:
+ ignoreZ = True
+ p0 = PathGeom.xy(p0)
+ lastP = p0
+ while edges:
+ print("(%.2f, %.2f, %.2f) %d %d" % (p0.x, p0.y, p0.z, haveEntry, ignoreZ))
+ for e in edges:
p1 = e.valueAt(e.FirstParameter)
p2 = e.valueAt(e.LastParameter)
- if PathGeom.pointsCoincide(p1, p):
- outputEdges.append((e,False))
- inputEdges.remove(e)
- lastP = p
- p = p2
+ if PathGeom.pointsCoincide(PathGeom.xy(p1) if ignoreZ else p1, p0):
+ outputEdges.append((e, False))
+ edges.remove(e)
+ lastP = None
+ ignoreZ = False
+ p0 = p2
debugEdge(e, ">>>>> no flip")
break
- elif PathGeom.pointsCoincide(p2, p):
- outputEdges.append((e,True))
- inputEdges.remove(e)
- lastP = p
- p = p1
+ elif PathGeom.pointsCoincide(PathGeom.xy(p2) if ignoreZ else p2, p0):
+ outputEdges.append((e, True))
+ edges.remove(e)
+ lastP = None
+ ignoreZ = False
+ p0 = p1
debugEdge(e, ">>>>> flip")
break
- #else:
- # debugEdge(e, "<<<<< (%.2f, %.2f, %.2f)" % (p.x, p.y, p.z))
- if lastP == p:
- raise ValueError("No connection to %s" % (p))
- #else:
- # print("xxxxxx (%.2f, %.2f, %.2f)" % (p.x, p.y, p.z))
+ else:
+ debugEdge(e, "<<<<< (%.2f, %.2f, %.2f)" % (p0.x, p0.y, p0.z))
+ if lastP == p0:
+ raise ValueError("No connection to %s" % (p0))
+ elif lastP:
+ print("xxxxxx (%.2f, %.2f, %.2f) (%.2f, %.2f, %.2f)" % (p0.x, p0.y, p0.z, lastP.x, lastP.y, lastP.z))
+ else:
+ print("xxxxxx (%.2f, %.2f, %.2f) -" % (p0.x, p0.y, p0.z))
+ lastP = p0
+ print("-cleanupEdges")
return outputEdges
- def shell(self):
- shell = self.wire.extrude(FreeCAD.Vector(0, 0, 10))
- redundant = filter(lambda f: f.Area == 0, shell.childShapes())
- if redundant:
- return shell.removeShape(redundant)
- return shell
+ def isStrut(self, edge):
+ p1 = PathGeom.xy(edge.valueAt(edge.FirstParameter))
+ p2 = PathGeom.xy(edge.valueAt(edge.LastParameter))
+ return PathGeom.pointsCoincide(p1, p2)
+
+ def cmdsForEdge(self, edge):
+ cmds = []
+
+ # OCC doesn't like it very much if the shapes align with each other. So if we have a slightly
+ # extended edge for the last edge in list we'll use that instead for stable results.
+ if PathGeom.pointsCoincide(edge.valueAt(edge.FirstParameter), self.lastEdge.valueAt(self.lastEdge.FirstParameter)):
+ shell = self.lastEdge.extrude(FreeCAD.Vector(0, 0, self.tag.height + 1))
+ else:
+ shell = edge.extrude(FreeCAD.Vector(0, 0, self.tag.height + 1))
+ shape = shell.common(self.tag.solid)
+
+ if not shape.Edges:
+ self.haveProblem = True
+
+ for e,flip in self.cleanupEdges(shape.Edges, edge):
+ debugEdge(e, '++++++++ %s' % ('.' if not flip else '@'))
+ cmds.extend(PathGeom.cmdsForEdge(e, flip, False))
+ return cmds
+
+ def commandsForEdges(self):
+ commands = []
+ for e in self.edges:
+ if self.isStrut(e):
+ continue
+ commands.extend(self.cmdsForEdge(e))
+ return commands
def add(self, edge):
self.tail = None
+ self.lastEdge = edge # see cmdsForEdge
if self.tag.solid.isInside(edge.valueAt(edge.LastParameter), 0.000001, True):
self.addEdge(edge)
else:
@@ -341,13 +384,8 @@ class MapWireToTag:
self.addEdge(e)
self.tail = tail
self.exit = i
- if self.wire:
- face = self.shell().common(self.tag.solid)
-
- for e,flip in self.cleanupEdges(face.Edges):
- debugEdge(e, '++++++++ %s' % ('.' if not flip else '@'))
- self.commands.extend(PathGeom.cmdsForEdge(e, flip, False))
self.complete = True
+ self.commands.extend(self.commandsForEdges())
def mappingComplete(self):
return self.complete
@@ -399,9 +437,11 @@ class PathData:
def findZLimits(self, edges):
# not considering arcs and spheres in Z direction, find the highes and lowest Z values
- minZ = edges[0].Vertexes[0].Point.z
- maxZ = minZ
+ minZ = 99999999999
+ maxZ = -99999999999
for e in edges:
+ if self.rapid.isRapid(e):
+ continue
for v in e.Vertexes:
if v.Point.z < minZ:
minZ = v.Point.z
@@ -476,10 +516,11 @@ class PathData:
debugPrint(" %d: %d" % (i, count))
#debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2)
#debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2)
- distance = (edge.LastParameter - edge.FirstParameter) / count
- for j in range(0, count):
- tag = edge.Curve.value((j+0.5) * distance)
- tags.append(Tag(tag.x, tag.y, W, H, angle, True))
+ if 0 != count:
+ distance = (edge.LastParameter - edge.FirstParameter) / count
+ for j in range(0, count):
+ tag = edge.Curve.value((j+0.5) * distance)
+ tags.append(Tag(tag.x, tag.y, W, H, angle, True))
return tags
@@ -554,6 +595,7 @@ class ObjectDressup:
return True
def createPath(self, edges, tags, rapid):
+ print("createPath")
commands = []
lastEdge = 0
lastTag = 0
@@ -562,10 +604,11 @@ class ObjectDressup:
inters = None
edge = None
+ self.mappers = []
mapper = None
while edge or lastEdge < len(edges):
- #print("------- lastEdge = %d/%d.%d/%d" % (lastEdge, lastTag, t, len(tags)))
+ debugPrint("------- lastEdge = %d/%d.%d/%d" % (lastEdge, lastTag, t, len(tags)))
if not edge:
edge = edges[lastEdge]
debugEdge(edge, "======= new edge: %d/%d" % (lastEdge, len(edges)))
@@ -587,6 +630,7 @@ class ObjectDressup:
i = tags[tIndex].intersects(edge, edge.FirstParameter)
if i and self.isValidTagStartIntersection(edge, i):
mapper = MapWireToTag(edge, tags[tIndex], i)
+ self.mappers.append(mapper)
edge = mapper.tail
@@ -606,8 +650,17 @@ class ObjectDressup:
# print(cmd)
return Path.Path(commands)
+ def problems(self):
+ return filter(lambda m: m.haveProblem, self.mappers)
def execute(self, obj):
+ #pr = cProfile.Profile()
+ #pr.enable()
+ self.doExecute(obj)
+ #pr.disable()
+ #pr.print_stats()
+
+ def doExecute(self,obj):
if not obj.Base:
return
if not obj.Base.isDerivedFrom("Path::Feature"):
@@ -642,7 +695,7 @@ class ObjectDressup:
tags = pathData.sortedTags(tags)
self.setTags(obj, tags, False)
for tag in tags:
- tag.createSolidsAt(pathData.minZ)
+ tag.createSolidsAt(pathData.minZ, self.toolRadius)
tagID = 0
for tag in tags:
@@ -653,12 +706,13 @@ class ObjectDressup:
if tag.angle != 90:
debugCone(tag.originAt(pathData.minZ), tag.r1, tag.r2, tag.actualHeight, "tag-%02d" % tagID)
else:
- debugCylinder(tag.originAt(pathData.minZ), tag.width/2, tag.actualHeight, "tag-%02d" % tagID)
+ debugCylinder(tag.originAt(pathData.minZ), tag.fullWidth()/2, tag.actualHeight, "tag-%02d" % tagID)
self.fingerprint = [tag.toString() for tag in tags]
self.tags = tags
obj.Path = self.createPath(pathData.edges, tags, pathData.rapid)
+ print("execute - done")
def setTags(self, obj, tags, update = True):
print("setTags(%d, %d)" % (len(tags), update))
@@ -681,32 +735,16 @@ class ObjectDressup:
FreeCAD.Console.PrintError(translate("PathDressup_HoldingTags", "Cannot insert holding tags for this path - please select a Profile path\n"))
return None
- ## setup the object's properties, in case they're not set yet
- #obj.Count = self.tagCount(obj)
- #obj.Angle = self.tagAngle(obj)
- #obj.Blacklist = self.tagBlacklist(obj)
-
- # if the heigt isn't set, use the height of the path
- #if not hasattr(obj, "Height") or not obj.Height:
- # obj.Height = pathData.maxZ - pathData.minZ
- # try and take an educated guess at the width
- #if not hasattr(obj, "Width") or not obj.Width:
- # width = sorted(pathData.base.Edges, key=lambda e: -e.Length)[0].Length / 10
- # while obj.Count > len([e for e in pathData.base.Edges if e.Length > 3*width]):
- # width = widht / 2
- # obj.Width = width
-
- # and the tool radius, not sure yet if it's needed
- #self.toolRadius = 5
- #toolLoad = PathUtils.getLastToolLoad(obj)
- #if toolLoad is None or toolLoad.ToolNumber == 0:
- # self.toolRadius = 5
- #else:
- # tool = PathUtils.getTool(obj, toolLoad.ToolNumber)
- # if not tool or tool.Diameter == 0:
- # self.toolRadius = 5
- # else:
- # self.toolRadius = tool.Diameter / 2
+ self.toolRadius = 5
+ toolLoad = PathUtils.getLastToolLoad(obj)
+ if toolLoad is None or toolLoad.ToolNumber == 0:
+ self.toolRadius = 5
+ else:
+ tool = PathUtils.getTool(obj, toolLoad.ToolNumber)
+ if not tool or tool.Diameter == 0:
+ self.toolRadius = 5
+ else:
+ self.toolRadius = tool.Diameter / 2
self.pathData = pathData
return self.pathData
@@ -723,8 +761,8 @@ class ObjectDressup:
return self.pathData.pathLength()
class TaskPanel:
- DataTag = QtCore.Qt.ItemDataRole.UserRole
- DataValue = QtCore.Qt.ItemDataRole.DisplayRole
+ DataX = QtCore.Qt.ItemDataRole.UserRole
+ DataY = QtCore.Qt.ItemDataRole.UserRole + 1
def __init__(self, obj):
self.obj = obj
@@ -738,6 +776,7 @@ class TaskPanel:
FreeCADGui.Selection.removeObserver(self.s)
def accept(self):
+ self.getFields()
FreeCAD.ActiveDocument.commitTransaction()
FreeCADGui.ActiveDocument.resetEdit()
FreeCADGui.Control.closeDialog()
@@ -750,40 +789,38 @@ class TaskPanel:
# install the function mode resident
FreeCADGui.Selection.addObserver(self.s)
- def tableWidgetItem(self, tag, val):
- item = QtGui.QTableWidgetItem()
- item.setTextAlignment(QtCore.Qt.AlignRight)
- item.setData(self.DataTag, tag)
- item.setData(self.DataValue, val)
- return item
-
def getFields(self):
+ width = self.form.dsbWidth.value()
+ height = self.form.dsbHeight.value()
+ angle = self.form.dsbAngle.value()
tags = []
- for row in range(0, self.form.twTags.rowCount()):
- x = self.form.twTags.item(row, 0).data(self.DataValue)
- y = self.form.twTags.item(row, 1).data(self.DataValue)
- w = self.form.twTags.item(row, 2).data(self.DataValue)
- h = self.form.twTags.item(row, 3).data(self.DataValue)
- a = self.form.twTags.item(row, 4).data(self.DataValue)
- tags.append(Tag(x, y, w, h, a, True))
- print("getFields: %d" % (len(tags)))
+ for i in range(0, self.form.lwTags.count()):
+ item = self.form.lwTags.item(i)
+ enabled = item.checkState() == QtCore.Qt.CheckState.Checked
+ x = item.data(self.DataX)
+ y = item.data(self.DataY)
+ tags.append(Tag(x, y, width, height, angle, enabled))
self.obj.Proxy.setTags(self.obj, tags)
- def updateTags(self):
+ def updateTagsView(self):
self.tags = self.obj.Proxy.getTags(self.obj)
- self.form.twTags.blockSignals(True)
- self.form.twTags.setSortingEnabled(False)
- self.form.twTags.clearSpans()
- print("updateTags: %d" % (len(self.tags)))
- self.form.twTags.setRowCount(len(self.tags))
- for row, tag in enumerate(self.tags):
- self.form.twTags.setItem(row, 0, self.tableWidgetItem(tag, tag.x))
- self.form.twTags.setItem(row, 1, self.tableWidgetItem(tag, tag.y))
- self.form.twTags.setItem(row, 2, self.tableWidgetItem(tag, tag.width))
- self.form.twTags.setItem(row, 3, self.tableWidgetItem(tag, tag.height))
- self.form.twTags.setItem(row, 4, self.tableWidgetItem(tag, tag.angle))
- self.form.twTags.setSortingEnabled(True)
- self.form.twTags.blockSignals(False)
+ print("updateTagsView: %d" % (len(self.tags)))
+ self.form.lwTags.blockSignals(True)
+ self.form.lwTags.clear()
+ for tag in self.tags:
+ lbl = "(%.2f, %.2f)" % (tag.x, tag.y)
+ item = QtGui.QListWidgetItem(lbl)
+ item.setData(self.DataX, tag.x)
+ item.setData(self.DataY, tag.y)
+ if tag.enabled:
+ item.setCheckState(QtCore.Qt.CheckState.Checked)
+ else:
+ item.setCheckState(QtCore.Qt.CheckState.Unchecked)
+ flags = QtCore.Qt.ItemFlag.ItemIsSelectable
+ flags |= QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsUserCheckable
+ item.setFlags(flags)
+ self.form.lwTags.addItem(item)
+ self.form.lwTags.blockSignals(False)
def cleanupUI(self):
print("cleanupUI")
@@ -792,67 +829,50 @@ class TaskPanel:
if obj.Name.startswith('tag'):
FreeCAD.ActiveDocument.removeObject(obj.Name)
- def updateUI(self):
- print("updateUI")
- self.cleanupUI()
- self.getFields()
- if debugDressup:
- FreeCAD.ActiveDocument.recompute()
-
-
- def whenApplyClicked(self):
- print("whenApplyClicked")
+ def generateNewTags(self):
+ print("generateNewTags")
self.cleanupUI()
count = self.form.sbCount.value()
- spacing = self.form.dsbSpacing.value()
width = self.form.dsbWidth.value()
height = self.form.dsbHeight.value()
angle = self.form.dsbAngle.value()
- tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle, spacing * 0.99)
+ tags = self.obj.Proxy.generateTags(self.obj, count, width, height, angle)
self.obj.Proxy.setTags(self.obj, tags)
- self.updateTags()
- if debugDressup:
- # this causes a big of an echo and a double click on the spin buttons, don't know why though
- FreeCAD.ActiveDocument.recompute()
+ self.updateTagsView()
+ #if debugDressup:
+ # # this causes a big of an echo and a double click on the spin buttons, don't know why though
+ # FreeCAD.ActiveDocument.recompute()
- def autoApply(self):
- print("autoApply")
- if self.form.cbAutoApply.checkState() == QtCore.Qt.CheckState.Checked:
- self.whenApplyClicked()
+# def autoApply(self):
+# print("autoApply")
+# if self.form.cbAutoApply.checkState() == QtCore.Qt.CheckState.Checked:
+# self.whenApplyClicked()
- def updateTagSpacing(self, count):
- print("updateTagSpacing")
- if count == 0:
- spacing = 0
- else:
- spacing = self.pathLength / count
- self.form.dsbSpacing.blockSignals(True)
- self.form.dsbSpacing.setValue(spacing)
- self.form.dsbSpacing.blockSignals(False)
+ def updateModel(self):
+ self.getFields()
+ self.updateTagsView()
+ #FreeCAD.ActiveDocument.recompute()
def whenCountChanged(self):
print("whenCountChanged")
- self.updateTagSpacing(self.form.sbCount.value())
- self.autoApply()
+ count = self.form.sbCount.value()
+ self.form.pbGenerate.setEnabled(count)
- def whenSpacingChanged(self):
- print("whenSpacingChanged")
- if self.form.dsbSpacing.value() == 0:
- count = 0
- else:
- count = int(self.pathLength / self.form.dsbSpacing.value())
- self.form.sbCount.blockSignals(True)
- self.form.sbCount.setValue(count)
- self.form.sbCount.blockSignals(False)
- self.autoApply()
+ def whenTagSelectionChanged(self):
+ print('whenTagSelectionChanged')
+ item = self.form.lwTags.currentItem()
+ self.form.pbDelete.setEnabled(not item is None)
- def whenOkClicked(self):
- print("whenOkClicked")
- self.whenApplyClicked()
- self.form.toolBox.setCurrentWidget(self.form.tbpTags)
+ def deleteSelectedTag(self):
+ item = self.form.lwTags.currentItem()
+ x = item.data(self.DataX)
+ y = item.data(self.DataY)
+ tags = filter(lambda t: t.x != x or t.y != y, self.tags)
+ self.obj.Proxy.setTags(self.obj, tags)
+ self.updateTagsView()
def setupSpinBox(self, widget, val, decimals = 2):
widget.setMinimum(0)
@@ -861,29 +881,44 @@ class TaskPanel:
widget.setValue(val)
def setFields(self):
- self.pathLength = self.obj.Proxy.getPathLength(self.obj)
- vHeader = self.form.twTags.verticalHeader()
- vHeader.setResizeMode(QtGui.QHeaderView.Fixed)
- vHeader.setDefaultSectionSize(20)
- self.updateTags()
- self.setupSpinBox(self.form.sbCount, self.form.twTags.rowCount(), None)
- self.setupSpinBox(self.form.dsbSpacing, 0)
+ self.updateTagsView()
+ self.setupSpinBox(self.form.sbCount, len(self.tags), None)
self.setupSpinBox(self.form.dsbHeight, self.obj.Proxy.getHeight(self.obj))
self.setupSpinBox(self.form.dsbWidth, self.obj.Proxy.getWidth(self.obj))
- self.setupSpinBox(self.form.dsbAngle, self.obj.Proxy.getAngle(self.obj))
- self.updateTagSpacing(self.form.twTags.rowCount())
+ self.setupSpinBox(self.form.dsbAngle, self.obj.Proxy.getAngle(self.obj), 0)
+ self.form.dsbAngle.setMaximum(90)
+ self.form.dsbAngle.setSingleStep(5.)
+
+ def updateModelHeight(self):
+ print('updateModelHeight')
+ self.updateModel()
+
+ def updateModelWidth(self):
+ print('updateModelWidth')
+ self.updateModel()
+
+ def updateModelAngle(self):
+ print('updateModelAngle')
+ self.updateModel()
+
+ def updateModelTags(self):
+ print('updateModelTags')
+ self.updateModel()
def setupUi(self):
self.setFields()
+ self.whenCountChanged()
+
self.form.sbCount.valueChanged.connect(self.whenCountChanged)
- self.form.dsbSpacing.valueChanged.connect(self.whenSpacingChanged)
- self.form.dsbHeight.valueChanged.connect(self.autoApply)
- self.form.dsbWidth.valueChanged.connect(self.autoApply)
- self.form.dsbAngle.valueChanged.connect(self.autoApply)
- #self.form.pbAdd.clicked.connect(self.)
- self.form.buttonBox.button(QtGui.QDialogButtonBox.Apply).clicked.connect(self.whenApplyClicked)
- self.form.buttonBox.button(QtGui.QDialogButtonBox.Ok).clicked.connect(self.whenOkClicked)
- self.form.twTags.itemChanged.connect(self.updateUI)
+ self.form.pbGenerate.clicked.connect(self.generateNewTags)
+
+ self.form.dsbHeight.editingFinished.connect(self.updateModelHeight)
+ self.form.dsbWidth.editingFinished.connect(self.updateModelWidth)
+ self.form.dsbAngle.editingFinished.connect(self.updateModelAngle)
+ self.form.lwTags.itemChanged.connect(self.updateModelTags)
+ self.form.lwTags.itemSelectionChanged.connect(self.whenTagSelectionChanged)
+
+ self.form.pbDelete.clicked.connect(self.deleteSelectedTag)
class SelObserver:
def __init__(self):
diff --git a/src/Mod/Path/PathScripts/PathGeom.py b/src/Mod/Path/PathScripts/PathGeom.py
index 4eaf54510..388b10952 100644
--- a/src/Mod/Path/PathScripts/PathGeom.py
+++ b/src/Mod/Path/PathScripts/PathGeom.py
@@ -82,6 +82,18 @@ class PathGeom:
Return True if two points are roughly identical (see also isRoughly)."""
return cls.isRoughly(p1.x, p2.x, error) and cls.isRoughly(p1.y, p2.y, error) and cls.isRoughly(p1.z, p2.z, error)
+ @classmethod
+ def edgesMatch(cls, e0, e1, error=0.0000001):
+ """(e0, e1, [error=0.0000001]
+ Return true if the edges start and end at the same point and have the same type of curve."""
+ if type(e0.Curve) != type(e1.Curve):
+ return False
+ if not cls.pointsCoincide(e0.valueAt(e0.FirstParameter), e1.valueAt(e1.FirstParameter)):
+ return False
+ if not cls.pointsCoincide(e0.valueAt(e0.LastParameter), e1.valueAt(e1.LastParameter)):
+ return False
+ return True
+
@classmethod
def edgeConnectsTo(cls, edge, vector):
"""(edge, vector)
diff --git a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py
index 516b35e7d..2d74688da 100644
--- a/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py
+++ b/src/Mod/Path/PathTests/TestPathDressupHoldingTags.py
@@ -52,7 +52,7 @@ class TestHoldingTags(PathTestBase):
def test01(self):
"""Verify solid for a 90 degree tag is a cylinder."""
tag = Tag(100, 200, 4, 5, 90, True)
- tag.createSolidsAt(17)
+ tag.createSolidsAt(17, 0)
self.assertIsNotNone(tag.solid)
self.assertCylinderAt(tag.solid, Vector(100, 200, 17), 2, 5)
@@ -60,7 +60,7 @@ class TestHoldingTags(PathTestBase):
def test02(self):
"""Verify trapezoidal tag has a cone shape with a lid."""
tag = Tag(0, 0, 18, 5, 45, True)
- tag.createSolidsAt(0)
+ tag.createSolidsAt(0, 0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 9, 4, 5)
@@ -68,14 +68,14 @@ class TestHoldingTags(PathTestBase):
def test03(self):
"""Verify pointy cone shape of tag with pointy end if width, angle and height match up."""
tag = Tag(0, 0, 10, 5, 45, True)
- tag.createSolidsAt(0)
+ tag.createSolidsAt(0, 0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 5, 0, 5)
def test04(self):
"""Verify height adjustment if tag isn't wide eough for angle."""
tag = Tag(0, 0, 5, 17, 60, True)
- tag.createSolidsAt(0)
+ tag.createSolidsAt(0, 0)
self.assertIsNotNone(tag.solid)
self.assertConeAt(tag.solid, Vector(0,0,0), 2.5, 0, 2.5 * math.tan((60/180.0)*math.pi))
diff --git a/src/Mod/Path/PathTests/TestPathGeom.py b/src/Mod/Path/PathTests/TestPathGeom.py
index 7ab7a5396..b6a3703c5 100644
--- a/src/Mod/Path/PathTests/TestPathGeom.py
+++ b/src/Mod/Path/PathTests/TestPathGeom.py
@@ -130,12 +130,14 @@ class TestPathGeom(PathTestBase):
commands.append(Path.Command('G0', {'X': 0}))
commands.append(Path.Command('G1', {'Y': 0}))
- wire = PathGeom.wireForPath(Path.Path(commands))
+ wire,rapid = PathGeom.wireForPath(Path.Path(commands))
self.assertEqual(len(wire.Edges), 4)
self.assertLine(wire.Edges[0], Vector(0,0,0), Vector(1,0,0))
self.assertLine(wire.Edges[1], Vector(1,0,0), Vector(1,1,0))
self.assertLine(wire.Edges[2], Vector(1,1,0), Vector(0,1,0))
self.assertLine(wire.Edges[3], Vector(0,1,0), Vector(0,0,0))
+ self.assertEqual(len(rapid), 1)
+ self.assertTrue(PathGeom.edgesMatch(rapid[0], wire.Edges[2]))
wires = PathGeom.wiresForPath(Path.Path(commands))
self.assertEqual(len(wires), 2)