From f6122965f2d3f6fd04790869fb11a910b76b164f Mon Sep 17 00:00:00 2001 From: Roy-043 <70520633+Roy-043@users.noreply.github.com> Date: Sat, 17 Sep 2022 10:08:41 +0200 Subject: [PATCH] Draft: Fix several path array issues --- src/Mod/Draft/draftobjects/patharray.py | 228 +++++++++++------------- 1 file changed, 102 insertions(+), 126 deletions(-) diff --git a/src/Mod/Draft/draftobjects/patharray.py b/src/Mod/Draft/draftobjects/patharray.py index 82284df5b4..014f19ed62 100644 --- a/src/Mod/Draft/draftobjects/patharray.py +++ b/src/Mod/Draft/draftobjects/patharray.py @@ -336,35 +336,40 @@ class PathArray(DraftLink): self.show_and_hide(obj, prop) def show_and_hide(self, obj, prop): - """Show and hide the properties depending on the touched property.""" - # The minus sign removes the Hidden property (show) - if prop == "Align": + """Show and hide the properties depending on the touched property. + + Note that when the array is created, some properties will change + more than once in a seemingly random order. + """ + # The minus sign removes the Hidden property (show). + if prop in ("Align", "AlignMode"): + + # Check if all referenced properties are available: + for pr in ("Align", "AlignMode", "ForceVertical", + "VerticalVector", "TangentVector"): + if not hasattr(obj, pr): + return + if obj.Align: - for pr in ("AlignMode", "ForceVertical", "VerticalVector", - "TangentVector"): - obj.setPropertyStatus(pr, "-Hidden") + obj.setPropertyStatus("AlignMode", "-Hidden") + + if obj.AlignMode == "Frenet": + for pr in ("ForceVertical", "VerticalVector"): + obj.setPropertyStatus(pr, "Hidden") + else: + for pr in ("ForceVertical", "VerticalVector"): + obj.setPropertyStatus(pr, "-Hidden") + + if obj.AlignMode == "Tangent": + obj.setPropertyStatus("TangentVector", "-Hidden") + else: + obj.setPropertyStatus("TangentVector", "Hidden") + else: - for pr in ("AlignMode", "ForceVertical", "VerticalVector", - "TangentVector"): + for pr in ("AlignMode", "ForceVertical", + "VerticalVector", "TangentVector"): obj.setPropertyStatus(pr, "Hidden") - if prop == "AlignMode": - if obj.AlignMode == "Original": - for pr in ("ForceVertical", "VerticalVector"): - obj.setPropertyStatus(pr, "-Hidden") - - obj.setPropertyStatus("TangentVector", "Hidden") - - elif obj.AlignMode == "Frenet": - for pr in ("ForceVertical", "VerticalVector", - "TangentVector"): - obj.setPropertyStatus(pr, "Hidden") - - elif obj.AlignMode == "Tangent": - for pr in ("ForceVertical", "VerticalVector", - "TangentVector"): - obj.setPropertyStatus(pr, "-Hidden") - def onDocumentRestored(self, obj): """Execute code when the document is restored. @@ -412,79 +417,55 @@ _PathArray = PathArray def placements_on_path(shapeRotation, pathwire, count, xlate, align, - mode='Original', forceNormal=False, + mode="Original", forceNormal=False, normalOverride=None): """Calculate the placements of a shape along a given path. - Each copy will be distributed evenly. + Copies will be distributed evenly. """ - closedpath = DraftGeomUtils.isReallyClosed(pathwire) - normal = DraftGeomUtils.get_normal(pathwire) - # for backward compatibility with previous getNormal implementation - if normal is None: - normal = App.Vector(0, 0, 1) + if mode == "Frenet": + forceNormal = False if forceNormal and normalOverride: normal = normalOverride + else: + normal = DraftGeomUtils.get_normal(pathwire) + if normal is None: + normal = App.Vector(0, 0, 1) path = Part.__sortEdges__(pathwire.Edges) - ends = [] - cdist = 0 - for e in path: # find cumulative edge end distance + # find cumulative edge end distance + cdist = 0 + ends = [] + for e in path: cdist += e.Length ends.append(cdist) + step = cdist / (count if DraftGeomUtils.isReallyClosed(pathwire) else count - 1) + remains = 0 + travel = 0 placements = [] - # place the start shape - pt = path[0].Vertexes[0].Point - _place = calculate_placement(shapeRotation, - path[0], 0, pt, xlate, align, normal, - mode, forceNormal) - placements.append(_place) - - # closed path doesn't need shape on last vertex - if not closedpath: - # place the end shape - pt = path[-1].Vertexes[-1].Point - _place = calculate_placement(shapeRotation, - path[-1], path[-1].Length, - pt, xlate, align, normal, - mode, forceNormal) - placements.append(_place) - - if count < 3: - return placements - - # place the middle shapes - if closedpath: - stop = count - else: - stop = count - 1 - step = float(cdist) / stop - remains = 0 - travel = step - for i in range(1, stop): + for i in range(0, count): # which edge in path should contain this shape? - # avoids problems with float math travel > ends[-1] - iend = len(ends) - 1 - for j in range(0, len(ends)): if travel <= ends[j]: iend = j break + else: + # avoids problems with float math travel > ends[-1] + iend = len(ends) - 1 # place shape at proper spot on proper edge remains = ends[iend] - travel offset = path[iend].Length - remains pt = path[iend].valueAt(get_parameter_from_v0(path[iend], offset)) - - _place = calculate_placement(shapeRotation, - path[iend], offset, - pt, xlate, align, normal, - mode, forceNormal) - placements.append(_place) + place = calculate_placement(shapeRotation, + path[iend], offset, + pt, xlate, align, normal, + mode, forceNormal) + placements.append(place) travel += step @@ -495,86 +476,81 @@ calculatePlacementsOnPath = placements_on_path def calculate_placement(globalRotation, - edge, offset, RefPt, xlate, align, normal=None, - mode='Original', overrideNormal=False): - """Orient shape to a local coordinate system (tangent, normal, binormal). - - Orient shape at parameter offset, normally length. + edge, offset, RefPt, xlate, align, + normal=App.Vector(0.0, 0.0, 1.0), + mode="Original", overrideNormal=False): + """Orient shape in the local coordinate system at parameter offset. http://en.wikipedia.org/wiki/Euler_angles (previous version) http://en.wikipedia.org/wiki/Quaternions """ - # Start with a null Placement so the translation goes to the right place. - # Then apply the global orientation. + # Default Placement: placement = App.Placement() placement.Rotation = globalRotation + placement.Base = RefPt + xlate - placement.move(RefPt + xlate) if not align: return placement - nullv = App.Vector(0, 0, 0) - defNormal = App.Vector(0.0, 0.0, 1.0) - if normal: - defNormal = normal + tol = 1e-6 # App.Rotation() tolerance is 1e-7. Shorter vectors are ignored. + nullv = App.Vector() - try: - t = edge.tangentAt(get_parameter_from_v0(edge, offset)) - t.normalize() - except Exception: - _wrn(translate("draft","Cannot calculate path tangent. Copy not aligned.")) + t = edge.tangentAt(get_parameter_from_v0(edge, offset)) + if t.isEqual(nullv, tol): + _wrn(translate("draft", "Length of tangent vector is zero. Copy not aligned.")) return placement - if mode in ('Original', 'Tangent'): - if normal is None: - n = defNormal - else: - n = normal - n.normalize() + # If the length of the normal is zero or if it is parallel to the tangent, + # we make the vectors equal (n = t). The App.Rotation() algorithm will + # then replace the normal with a default axis. + # For the vector with the lowest App.Rotation() priority we use a null + # vector. Calculating this binormal would not make sense in the mentioned + # cases. And in all other cases calculating it is not necessary as + # App.Rotation() will ignore it. - try: - b = t.cross(n) - b.normalize() - except Exception: - # weird special case, tangent and normal parallel - b = nullv - _wrn(translate("draft","Tangent and normal are parallel. Copy not aligned.")) - return placement + if mode in ("Original", "Tangent"): + n = normal + if n.isEqual(nullv, tol): + _wrn(translate("draft", "Length of normal vector is zero. Using a default axis instead.")) + n = t + else: + n_nor = n.normalize() + t_nor = t.normalize() + if n_nor.isEqual(t_nor, tol) or n_nor.isEqual(t_nor.negative(), tol): + _wrn(translate("draft", "Tangent and normal vectors are parallel. Normal replaced by a default axis.")) + n = t if overrideNormal: - priority = "XZY" - newRot = App.Rotation(t, b, n, priority) # t/x, b/y, n/z + newRot = App.Rotation(t, nullv, n, "XZY") # priority = "XZY" else: - # must follow X, try to follow Z, Y is what it is - priority = "XZY" - newRot = App.Rotation(t, n, b, priority) + newRot = App.Rotation(t, n, nullv, "XYZ") # priority = "XYZ" - elif mode == 'Frenet': + elif mode == "Frenet": try: n = edge.normalAt(get_parameter_from_v0(edge, offset)) - n.normalize() - except App.Base.FreeCADError: # no/infinite normals here - n = defNormal - _msg(translate("draft","Cannot calculate path normal, using default.")) + except App.Base.FreeCADError: # no/infinite normals here + _wrn(translate("draft", "Cannot calculate normal vector. Using the default normal instead.")) + n = normal - try: - b = t.cross(n) - b.normalize() - except Exception: - b = nullv - _wrn(translate("draft","Cannot calculate path binormal. Copy not aligned.")) - return placement + if n.isEqual(nullv, tol): + _wrn(translate("draft", "Length of normal vector is zero. Using a default axis instead.")) + n = t + else: + n_nor = n.normalize() + t_nor = t.normalize() + if n_nor.isEqual(t_nor, tol) or n_nor.isEqual(t_nor.negative(), tol): + _wrn(translate("draft", "Tangent and normal vectors are parallel. Normal replaced by a default axis.")) + n = t + + newRot = App.Rotation(t, n, nullv, "XYZ") # priority = "XYZ" - priority = "XZY" - newRot = App.Rotation(t, n, b, priority) # t/x, n/y, b/z else: - _msg(translate("draft","AlignMode {} is not implemented").format(mode)) + _msg(translate("draft", "AlignMode {} is not implemented").format(mode)) return placement - # Have valid tangent, normal, binormal newGRot = newRot.multiply(globalRotation) - placement.Rotation = newGRot + placement.Base = RefPt + newRot.multVec(xlate) return placement