From ff521b1a5354c830a9f759b2edcc5348ebbc30fd 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 c743f3f083f05ba5685976216776540e79cd3cfa 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 c1cf0c0f8b214ee86c159a8d1991215a06102280 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 7938cf498fdd4c62f180d68ca66fbf23d723d8d9 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 66c30313b0869fa63b06a60ac738ac1f54301059 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 dba9585d89341b69656038a45eb05870b8b0a9c0 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 f6e0638cab9ead6d4042646bd3ec738d32acfb7b 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 5abe618d652c8e1fa5d641cd44a065b330fc6c14 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 5aa68ea29462d98a9f4f4ed620648587d9c88b2d 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 942b4748fce59b1de2bcb32d29bf750455159801 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 d310986cd8df549ffb05cf90cc011841955a1ed8 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 afd644fd7235f78224169d73dc50a6a029c1166d 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 8a17342b6936f7b3c97b0043e165cc790d973140 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 #add4897 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 d4b65cffff7d1dc8888ef59d61ccfa6229da6d20 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: