From cb85072bbd34554dd6f110e3f5e7b872b9cc21ad Mon Sep 17 00:00:00 2001 From: Markus Lampert Date: Fri, 30 Dec 2016 19:16:27 -0800 Subject: [PATCH] Fixed alignment issue and unit tests. The trick is really to over-extend edges before creationg shapes for the common operation, and trying to avoid alignment of the edge with the cone's seam. --- .../Path/Gui/Resources/panels/DogboneEdit.ui | 6 +- .../Gui/Resources/panels/HoldingTagsEdit.ui | 239 +++------ .../PathScripts/PathDressupHoldingTags.py | 473 ++++++++++-------- src/Mod/Path/PathScripts/PathGeom.py | 12 + .../PathTests/TestPathDressupHoldingTags.py | 8 +- src/Mod/Path/PathTests/TestPathGeom.py | 4 +- 6 files changed, 355 insertions(+), 387 deletions(-) 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)