From f87ae4de081dff2e03bbb664902951996012ff5d Mon Sep 17 00:00:00 2001 From: alafr Date: Sat, 14 Mar 2020 18:29:50 +0100 Subject: [PATCH 01/14] Arch Structure: Refactor getExtrusionData - group all `base, placement = self.rebase(baseface)` in one place - group all normal calculations in one place - fix placement of tool (bug introduced by my previous PR) - allow multiple faces when face is created by FaceMaker - remove unused code: if `len(obj.Base.Shape.Edges) == 1` is true, then there is a Wire and `obj.Base.Shape.Wires` should be also truthy --- src/Mod/Arch/ArchStructure.py | 71 +++++++++++++++-------------------- 1 file changed, 31 insertions(+), 40 deletions(-) diff --git a/src/Mod/Arch/ArchStructure.py b/src/Mod/Arch/ArchStructure.py index 63a691d4a4..5ee6f55f46 100644 --- a/src/Mod/Arch/ArchStructure.py +++ b/src/Mod/Arch/ArchStructure.py @@ -712,9 +712,7 @@ class _Structure(ArchComponent.Component): self.applyShape(obj,base,pl) def getExtrusionData(self,obj): - - """returns (shape,extrusion vector,placement) or None""" - + """returns (shape,extrusion vector or path,placement) or None""" if hasattr(obj,"IfcType"): IfcType = obj.IfcType else: @@ -728,11 +726,11 @@ class _Structure(ArchComponent.Component): length = obj.Length.Value width = obj.Width.Value height = obj.Height.Value - normal = None if not height: height = self.getParentHeight(obj) - base = None - placement = None + baseface = None + extrusion = None + normal = None if obj.Base: if hasattr(obj.Base,'Shape'): if obj.Base.Shape: @@ -742,22 +740,8 @@ class _Structure(ArchComponent.Component): if not DraftGeomUtils.isCoplanar(obj.Base.Shape.Faces,tolerance=0.01): return None else: - base,placement = self.rebase(obj.Base.Shape) - normal = obj.Base.Shape.Faces[0].normalAt(0,0) - normal = placement.inverse().Rotation.multVec(normal) - if (len(obj.Shape.Solids) > 1) and (len(obj.Shape.Solids) == len(obj.Base.Shape.Faces)): - # multiple extrusions - b = [] - p = [] - hint = obj.Base.Shape.Faces[0].normalAt(0,0) - for f in obj.Base.Shape.Faces: - bf,pf = self.rebase(f,hint) - b.append(bf) - p.append(pf) - base = b - placement = p + baseface = obj.Base.Shape.copy() elif obj.Base.Shape.Wires: - baseface = None if hasattr(obj,"FaceMaker"): if obj.FaceMaker != "None": try: @@ -765,9 +749,6 @@ class _Structure(ArchComponent.Component): except: FreeCAD.Console.PrintError(translate("Arch","Facemaker returned an error")+"\n") return None - if len(baseface.Faces) > 1: - baseface = baseface.Faces[0] - normal = baseface.normalAt(0,0) if not baseface: for w in obj.Base.Shape.Wires: if not w.isClosed(): @@ -781,15 +762,7 @@ class _Structure(ArchComponent.Component): if baseface: baseface = baseface.fuse(f) else: - baseface = f - normal = f.normalAt(0,0) - base,placement = self.rebase(baseface) - normal = placement.inverse().Rotation.multVec(normal) - elif (len(obj.Base.Shape.Edges) == 1) and (len(obj.Base.Shape.Vertexes) == 1): - # closed edge - w = Part.Wire(obj.Base.Shape.Edges[0]) - baseface = Part.Face(w) - base,placement = self.rebase(baseface) + baseface = f.copy() elif length and width and height: if (length > height) and (IfcType != "Slab"): h2 = height/2 or 0.5 @@ -807,8 +780,7 @@ class _Structure(ArchComponent.Component): v4 = Vector(-l2,w2,0) import Part baseface = Part.Face(Part.makePolygon([v1,v2,v3,v4,v1])) - base,placement = self.rebase(baseface) - if base and placement: + if baseface: if obj.Tool: if obj.Tool.Shape: edges = obj.Tool.Shape.Edges @@ -819,10 +791,28 @@ class _Structure(ArchComponent.Component): else: if obj.Normal.Length: normal = Vector(obj.Normal).normalize() - if isinstance(placement,list): - normal = placement[0].inverse().Rotation.multVec(normal) - else: - normal = placement.inverse().Rotation.multVec(normal) + else: + normal = baseface.Faces[0].normalAt(0, 0) + base = None + placement = None + inverse_placement = None + if len(baseface.Faces) > 1: + base = [] + placement = [] + hint = baseface.Faces[0].normalAt(0, 0) + for f in baseface.Faces: + bf, pf = self.rebase(f, hint) + base.append(bf) + placement.append(pf) + inverse_placement = placement[0].inverse() + else: + base, placement = self.rebase(baseface) + inverse_placement = placement.inverse() + if extrusion: + if isinstance(extrusion, FreeCAD.Vector): + extrusion = inverse_placement.Rotation.multVec(extrusion) + elif normal: + normal = inverse_placement.Rotation.multVec(normal) if not normal: normal = Vector(0,0,1) if not normal.Length: @@ -834,7 +824,8 @@ class _Structure(ArchComponent.Component): else: if height: extrusion = normal.multiply(height) - return (base,extrusion,placement) + if extrusion: + return (base, extrusion, placement) return None def onChanged(self,obj,prop): From 80736caa348ac9d7ace20e45beea287f3b12f699 Mon Sep 17 00:00:00 2001 From: alafr Date: Sat, 21 Mar 2020 11:42:20 +0100 Subject: [PATCH 02/14] Arch Structure: fix multiple extrusions along path Fix the exception when trying to copy a wire with FreeCAD.Vector constructor --- src/Mod/Arch/ArchStructure.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Mod/Arch/ArchStructure.py b/src/Mod/Arch/ArchStructure.py index 5ee6f55f46..fe8adf90a3 100644 --- a/src/Mod/Arch/ArchStructure.py +++ b/src/Mod/Arch/ArchStructure.py @@ -665,21 +665,25 @@ class _Structure(ArchComponent.Component): if i < len(ev): evi = ev[i] else: - evi = FreeCAD.Vector(ev[-1]) + evi = ev[-1] + if isinstance(evi, FreeCAD.Vector): + evi = FreeCAD.Vector(evi) + else: + evi = evi.copy() if i < len(pla): pli = pla[i] else: pli = pla[-1].copy() shi.Placement = pli.multiply(shi.Placement) - if not isinstance(evi, FreeCAD.Vector): + if isinstance(evi, FreeCAD.Vector): + extv = pla[0].Rotation.multVec(evi) + shi = shi.extrude(extv) + else: try: shi = evi.makePipe(shi) except Part.OCCError: FreeCAD.Console.PrintError(translate("Arch","Error: The base shape couldn't be extruded along this tool object")+"\n") return - else: - extv = pla[0].Rotation.multVec(evi) - shi = shi.extrude(extv) base.append(shi) if len(base) == 1: base = base[0] From bac868a298168bd0cc8f93042a734ce7a82ff6ce Mon Sep 17 00:00:00 2001 From: alafr Date: Wed, 8 Apr 2020 22:44:20 +0200 Subject: [PATCH 03/14] Draft: Add geometry functions for Arch Structure * get_referenced_edges(property_value): returns the Edges referenced by the value of a App:PropertyLink, App::PropertyLinkSub or App::PropertyLinkSubList property. * get_placement_perpendicular_to_wire(wire): returns the placement whose base is the wire's first vertex and it's z axis aligned to the wire's tangent. * get_extended_wire(wire, offset_start, offset_end): returns a wire trimmed (negative offset) or extended (positive offset) at its first vertex, last vertex or both ends. For example: get_extended_wire(wire, -100.0, 0.0) -> returns a copy of the wire with its first 100 mm removed get_extended_wire(wire, 0.0, 100.0) -> returns a copy of the wire extended by 100 mm after it's last vertex --- src/Mod/Draft/DraftGeomUtils.py | 7 +- src/Mod/Draft/draftgeoutils/edges.py | 25 ++++++- src/Mod/Draft/draftgeoutils/general.py | 16 +++-- src/Mod/Draft/draftgeoutils/wires.py | 91 +++++++++++++++++++++++++- 4 files changed, 129 insertions(+), 10 deletions(-) diff --git a/src/Mod/Draft/DraftGeomUtils.py b/src/Mod/Draft/DraftGeomUtils.py index 73a6c6288e..a1f1ab603b 100644 --- a/src/Mod/Draft/DraftGeomUtils.py +++ b/src/Mod/Draft/DraftGeomUtils.py @@ -86,7 +86,8 @@ from draftgeoutils.edges import (findEdge, is_line, invert, findMidpoint, - getTangent) + getTangent, + get_referenced_edges) from draftgeoutils.faces import (concatenate, getBoundary, @@ -128,7 +129,9 @@ from draftgeoutils.wires import (findWires, rebaseWire, removeInterVertices, cleanProjection, - tessellateProjection) + tessellateProjection, + get_placement_perpendicular_to_wire, + get_extended_wire) # Needs wires functions from draftgeoutils.fillets import (fillet, diff --git a/src/Mod/Draft/draftgeoutils/edges.py b/src/Mod/Draft/draftgeoutils/edges.py index 67a6a33a2e..4783cfb5f0 100644 --- a/src/Mod/Draft/draftgeoutils/edges.py +++ b/src/Mod/Draft/draftgeoutils/edges.py @@ -221,8 +221,31 @@ def getTangent(edge, from_point=None): return None + +def get_referenced_edges(property_value): + """ Returns the Edges referenced by the value of a App:PropertyLink, App::PropertyLinkList, + App::PropertyLinkSub or App::PropertyLinkSubList property. """ + edges = [] + if not isinstance(property_value, list): + property_value = [property_value] + for element in property_value: + if hasattr(element, "Shape") and element.Shape: + edges += shape.Edges + elif isinstance(element, tuple) and len(element) == 2: + object, subelement_names = element + if hasattr(object, "Shape") and object.Shape: + if len(subelement_names) == 1 and subelement_names[0] == "": + edges += object.Shape.Edges + else: + for subelement_name in subelement_names: + if subelement_name.startswith("Edge"): + edge_number = int(subelement_name.lstrip("Edge")) - 1 + if edge_number < len(object.Shape.Edges): + edges.append(object.Shape.Edges[edge_number]) + return edges + # compatibility layer isLine = is_line -## @} +## @} \ No newline at end of file diff --git a/src/Mod/Draft/draftgeoutils/general.py b/src/Mod/Draft/draftgeoutils/general.py index 5ef9be80ec..30fbfe8a27 100644 --- a/src/Mod/Draft/draftgeoutils/general.py +++ b/src/Mod/Draft/draftgeoutils/general.py @@ -59,12 +59,16 @@ def precision(): return precisionInt # return PARAMGRP.GetInt("precision", 6) -def vec(edge): - """Return a vector from an edge or a Part.LineSegment.""" - # if edge is not straight, you'll get strange results! - if isinstance(edge, Part.Shape): - return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) - elif isinstance(edge, Part.LineSegment): +def vec(edge, use_orientation = False): + """ vec(edge[, use_orientation]) or vec(line): returns a vector from an edge or a Part.LineSegment. + If use_orientation is True, it takes into account the edges orientation. + If edge is not straight, you'll get strange results! """ + if isinstance(edge, Part.Edge): + if edge.Orientation == "Forward" or not use_orientation: + return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) + else: + return edge.Vertexes[0].Point.sub(edge.Vertexes[-1].Point) + elif isinstance(edge,Part.LineSegment): return edge.EndPoint.sub(edge.StartPoint) else: return None diff --git a/src/Mod/Draft/draftgeoutils/wires.py b/src/Mod/Draft/draftgeoutils/wires.py index 8c6f9dfed9..a2cd9d7368 100644 --- a/src/Mod/Draft/draftgeoutils/wires.py +++ b/src/Mod/Draft/draftgeoutils/wires.py @@ -435,4 +435,93 @@ def tessellateProjection(shape, seglen): return Part.makeCompound(newedges) -## @} +def get_placement_perpendicular_to_wire(wire): + """ Returns the placement whose base is the wire's first vertex and it's z axis aligned to the wire's tangent. """ + pl = FreeCAD.Placement() + if wire.Length > 0.0: + pl.Base = wire.OrderedVertexes[0].Point + first_edge = wire.OrderedEdges[0] + if first_edge.Orientation == "Forward": + zaxis = -first_edge.tangentAt(first_edge.FirstParameter) + else: + zaxis = first_edge.tangentAt(first_edge.LastParameter) + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 0, 1), zaxis, "ZYX") + else: + FreeCAD.Console.PrintError("debug: get_placement_perpendicular_to_wire called with a zero-length wire.\n") + return pl + + +def get_extended_wire(wire, offset_start, offset_end): + """ Returns a wire trimmed (negative offset) or extended (positive offset) at its first vertex, last vertex or both ends. + get_extended_wire(wire, -100.0, 0.0) -> returns a copy of the wire with its first 100 mm removed + get_extended_wire(wire, 0.0, 100.0) -> returns a copy of the wire extended by 100 mm after it's last vertex """ + if min(offset_start, offset_end, offset_start + offset_end) <= -wire.Length: + FreeCAD.Console.PrintError("debug: get_extended_wire error, wire's length insufficient for trimming.\n") + return wire + if offset_start < 0: # Trim the wire from the first vertex + offset_start = -offset_start + out_edges = [] + for edge in wire.OrderedEdges: + if offset_start >= edge.Length: # Remove entire edge + offset_start -= edge.Length + elif round(offset_start, precision()) > 0: # Split edge, to remove the required length + if edge.Orientation == "Forward": + new_edge = edge.split(edge.getParameterByLength(offset_start)).OrderedEdges[1] + else: + new_edge = edge.split(edge.getParameterByLength(edge.Length - offset_start)).OrderedEdges[0] + new_edge.Placement = edge.Placement # Strangely, edge.split discards the placement and orientation + new_edge.Orientation = edge.Orientation + out_edges.append(new_edge) + offset_start = 0 + else: # Keep the remaining entire edges + out_edges.append(edge) + wire = Part.Wire(out_edges) + elif offset_start > 0: # Extend the first edge along its normal + first_edge = wire.OrderedEdges[0] + if first_edge.Orientation == "Forward": + start, end = first_edge.FirstParameter, first_edge.LastParameter + vec = first_edge.tangentAt(start).multiply(offset_start) + else: + start, end = first_edge.LastParameter, first_edge.FirstParameter + vec = -first_edge.tangentAt(start).multiply(offset_start) + if geomType(first_edge) == "Line": # Replace first edge with the extended new edge + new_edge = Part.LineSegment(first_edge.valueAt(start).sub(vec), first_edge.valueAt(end)).toShape() + wire = Part.Wire([new_edge] + wire.OrderedEdges[1:]) + else: # Add a straight edge before the first vertex + new_edge = Part.LineSegment(first_edge.valueAt(start).sub(vec), first_edge.valueAt(start)).toShape() + wire = Part.Wire([new_edge] + wire.OrderedEdges) + if offset_end < 0: # Trim the wire from the last vertex + offset_end = -offset_end + out_edges = [] + for edge in reversed(wire.OrderedEdges): + if offset_end >= edge.Length: # Remove entire edge + offset_end -= edge.Length + elif round(offset_end, precision()) > 0: # Split edge, to remove the required length + if edge.Orientation == "Forward": + new_edge = edge.split(edge.getParameterByLength(edge.Length - offset_end)).OrderedEdges[0] + else: + new_edge = edge.split(edge.getParameterByLength(offset_end)).OrderedEdges[1] + new_edge.Placement = edge.Placement # Strangely, edge.split discards the placement and orientation + new_edge.Orientation = edge.Orientation + out_edges.insert(0, new_edge) + offset_end = 0 + else: # Keep the remaining entire edges + out_edges.insert(0, edge) + wire = Part.Wire(out_edges) + elif offset_end > 0: # Extend the last edge along its normal + last_edge = wire.OrderedEdges[-1] + if last_edge.Orientation == "Forward": + start, end = last_edge.FirstParameter, last_edge.LastParameter + vec = last_edge.tangentAt(end).multiply(offset_end) + else: + start, end = last_edge.LastParameter, last_edge.FirstParameter + vec = -last_edge.tangentAt(end).multiply(offset_end) + if geomType(last_edge) == "Line": # Replace last edge with the extended new edge + new_edge = Part.LineSegment(last_edge.valueAt(start), last_edge.valueAt(end).add(vec)).toShape() + wire = Part.Wire(wire.OrderedEdges[:-1] + [new_edge]) + else: # Add a straight edge after the last vertex + new_edge = Part.LineSegment(last_edge.valueAt(end), last_edge.valueAt(end).add(vec)).toShape() + wire = Part.Wire(wire.OrderedEdges + [new_edge]) + return wire + +## @} \ No newline at end of file From 7f83d8f0b034a783594817dcbb54ea148ebb52e2 Mon Sep 17 00:00:00 2001 From: alafr Date: Sat, 14 Mar 2020 18:32:19 +0100 Subject: [PATCH 04/14] Arch Structure : use Edges as a Tool, add options * New widget for Tool selection: this enables the use of some Edges and not only entire Shapes - it makes possible to build Structures from a master Sketch. * Add option BasePerpendicularToTool : option to create multiple Structures with a single Base (profile), along different Tools (paths). A copy of the profile is created and aligned perpendicular to the path at it start point. This can already be done with multiple Draft Clones attached using the "NormalToEdge" mode, but this new option will make it more straightforward and with less objects in the document. * Group properties related to the Tool in a group "Extrusion Path" * Add a readonly property ComputedLength (it will be needed in BIM schedules) --- src/Mod/Arch/ArchStructure.py | 121 +++++++++++++++++++++++++++++----- 1 file changed, 103 insertions(+), 18 deletions(-) diff --git a/src/Mod/Arch/ArchStructure.py b/src/Mod/Arch/ArchStructure.py index fe8adf90a3..81de3c8b52 100644 --- a/src/Mod/Arch/ArchStructure.py +++ b/src/Mod/Arch/ArchStructure.py @@ -611,7 +611,23 @@ class _Structure(ArchComponent.Component): pl = obj.PropertiesList if not "Tool" in pl: - obj.addProperty("App::PropertyLink","Tool","Structure",QT_TRANSLATE_NOOP("App::Property","An optional extrusion path for this element")) + obj.addProperty("App::PropertyLinkSubList", "Tool", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "An optional extrusion path for this element")) + if not "ComputedLength" in pl: + obj.addProperty("App::PropertyDistance", "ComputedLength", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "The computed length of the extrusion path"), 1) + if not "ToolOffsetFirst" in pl: + obj.addProperty("App::PropertyDistance", "ToolOffsetFirst", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "Start offset distance along the extrusion path (positive: extend, negative: trim")) + if not "ToolOffsetLast" in pl: + obj.addProperty("App::PropertyDistance", "ToolOffsetLast", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "End offset distance along the extrusion path (positive: extend, negative: trim")) + if not "BasePerpendicularToTool" in pl: + obj.addProperty("App::PropertyBool", "BasePerpendicularToTool", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "Automatically align the Base of the Structure perpendicular to the Tool axis")) + if not "BaseOffsetX" in pl: + obj.addProperty("App::PropertyDistance", "BaseOffsetX", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "X offset between the Base origin and the Tool axis (only used if BasePerpendicularToTool is True)")) + if not "BaseOffsetY" in pl: + obj.addProperty("App::PropertyDistance", "BaseOffsetY", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "Y offset between the Base origin and the Tool axis (only used if BasePerpendicularToTool is True)")) + if not "BaseMirror" in pl: + obj.addProperty("App::PropertyBool", "BaseMirror", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "Mirror the Base along its Y axis (only used if BasePerpendicularToTool is True)")) + if not "BaseRotation" in pl: + obj.addProperty("App::PropertyAngle", "BaseRotation", "ExtrusionPath", QT_TRANSLATE_NOOP("App::Property", "Base rotation around the Tool axis (only used if BasePerpendicularToTool is True)")) if not "Length" in pl: obj.addProperty("App::PropertyLength","Length","Structure",QT_TRANSLATE_NOOP("App::Property","The length of this element, if not based on a profile")) if not "Width" in pl: @@ -660,6 +676,7 @@ class _Structure(ArchComponent.Component): if not isinstance(pla,list): pla = [pla] base = [] + extrusion_length = 0.0 for i in range(len(sh)): shi = sh[i] if i < len(ev): @@ -685,10 +702,12 @@ class _Structure(ArchComponent.Component): FreeCAD.Console.PrintError(translate("Arch","Error: The base shape couldn't be extruded along this tool object")+"\n") return base.append(shi) + extrusion_length += evi.Length if len(base) == 1: base = base[0] else: base = Part.makeCompound(base) + obj.ComputedLength = FreeCAD.Units.Quantity(extrusion_length, FreeCAD.Units.Length) if obj.Base: if hasattr(obj.Base,'Shape'): if obj.Base.Shape.isNull(): @@ -785,13 +804,30 @@ class _Structure(ArchComponent.Component): import Part baseface = Part.Face(Part.makePolygon([v1,v2,v3,v4,v1])) if baseface: - if obj.Tool: - if obj.Tool.Shape: - edges = obj.Tool.Shape.Edges - if len(edges) == 1 and DraftGeomUtils.geomType(edges[0]) == "Line": - extrusion = DraftGeomUtils.vec(edges[0]) + if hasattr(obj, "Tool") and obj.Tool: + tool = obj.Tool + edges = DraftGeomUtils.get_referenced_edges(tool) + if len(edges) > 0: + extrusion = Part.Wire(Part.__sortEdges__(edges)) + if hasattr(obj, "ToolOffsetFirst"): + offset_start = float(obj.ToolOffsetFirst.getValueAs("mm")) else: - extrusion = obj.Tool.Shape.copy() + offset_start = 0.0 + if hasattr(obj, "ToolOffsetLast"): + offset_end = float(obj.ToolOffsetLast.getValueAs("mm")) + else: + offset_end = 0.0 + if offset_start != 0.0 or offset_end != 0.0: + extrusion = DraftGeomUtils.get_extended_wire(extrusion, offset_start, offset_end) + if hasattr(obj, "BasePerpendicularToTool") and obj.BasePerpendicularToTool: + pl = FreeCAD.Placement() + if hasattr(obj, "BaseRotation"): + pl.rotate(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, 1), -obj.BaseRotation) + if hasattr(obj, "BaseOffsetX") and hasattr(obj, "BaseOffsetY"): + pl.translate(FreeCAD.Vector(obj.BaseOffsetX, obj.BaseOffsetY, 0)) + if hasattr(obj, "BaseMirror"): + pl.rotate(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 1, 0), 180) + baseface.Placement = DraftGeomUtils.get_placement_perpendicular_to_wire(extrusion).multiply(pl) else: if obj.Normal.Length: normal = Vector(obj.Normal).normalize() @@ -813,6 +849,8 @@ class _Structure(ArchComponent.Component): base, placement = self.rebase(baseface) inverse_placement = placement.inverse() if extrusion: + if len(extrusion.Edges) == 1 and DraftGeomUtils.geomType(extrusion.Edges[0]) == "Line": + extrusion = DraftGeomUtils.vec(extrusion.Edges[0]) if isinstance(extrusion, FreeCAD.Vector): extrusion = inverse_placement.Rotation.multVec(extrusion) elif normal: @@ -848,8 +886,8 @@ class _Structure(ArchComponent.Component): ev = extdata[2].Rotation.multVec(extdata[1]) nodes.Placement = nodes.Placement.multiply(extdata[2]) if IfcType not in ["Slab"]: - if obj.Tool: - nodes = obj.Tool.Shape + if not isinstance(extdata[1], FreeCAD.Vector): + nodes = extdata[1] elif extdata[1].Length > 0: if hasattr(nodes,"CenterOfMass"): import Part @@ -1044,45 +1082,56 @@ class StructureTaskPanel(ArchComponent.ComponentTaskPanel): def __init__(self,obj): ArchComponent.ComponentTaskPanel.__init__(self) - self.optwid = QtGui.QWidget() - self.optwid.setWindowTitle(QtGui.QApplication.translate("Arch", "Node Tools", None)) - lay = QtGui.QVBoxLayout(self.optwid) + self.nodes_widget = QtGui.QWidget() + self.nodes_widget.setWindowTitle(QtGui.QApplication.translate("Arch", "Node Tools", None)) + lay = QtGui.QVBoxLayout(self.nodes_widget) - self.resetButton = QtGui.QPushButton(self.optwid) + self.resetButton = QtGui.QPushButton(self.nodes_widget) self.resetButton.setIcon(QtGui.QIcon(":/icons/edit-undo.svg")) self.resetButton.setText(QtGui.QApplication.translate("Arch", "Reset nodes", None)) lay.addWidget(self.resetButton) QtCore.QObject.connect(self.resetButton, QtCore.SIGNAL("clicked()"), self.resetNodes) - self.editButton = QtGui.QPushButton(self.optwid) + self.editButton = QtGui.QPushButton(self.nodes_widget) self.editButton.setIcon(QtGui.QIcon(":/icons/Draft_Edit.svg")) self.editButton.setText(QtGui.QApplication.translate("Arch", "Edit nodes", None)) lay.addWidget(self.editButton) QtCore.QObject.connect(self.editButton, QtCore.SIGNAL("clicked()"), self.editNodes) - self.extendButton = QtGui.QPushButton(self.optwid) + self.extendButton = QtGui.QPushButton(self.nodes_widget) self.extendButton.setIcon(QtGui.QIcon(":/icons/Snap_Perpendicular.svg")) self.extendButton.setText(QtGui.QApplication.translate("Arch", "Extend nodes", None)) self.extendButton.setToolTip(QtGui.QApplication.translate("Arch", "Extends the nodes of this element to reach the nodes of another element", None)) lay.addWidget(self.extendButton) QtCore.QObject.connect(self.extendButton, QtCore.SIGNAL("clicked()"), self.extendNodes) - self.connectButton = QtGui.QPushButton(self.optwid) + self.connectButton = QtGui.QPushButton(self.nodes_widget) self.connectButton.setIcon(QtGui.QIcon(":/icons/Snap_Intersection.svg")) self.connectButton.setText(QtGui.QApplication.translate("Arch", "Connect nodes", None)) self.connectButton.setToolTip(QtGui.QApplication.translate("Arch", "Connects nodes of this element with the nodes of another element", None)) lay.addWidget(self.connectButton) QtCore.QObject.connect(self.connectButton, QtCore.SIGNAL("clicked()"), self.connectNodes) - self.toggleButton = QtGui.QPushButton(self.optwid) + self.toggleButton = QtGui.QPushButton(self.nodes_widget) self.toggleButton.setIcon(QtGui.QIcon(":/icons/dagViewVisible.svg")) self.toggleButton.setText(QtGui.QApplication.translate("Arch", "Toggle all nodes", None)) self.toggleButton.setToolTip(QtGui.QApplication.translate("Arch", "Toggles all structural nodes of the document on/off", None)) lay.addWidget(self.toggleButton) QtCore.QObject.connect(self.toggleButton, QtCore.SIGNAL("clicked()"), self.toggleNodes) - self.form = [self.form,self.optwid] + self.extrusion_widget = QtGui.QWidget() + self.extrusion_widget.setWindowTitle(QtGui.QApplication.translate("Arch", "Extrusion Tools", None)) + lay = QtGui.QVBoxLayout(self.extrusion_widget) + + self.selectToolButton = QtGui.QPushButton(self.extrusion_widget) + self.selectToolButton.setIcon(QtGui.QIcon()) + self.selectToolButton.setText(QtGui.QApplication.translate("Arch", "Select tool...", None)) + self.selectToolButton.setToolTip(QtGui.QApplication.translate("Arch", "Select object or edges to be used as a Tool (extrusion path)", None)) + lay.addWidget(self.selectToolButton) + QtCore.QObject.connect(self.selectToolButton, QtCore.SIGNAL("clicked()"), self.setSelectionFromTool) + + self.form = [self.form, self.nodes_widget, self.extrusion_widget] self.Object = obj self.observer = None self.nodevis = None @@ -1177,6 +1226,42 @@ class StructureTaskPanel(ArchComponent.ComponentTaskPanel): self.nodevis.append([obj,obj.ViewObject.ShowNodes]) obj.ViewObject.ShowNodes = True + def setSelectionFromTool(self): + FreeCADGui.Selection.clearSelection() + if hasattr(self.Object, "Tool"): + tool = self.Object.Tool + if hasattr(tool, "Shape") and tool.Shape: + FreeCADGui.Selection.addSelection(tool) + else: + if not isinstance(tool, list): + tool = [tool] + for o, subs in tool: + FreeCADGui.Selection.addSelection(o, subs) + QtCore.QObject.disconnect(self.selectToolButton, QtCore.SIGNAL("clicked()"), self.setSelectionFromTool) + QtCore.QObject.connect(self.selectToolButton, QtCore.SIGNAL("clicked()"), self.setToolFromSelection) + self.selectToolButton.setText(QtGui.QApplication.translate("Arch", "Done", None)) + + def setToolFromSelection(self): + objectList = [] + selEx = FreeCADGui.Selection.getSelectionEx() + for selExi in selEx: + if len(selExi.SubElementNames) == 0: + # Add entirely selected objects + objectList.append(selExi.Object) + else: + subElementsNames = [subElementName for subElementName in selExi.SubElementNames if subElementName.startswith("Edge")] + # Check that at least an edge is selected from the object's shape + if len(subElementsNames) > 0: + objectList.append((selExi.Object, subElementsNames)) + if self.Object.getTypeIdOfProperty("Tool") != "App::PropertyLinkSubList": + # Upgrade property Tool from App::PropertyLink to App::PropertyLinkSubList (note: Undo/Redo fails) + self.Object.removeProperty("Tool") + self.Object.addProperty("App::PropertyLinkSubList", "Tool", "Structure", QT_TRANSLATE_NOOP("App::Property", "An optional extrusion path for this element")) + self.Object.Tool = objectList + QtCore.QObject.disconnect(self.selectToolButton, QtCore.SIGNAL("clicked()"), self.setToolFromSelection) + QtCore.QObject.connect(self.selectToolButton, QtCore.SIGNAL("clicked()"), self.setSelectionFromTool) + self.selectToolButton.setText(QtGui.QApplication.translate("Arch", "Select tool...", None)) + def accept(self): if self.observer: From 856063090d68dc6a183e476679e0106035cb006d Mon Sep 17 00:00:00 2001 From: alafr Date: Thu, 16 Apr 2020 22:48:39 +0200 Subject: [PATCH 05/14] Arch Structure: fix the nodes default placement. --- src/Mod/Arch/ArchStructure.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Mod/Arch/ArchStructure.py b/src/Mod/Arch/ArchStructure.py index 81de3c8b52..68962657ab 100644 --- a/src/Mod/Arch/ArchStructure.py +++ b/src/Mod/Arch/ArchStructure.py @@ -883,15 +883,15 @@ class _Structure(ArchComponent.Component): extdata = self.getExtrusionData(obj) if extdata and not isinstance(extdata[0],list): nodes = extdata[0] - ev = extdata[2].Rotation.multVec(extdata[1]) - nodes.Placement = nodes.Placement.multiply(extdata[2]) if IfcType not in ["Slab"]: if not isinstance(extdata[1], FreeCAD.Vector): nodes = extdata[1] elif extdata[1].Length > 0: if hasattr(nodes,"CenterOfMass"): import Part - nodes = Part.LineSegment(nodes.CenterOfMass,nodes.CenterOfMass.add(ev)).toShape() + nodes = Part.LineSegment(nodes.CenterOfMass,nodes.CenterOfMass.add(extdata[1])).toShape() + if isinstance(extdata[1], FreeCAD.Vector): + nodes.Placement = nodes.Placement.multiply(extdata[2]) offset = FreeCAD.Vector() if hasattr(obj,"NodesOffset"): offset = FreeCAD.Vector(0,0,obj.NodesOffset.Value) From fda4198ef0903b594a9d379f8709a0f6d815bfd9 Mon Sep 17 00:00:00 2001 From: alafr Date: Fri, 10 Apr 2020 12:23:16 +0200 Subject: [PATCH 06/14] Arch Structure: fix extrusion orientation --- src/Mod/Arch/ArchStructure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Arch/ArchStructure.py b/src/Mod/Arch/ArchStructure.py index 68962657ab..11d6ee6740 100644 --- a/src/Mod/Arch/ArchStructure.py +++ b/src/Mod/Arch/ArchStructure.py @@ -850,7 +850,7 @@ class _Structure(ArchComponent.Component): inverse_placement = placement.inverse() if extrusion: if len(extrusion.Edges) == 1 and DraftGeomUtils.geomType(extrusion.Edges[0]) == "Line": - extrusion = DraftGeomUtils.vec(extrusion.Edges[0]) + extrusion = DraftGeomUtils.vec(extrusion.Edges[0], True) if isinstance(extrusion, FreeCAD.Vector): extrusion = inverse_placement.Rotation.multVec(extrusion) elif normal: From 3f72d434c58b929b9d1d870beb623d389eb5cf31 Mon Sep 17 00:00:00 2001 From: alafr Date: Wed, 8 Apr 2020 22:54:04 +0200 Subject: [PATCH 07/14] Arch structure : workflow with master sketch * Add a command that creates multiple Arch Structure objects from a selected base, using each selected edge as an extrusion path. It creates one Arch Structure object from each edge. Therefore, the Arch Structure objects can then be individually edited if needed. * Split the command Arch Structural System from Arch Structure. For backwards compatibility, the command Arch Structure will run the command Arch Structural System when the requirements for the Arch Structural System (Structure + Axis) are met. --- src/Mod/Arch/ArchStructure.py | 99 +++++++++++++++++++++++++++++++---- src/Mod/Arch/InitGui.py | 2 +- 2 files changed, 89 insertions(+), 12 deletions(-) diff --git a/src/Mod/Arch/ArchStructure.py b/src/Mod/Arch/ArchStructure.py index 11d6ee6740..6a45939825 100644 --- a/src/Mod/Arch/ArchStructure.py +++ b/src/Mod/Arch/ArchStructure.py @@ -185,6 +185,77 @@ def placeAlongEdge(p1,p2,horizontal=False): return pl +class CommandStructuresFromSelection: + """ The Arch Structures from selection command definition. """ + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Arch_MultipleStructures', + 'MenuText': QT_TRANSLATE_NOOP("Arch_Structure", "Multiple Structures"), + 'ToolTip': QT_TRANSLATE_NOOP("Arch_Structure", "Create multiple Arch Structure objects from a selected base, using each selected edge as an extrusion path")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + selex = FreeCADGui.Selection.getSelectionEx() + if len(selex) >= 2: + FreeCAD.ActiveDocument.openTransaction(translate("Arch", "Create Structures From Selection")) + FreeCADGui.addModule("Arch") + FreeCADGui.addModule("Draft") + base = selex[0].Object # The first selected object is the base for the Structure objects + for selexi in selex[1:]: # All the edges from the other objects are used as a Tool (extrusion paths) + if len(selexi.SubElementNames) == 0: + subelement_names = ["Edge" + str(i) for i in range(1, len(selexi.Object.Shape.Edges) + 1)] + else: + subelement_names = [sub for sub in selexi.SubElementNames if sub.startswith("Edge")] + for sub in subelement_names: + FreeCADGui.doCommand("structure = Arch.makeStructure(FreeCAD.ActiveDocument." + base.Name + ")") + FreeCADGui.doCommand("structure.Tool = (FreeCAD.ActiveDocument." + selexi.Object.Name + ", '" + sub + "')") + FreeCADGui.doCommand("structure.BasePerpendicularToTool = True") + FreeCADGui.doCommand("Draft.autogroup(structure)") + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + else: + FreeCAD.Console.PrintError(translate("Arch", "Please select the base object first and then the edges to use as extrusion paths") + "\n") + + +class CommandStructuralSystem: + """ The Arch Structural System command definition. """ + + def __init__(self): + pass + + def GetResources(self): + return {'Pixmap': 'Arch_StructuralSystem', + 'MenuText': QT_TRANSLATE_NOOP("Arch_Structure", "Structural System"), + 'ToolTip': QT_TRANSLATE_NOOP("Arch_Structure", "Create a structural system object from a selected structure and axis")} + + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + def Activated(self): + sel = FreeCADGui.Selection.getSelection() + if sel: + st = Draft.getObjectsOfType(sel, "Structure") + ax = Draft.getObjectsOfType(sel, "Axis") + if ax: + FreeCAD.ActiveDocument.openTransaction(translate("Arch", "Create Structural System")) + FreeCADGui.addModule("Arch") + if st: + FreeCADGui.doCommand("obj = Arch.makeStructuralSystem(" + ArchCommands.getStringList(st) + ", " + ArchCommands.getStringList(ax) + ")") + else: + FreeCADGui.doCommand("obj = Arch.makeStructuralSystem(axes = " + ArchCommands.getStringList(ax) + ")") + FreeCADGui.addModule("Draft") + FreeCADGui.doCommand("Draft.autogroup(obj)") + FreeCAD.ActiveDocument.commitTransaction() + FreeCAD.ActiveDocument.recompute() + else: + FreeCAD.Console.PrintError(translate("Arch", "Please select at least an axis object") + "\n") + + class _CommandStructure: "the Arch Structure command definition" @@ -224,16 +295,7 @@ class _CommandStructure: st = Draft.getObjectsOfType(sel,"Structure") ax = Draft.getObjectsOfType(sel,"Axis") if ax: - FreeCAD.ActiveDocument.openTransaction(translate("Arch","Create Structural System")) - FreeCADGui.addModule("Arch") - if st: - FreeCADGui.doCommand("obj = Arch.makeStructuralSystem(" + ArchCommands.getStringList(st) + "," + ArchCommands.getStringList(ax) + ")") - else: - FreeCADGui.doCommand("obj = Arch.makeStructuralSystem(axes=" + ArchCommands.getStringList(ax) + ")") - FreeCADGui.addModule("Draft") - FreeCADGui.doCommand("Draft.autogroup(obj)") - FreeCAD.ActiveDocument.commitTransaction() - FreeCAD.ActiveDocument.recompute() + FreeCADGui.runCommand("Arch_StructuralSystem") return elif not(ax) and not(st): FreeCAD.ActiveDocument.openTransaction(translate("Arch","Create Structure")) @@ -1395,4 +1457,19 @@ class _ViewProviderStructuralSystem(ArchComponent.ViewProviderComponent): if FreeCAD.GuiUp: - FreeCADGui.addCommand('Arch_Structure',_CommandStructure()) + FreeCADGui.addCommand("Arch_Structure", _CommandStructure()) + FreeCADGui.addCommand("Arch_StructuralSystem", CommandStructuralSystem()) + FreeCADGui.addCommand("Arch_StructuresFromSelection", CommandStructuresFromSelection()) + + class _ArchStructureGroupCommand: + + def GetCommands(self): + return ("Arch_Structure", "Arch_StructuralSystem", "Arch_StructuresFromSelection") + def GetResources(self): + return { "MenuText": QT_TRANSLATE_NOOP("Arch_Structure", "Structure tools"), + "ToolTip": QT_TRANSLATE_NOOP("Arch_Structure", "Structure tools") + } + def IsActive(self): + return not FreeCAD.ActiveDocument is None + + FreeCADGui.addCommand("Arch_StructureTools", _ArchStructureGroupCommand()) diff --git a/src/Mod/Arch/InitGui.py b/src/Mod/Arch/InitGui.py index d6317f3efd..facfa2b4f8 100644 --- a/src/Mod/Arch/InitGui.py +++ b/src/Mod/Arch/InitGui.py @@ -57,7 +57,7 @@ class ArchWorkbench(FreeCADGui.Workbench): import Arch # Set up command lists - self.archtools = ["Arch_Wall", "Arch_Structure", "Arch_Rebar", + self.archtools = ["Arch_Wall", "Arch_StructureTools", "Arch_Rebar", "Arch_CurtainWall","Arch_BuildingPart", "Arch_Project", "Arch_Site", "Arch_Building", "Arch_Floor", "Arch_Reference", From 0feb039deb36fcd227bfe09e7dcfe581d01f94f0 Mon Sep 17 00:00:00 2001 From: alafr Date: Wed, 8 Apr 2020 22:23:43 +0200 Subject: [PATCH 08/14] Arch Structure: multiple structures command icon --- src/Mod/Arch/Resources/Arch.qrc | 1 + .../icons/Arch_MultipleStructures.svg | 264 ++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 src/Mod/Arch/Resources/icons/Arch_MultipleStructures.svg diff --git a/src/Mod/Arch/Resources/Arch.qrc b/src/Mod/Arch/Resources/Arch.qrc index 9fd513dc8e..749c4ea26e 100644 --- a/src/Mod/Arch/Resources/Arch.qrc +++ b/src/Mod/Arch/Resources/Arch.qrc @@ -37,6 +37,7 @@ icons/Arch_Material_Multi.svg icons/Arch_MergeWalls.svg icons/Arch_MeshToShape.svg + icons/Arch_MultipleStructures.svg icons/Arch_Nest.svg icons/Arch_Panel.svg icons/Arch_Panel_Clone.svg diff --git a/src/Mod/Arch/Resources/icons/Arch_MultipleStructures.svg b/src/Mod/Arch/Resources/icons/Arch_MultipleStructures.svg new file mode 100644 index 0000000000..33eb2ea45b --- /dev/null +++ b/src/Mod/Arch/Resources/icons/Arch_MultipleStructures.svg @@ -0,0 +1,264 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Antoine Lafr + + + Arch_Structure + 2020-04-11 + http://www.freecadweb.org/wiki/index.php?title=Artwork + + + FreeCAD + + + FreeCAD/src/Mod/Arch/Resources/icons/Arch_MultipleStructures.svg + + + FreeCAD LGPL2+ + + + https://www.gnu.org/copyleft/lesser.html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 2ae21293832c9c8d9d84465bcbe241c940201e85 Mon Sep 17 00:00:00 2001 From: alafr Date: Fri, 10 Apr 2020 18:49:17 +0200 Subject: [PATCH 09/14] Draft: test for DraftGeomUtils.get_extended_wire --- src/Mod/Draft/CMakeLists.txt | 1 + src/Mod/Draft/TestDraft.py | 4 + .../Draft/drafttests/test_draftgeomutils.py | 142 ++++++++++++++++++ 3 files changed, 147 insertions(+) create mode 100644 src/Mod/Draft/drafttests/test_draftgeomutils.py diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index b684d6f33d..fd2535451a 100644 --- a/src/Mod/Draft/CMakeLists.txt +++ b/src/Mod/Draft/CMakeLists.txt @@ -62,6 +62,7 @@ SET(Draft_tests drafttests/test_dwg.py drafttests/test_oca.py drafttests/test_airfoildat.py + drafttests/test_draftgeomutils.py drafttests/draft_test_objects.py drafttests/README.md ) diff --git a/src/Mod/Draft/TestDraft.py b/src/Mod/Draft/TestDraft.py index f06b1de2c9..094902d278 100644 --- a/src/Mod/Draft/TestDraft.py +++ b/src/Mod/Draft/TestDraft.py @@ -108,6 +108,9 @@ from drafttests.test_dwg import DraftDWG as DraftTest06 # from drafttests.test_oca import DraftOCA as DraftTest07 # from drafttests.test_airfoildat import DraftAirfoilDAT as DraftTest08 +# Testing the utils module +from drafttests.test_draftgeomutils import TestDraftGeomUtils as DraftTest09 + # Use the modules so that code checkers don't complain (flake8) True if DraftTest01 else False True if DraftTest02 else False @@ -117,3 +120,4 @@ True if DraftTest05 else False True if DraftTest06 else False # True if DraftTest07 else False # True if DraftTest08 else False +True if DraftTest09 else False diff --git a/src/Mod/Draft/drafttests/test_draftgeomutils.py b/src/Mod/Draft/drafttests/test_draftgeomutils.py new file mode 100644 index 0000000000..263f7d3115 --- /dev/null +++ b/src/Mod/Draft/drafttests/test_draftgeomutils.py @@ -0,0 +1,142 @@ +""" Unit test for the DraftGeomUtils module. """ +# *************************************************************************** +# * Copyright (c) 2020 Antoine Lafr * +# * * +# * This file is part of the FreeCAD CAx development system. * +# * * +# * 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. * +# * * +# * FreeCAD 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 FreeCAD; if not, write to the Free Software * +# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * +# * USA * +# * * +# *************************************************************************** + +import unittest +import FreeCAD +import Part +import DraftGeomUtils +import drafttests.auxiliary as aux +from draftutils.messages import _msg + +class TestDraftGeomUtils(unittest.TestCase): + """ Testing the functions in the file DraftGeomUtils.py """ + + def setUp(self): + """ Prepare the test. Nothing to do here, DraftGeomUtils doesn't need a document. """ + aux.draw_header() + + def test_get_extended_wire(self): + """ Test the DraftGeomUtils.get_extended_wire function. """ + operation = "DraftGeomUtils.get_extended_wire" + _msg(" Test '{}'".format(operation)) + + # Build wires made with straight edges and various combination of Orientation: the wires 1-4 are all equivalent + points = [FreeCAD.Vector(0.0, 0.0, 0.0), + FreeCAD.Vector(1500.0, 2000.0, 0.0), + FreeCAD.Vector(4500.0, 2000.0, 0.0), + FreeCAD.Vector(4500.0, 2000.0, 2500.0)] + + edges = [] + for start, end in zip(points[:-1], points[1:]): + edge = Part.makeLine(start, end) + edges.append(edge) + wire1 = Part.Wire(edges) + + edges = [] + for start, end in zip(points[:-1], points[1:]): + edge = Part.makeLine(end, start) + edge.Orientation = "Reversed" + edges.append(edge) + wire2 = Part.Wire(edges) + + edges = [] + for start, end in zip(points[:-1], points[1:]): + edge = Part.makeLine(start, end) + edge.Orientation = "Reversed" + edges.insert(0, edge) + wire3 = Part.Wire(edges) + wire3.Orientation = "Reversed" + + edges = [] + for start, end in zip(points[:-1], points[1:]): + edge = Part.makeLine(end, start) + edges.insert(0, edge) + wire4 = Part.Wire(edges) + wire4.Orientation = "Reversed" + + # Build wires made with arcs and various combination of Orientation: the wires 5-8 are all equivalent + points = [FreeCAD.Vector(0.0, 0.0, 0.0), + FreeCAD.Vector(1000.0, 1000.0, 0.0), + FreeCAD.Vector(2000.0, 0.0, 0.0), + FreeCAD.Vector(3000.0, 0.0, 1000.0), + FreeCAD.Vector(4000.0, 0.0, 0.0)] + + edges = [] + for start, mid, end in zip(points[:-2], points[1:-1], points[2:]): + edge = Part.Arc(start, mid, end).toShape() + edges.append(edge) + wire5 = Part.Wire(edges) + + edges = [] + for start, mid, end in zip(points[:-2], points[1:-1], points[2:]): + edge = Part.Arc(end, mid, start).toShape() + edge.Orientation = "Reversed" + edges.append(edge) + wire6 = Part.Wire(edges) + + edges = [] + for start, mid, end in zip(points[:-2], points[1:-1], points[2:]): + edge = Part.Arc(start, mid, end).toShape() + edge.Orientation = "Reversed" + edges.insert(0, edge) + wire7 = Part.Wire(edges) + wire7.Orientation = "Reversed" + + edges = [] + for start, mid, end in zip(points[:-2], points[1:-1], points[2:]): + edge = Part.Arc(end, mid, start).toShape() + edges.insert(0, edge) + wire8 = Part.Wire(edges) + wire8.Orientation = "Reversed" + + # Run "get_extended_wire" for all the wires with various offset_start, offset_end combinations + num_subtests = 0 + offset_values = (2000.0, 0.0, -1000, -2000, -3000, -5500) + for i, wire in enumerate((wire1, wire2, wire3, wire4, wire5, wire6, wire7, wire8)): + _msg(" Running tests with wire{}".format(i + 1)) + for offset_start in offset_values: + for offset_end in offset_values: + if offset_start + offset_end > -wire.Length: + subtest = "get_extended_wire(wire{0}, {1}, {2})".format(i + 1, offset_start, offset_end) + num_subtests += 1 # TODO: it should be "with self.subtest(subtest):" but then it doesn't report failures. + extended = DraftGeomUtils.get_extended_wire(wire, offset_start, offset_end) + # Test that the extended wire's length is correctly changed + self.assertAlmostEqual(extended.Length, wire.Length + offset_start + offset_end, + DraftGeomUtils.precision(), "'{0}.{1}' failed".format(operation, subtest)) + if offset_start == 0.0: + # If offset_start is 0.0, check that the wire's start point is unchanged + self.assertAlmostEqual(extended.OrderedVertexes[0].Point.distanceToPoint(wire.OrderedVertexes[0].Point), 0.0, + DraftGeomUtils.precision(), "'{0}.{1}' failed".format(operation, subtest)) + if offset_end == 0.0: + # If offset_end is 0.0, check that the wire's end point is unchanged + self.assertAlmostEqual(extended.OrderedVertexes[-1].Point.distanceToPoint(wire.OrderedVertexes[-1].Point), 0.0, + DraftGeomUtils.precision(), "'{0}.{1}' failed".format(operation, subtest)) + _msg(" Test completed, {} subtests run".format(num_subtests)) + + def tearDown(self): + """ Finish the test. Nothing to do here, DraftGeomUtils doesn't need a document. """ + pass + +# suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDraftGeomUtils) +# unittest.TextTestRunner().run(suite) From 3186ddd02935fa7c474b04325c22903d8598e728 Mon Sep 17 00:00:00 2001 From: alafr Date: Mon, 15 Jun 2020 19:56:47 +0200 Subject: [PATCH 10/14] DraftGeomUtils: Doctstring formatting Adress vocx-fc's review --- src/Mod/Draft/draftgeoutils/edges.py | 4 ++-- src/Mod/Draft/draftgeoutils/general.py | 8 +++++--- src/Mod/Draft/draftgeoutils/wires.py | 10 ++++++---- src/Mod/Draft/drafttests/test_draftgeomutils.py | 10 +++++----- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Mod/Draft/draftgeoutils/edges.py b/src/Mod/Draft/draftgeoutils/edges.py index 4783cfb5f0..7bfda8256a 100644 --- a/src/Mod/Draft/draftgeoutils/edges.py +++ b/src/Mod/Draft/draftgeoutils/edges.py @@ -223,8 +223,8 @@ def getTangent(edge, from_point=None): def get_referenced_edges(property_value): - """ Returns the Edges referenced by the value of a App:PropertyLink, App::PropertyLinkList, - App::PropertyLinkSub or App::PropertyLinkSubList property. """ + """Return the Edges referenced by the value of a App:PropertyLink, App::PropertyLinkList, + App::PropertyLinkSub or App::PropertyLinkSubList property.""" edges = [] if not isinstance(property_value, list): property_value = [property_value] diff --git a/src/Mod/Draft/draftgeoutils/general.py b/src/Mod/Draft/draftgeoutils/general.py index 30fbfe8a27..2d358cb019 100644 --- a/src/Mod/Draft/draftgeoutils/general.py +++ b/src/Mod/Draft/draftgeoutils/general.py @@ -60,9 +60,11 @@ def precision(): def vec(edge, use_orientation = False): - """ vec(edge[, use_orientation]) or vec(line): returns a vector from an edge or a Part.LineSegment. - If use_orientation is True, it takes into account the edges orientation. - If edge is not straight, you'll get strange results! """ + """Return a vector from an edge or a Part.LineSegment. + + If use_orientation is True, it takes into account the edges orientation. + If edge is not straight, you'll get strange results! + """ if isinstance(edge, Part.Edge): if edge.Orientation == "Forward" or not use_orientation: return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) diff --git a/src/Mod/Draft/draftgeoutils/wires.py b/src/Mod/Draft/draftgeoutils/wires.py index a2cd9d7368..8f4fdc10fc 100644 --- a/src/Mod/Draft/draftgeoutils/wires.py +++ b/src/Mod/Draft/draftgeoutils/wires.py @@ -436,7 +436,7 @@ def tessellateProjection(shape, seglen): return Part.makeCompound(newedges) def get_placement_perpendicular_to_wire(wire): - """ Returns the placement whose base is the wire's first vertex and it's z axis aligned to the wire's tangent. """ + """Return the placement whose base is the wire's first vertex and it's z axis aligned to the wire's tangent.""" pl = FreeCAD.Placement() if wire.Length > 0.0: pl.Base = wire.OrderedVertexes[0].Point @@ -452,9 +452,11 @@ def get_placement_perpendicular_to_wire(wire): def get_extended_wire(wire, offset_start, offset_end): - """ Returns a wire trimmed (negative offset) or extended (positive offset) at its first vertex, last vertex or both ends. - get_extended_wire(wire, -100.0, 0.0) -> returns a copy of the wire with its first 100 mm removed - get_extended_wire(wire, 0.0, 100.0) -> returns a copy of the wire extended by 100 mm after it's last vertex """ + """Return a wire trimmed (negative offset) or extended (positive offset) at its first vertex, last vertex or both ends. + + get_extended_wire(wire, -100.0, 0.0) -> returns a copy of the wire with its first 100 mm removed + get_extended_wire(wire, 0.0, 100.0) -> returns a copy of the wire extended by 100 mm after it's last vertex + """ if min(offset_start, offset_end, offset_start + offset_end) <= -wire.Length: FreeCAD.Console.PrintError("debug: get_extended_wire error, wire's length insufficient for trimming.\n") return wire diff --git a/src/Mod/Draft/drafttests/test_draftgeomutils.py b/src/Mod/Draft/drafttests/test_draftgeomutils.py index 263f7d3115..3cf94f1c38 100644 --- a/src/Mod/Draft/drafttests/test_draftgeomutils.py +++ b/src/Mod/Draft/drafttests/test_draftgeomutils.py @@ -1,4 +1,3 @@ -""" Unit test for the DraftGeomUtils module. """ # *************************************************************************** # * Copyright (c) 2020 Antoine Lafr * # * * @@ -21,6 +20,7 @@ # * USA * # * * # *************************************************************************** +"""Unit test for the DraftGeomUtils module.""" import unittest import FreeCAD @@ -30,14 +30,14 @@ import drafttests.auxiliary as aux from draftutils.messages import _msg class TestDraftGeomUtils(unittest.TestCase): - """ Testing the functions in the file DraftGeomUtils.py """ + """Testing the functions in the file DraftGeomUtils.py""" def setUp(self): - """ Prepare the test. Nothing to do here, DraftGeomUtils doesn't need a document. """ + """Prepare the test. Nothing to do here, DraftGeomUtils doesn't need a document.""" aux.draw_header() def test_get_extended_wire(self): - """ Test the DraftGeomUtils.get_extended_wire function. """ + """Test the DraftGeomUtils.get_extended_wire function.""" operation = "DraftGeomUtils.get_extended_wire" _msg(" Test '{}'".format(operation)) @@ -135,7 +135,7 @@ class TestDraftGeomUtils(unittest.TestCase): _msg(" Test completed, {} subtests run".format(num_subtests)) def tearDown(self): - """ Finish the test. Nothing to do here, DraftGeomUtils doesn't need a document. """ + """Finish the test. Nothing to do here, DraftGeomUtils doesn't need a document.""" pass # suite = unittest.defaultTestLoader.loadTestsFromTestCase(TestDraftGeomUtils) From 163e2460b01aae15aab4c8dd5deed177927e7eb7 Mon Sep 17 00:00:00 2001 From: alafr Date: Thu, 18 Jun 2020 08:16:28 +0200 Subject: [PATCH 11/14] Fix missing import --- src/Mod/Draft/draftgeoutils/wires.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Draft/draftgeoutils/wires.py b/src/Mod/Draft/draftgeoutils/wires.py index 8f4fdc10fc..cd102da8dd 100644 --- a/src/Mod/Draft/draftgeoutils/wires.py +++ b/src/Mod/Draft/draftgeoutils/wires.py @@ -32,6 +32,7 @@ import lazy_loader.lazy_loader as lz import FreeCAD as App import DraftVecUtils import WorkingPlane +import FreeCAD from draftgeoutils.general import geomType, vec, precision from draftgeoutils.geometry import get_normal From e66ec4b659d1a12373cc328a4b2280b67f4507fb Mon Sep 17 00:00:00 2001 From: alafr Date: Thu, 18 Jun 2020 17:30:48 +0200 Subject: [PATCH 12/14] DraftGeomUtils: import FreeCAD as App --- src/Mod/Draft/draftgeoutils/wires.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Mod/Draft/draftgeoutils/wires.py b/src/Mod/Draft/draftgeoutils/wires.py index cd102da8dd..5ebba5a889 100644 --- a/src/Mod/Draft/draftgeoutils/wires.py +++ b/src/Mod/Draft/draftgeoutils/wires.py @@ -32,7 +32,7 @@ import lazy_loader.lazy_loader as lz import FreeCAD as App import DraftVecUtils import WorkingPlane -import FreeCAD +import FreeCAD as App from draftgeoutils.general import geomType, vec, precision from draftgeoutils.geometry import get_normal @@ -438,7 +438,7 @@ def tessellateProjection(shape, seglen): def get_placement_perpendicular_to_wire(wire): """Return the placement whose base is the wire's first vertex and it's z axis aligned to the wire's tangent.""" - pl = FreeCAD.Placement() + pl = App.Placement() if wire.Length > 0.0: pl.Base = wire.OrderedVertexes[0].Point first_edge = wire.OrderedEdges[0] @@ -446,9 +446,9 @@ def get_placement_perpendicular_to_wire(wire): zaxis = -first_edge.tangentAt(first_edge.FirstParameter) else: zaxis = first_edge.tangentAt(first_edge.LastParameter) - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), FreeCAD.Vector(0, 0, 1), zaxis, "ZYX") + pl.Rotation = App.Rotation(App.Vector(1, 0, 0), App.Vector(0, 0, 1), zaxis, "ZYX") else: - FreeCAD.Console.PrintError("debug: get_placement_perpendicular_to_wire called with a zero-length wire.\n") + App.Console.PrintError("debug: get_placement_perpendicular_to_wire called with a zero-length wire.\n") return pl @@ -459,7 +459,7 @@ def get_extended_wire(wire, offset_start, offset_end): get_extended_wire(wire, 0.0, 100.0) -> returns a copy of the wire extended by 100 mm after it's last vertex """ if min(offset_start, offset_end, offset_start + offset_end) <= -wire.Length: - FreeCAD.Console.PrintError("debug: get_extended_wire error, wire's length insufficient for trimming.\n") + App.Console.PrintError("debug: get_extended_wire error, wire's length insufficient for trimming.\n") return wire if offset_start < 0: # Trim the wire from the first vertex offset_start = -offset_start From 6a299ac3f686ade4975476d7431257622e070596 Mon Sep 17 00:00:00 2001 From: alafr Date: Thu, 18 Jun 2020 17:39:20 +0200 Subject: [PATCH 13/14] DraftGeomUtils: formatting Restore a change from commit #97235de that i accidentally deleted during conflict resolution --- src/Mod/Draft/draftgeoutils/general.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mod/Draft/draftgeoutils/general.py b/src/Mod/Draft/draftgeoutils/general.py index 2d358cb019..8235a0078c 100644 --- a/src/Mod/Draft/draftgeoutils/general.py +++ b/src/Mod/Draft/draftgeoutils/general.py @@ -70,7 +70,7 @@ def vec(edge, use_orientation = False): return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) else: return edge.Vertexes[0].Point.sub(edge.Vertexes[-1].Point) - elif isinstance(edge,Part.LineSegment): + elif isinstance(edge, Part.LineSegment): return edge.EndPoint.sub(edge.StartPoint) else: return None From 5bf974a0e899f59d0670b9376fa349933102f45c Mon Sep 17 00:00:00 2001 From: alafr Date: Fri, 11 Sep 2020 22:36:53 +0200 Subject: [PATCH 14/14] Draft: Conform to vocx-fc's review - check for Part.Shape as it was before, to avoid any risk of breaking other tools - change the order of Draft tests --- src/Mod/Draft/TestDraft.py | 20 ++++++++++---------- src/Mod/Draft/draftgeoutils/general.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Mod/Draft/TestDraft.py b/src/Mod/Draft/TestDraft.py index 094902d278..6845cfb726 100644 --- a/src/Mod/Draft/TestDraft.py +++ b/src/Mod/Draft/TestDraft.py @@ -101,15 +101,15 @@ from drafttests.test_import import DraftImport as DraftTest01 from drafttests.test_creation import DraftCreation as DraftTest02 from drafttests.test_modification import DraftModification as DraftTest03 -# Handling of file formats tests -from drafttests.test_svg import DraftSVG as DraftTest04 -from drafttests.test_dxf import DraftDXF as DraftTest05 -from drafttests.test_dwg import DraftDWG as DraftTest06 -# from drafttests.test_oca import DraftOCA as DraftTest07 -# from drafttests.test_airfoildat import DraftAirfoilDAT as DraftTest08 - # Testing the utils module -from drafttests.test_draftgeomutils import TestDraftGeomUtils as DraftTest09 +from drafttests.test_draftgeomutils import TestDraftGeomUtils as DraftTest04 + +# Handling of file formats tests +from drafttests.test_svg import DraftSVG as DraftTest05 +from drafttests.test_dxf import DraftDXF as DraftTest06 +from drafttests.test_dwg import DraftDWG as DraftTest07 +# from drafttests.test_oca import DraftOCA as DraftTest08 +# from drafttests.test_airfoildat import DraftAirfoilDAT as DraftTest09 # Use the modules so that code checkers don't complain (flake8) True if DraftTest01 else False @@ -118,6 +118,6 @@ True if DraftTest03 else False True if DraftTest04 else False True if DraftTest05 else False True if DraftTest06 else False -# True if DraftTest07 else False +True if DraftTest07 else False # True if DraftTest08 else False -True if DraftTest09 else False +# True if DraftTest09 else False diff --git a/src/Mod/Draft/draftgeoutils/general.py b/src/Mod/Draft/draftgeoutils/general.py index 8235a0078c..8c44dbbc8e 100644 --- a/src/Mod/Draft/draftgeoutils/general.py +++ b/src/Mod/Draft/draftgeoutils/general.py @@ -65,11 +65,11 @@ def vec(edge, use_orientation = False): If use_orientation is True, it takes into account the edges orientation. If edge is not straight, you'll get strange results! """ - if isinstance(edge, Part.Edge): - if edge.Orientation == "Forward" or not use_orientation: - return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) - else: + if isinstance(edge, Part.Shape): + if use_orientation and isinstance(edge, Part.Edge) and edge.Orientation == "Reversed": return edge.Vertexes[0].Point.sub(edge.Vertexes[-1].Point) + else: + return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) elif isinstance(edge, Part.LineSegment): return edge.EndPoint.sub(edge.StartPoint) else: