diff --git a/src/Mod/Arch/ArchStructure.py b/src/Mod/Arch/ArchStructure.py index 67f4d52427..e7f3e385b5 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")) @@ -611,7 +673,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,31 +738,38 @@ 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): 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) + 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(): @@ -712,9 +797,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 +811,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 +825,8 @@ class _Structure(ArchComponent.Component): if not DraftGeomUtils.isCoplanar(obj.Base.Shape.Faces,tol=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 +834,6 @@ class _Structure(ArchComponent.Component): except Exception: 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 +847,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,22 +865,58 @@ 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 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 baseface: + 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() - 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 len(extrusion.Edges) == 1 and DraftGeomUtils.geomType(extrusion.Edges[0]) == "Line": + extrusion = DraftGeomUtils.vec(extrusion.Edges[0], True) + 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 +928,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): @@ -850,15 +945,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 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 - 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) @@ -1049,45 +1144,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 @@ -1182,6 +1288,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: @@ -1315,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 56a3ce01fe..1c40171a93 100644 --- a/src/Mod/Arch/InitGui.py +++ b/src/Mod/Arch/InitGui.py @@ -59,7 +59,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", diff --git a/src/Mod/Arch/Resources/Arch.qrc b/src/Mod/Arch/Resources/Arch.qrc index 71984781ac..6fc604d818 100644 --- a/src/Mod/Arch/Resources/Arch.qrc +++ b/src/Mod/Arch/Resources/Arch.qrc @@ -38,6 +38,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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Mod/Draft/CMakeLists.txt b/src/Mod/Draft/CMakeLists.txt index 8ba6ce8e71..29ebf83e94 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/DraftGeomUtils.py b/src/Mod/Draft/DraftGeomUtils.py index d1ce906ed2..4940a29e5d 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, @@ -129,7 +130,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/TestDraft.py b/src/Mod/Draft/TestDraft.py index f06b1de2c9..6845cfb726 100644 --- a/src/Mod/Draft/TestDraft.py +++ b/src/Mod/Draft/TestDraft.py @@ -101,12 +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 +# Testing the utils module +from drafttests.test_draftgeomutils import TestDraftGeomUtils as DraftTest04 + # 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 +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 @@ -115,5 +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 diff --git a/src/Mod/Draft/draftgeoutils/edges.py b/src/Mod/Draft/draftgeoutils/edges.py index 67a6a33a2e..7bfda8256a 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): + """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] + 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 692bc71aef..ba45625906 100644 --- a/src/Mod/Draft/draftgeoutils/general.py +++ b/src/Mod/Draft/draftgeoutils/general.py @@ -59,11 +59,17 @@ 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! +def vec(edge, use_orientation = False): + """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.Shape): - return edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) + 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: diff --git a/src/Mod/Draft/draftgeoutils/wires.py b/src/Mod/Draft/draftgeoutils/wires.py index 9ee4a5a1ee..394ce80052 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 as App from draftgeoutils.general import geomType, vec, precision from draftgeoutils.geometry import get_normal @@ -435,4 +436,95 @@ def tessellateProjection(shape, seglen): return Part.makeCompound(newedges) -## @} +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 = App.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 = App.Rotation(App.Vector(1, 0, 0), App.Vector(0, 0, 1), zaxis, "ZYX") + else: + App.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): + """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: + 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 + 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 diff --git a/src/Mod/Draft/drafttests/test_draftgeomutils.py b/src/Mod/Draft/drafttests/test_draftgeomutils.py new file mode 100644 index 0000000000..3cf94f1c38 --- /dev/null +++ b/src/Mod/Draft/drafttests/test_draftgeomutils.py @@ -0,0 +1,142 @@ +# *************************************************************************** +# * 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 * +# * * +# *************************************************************************** +"""Unit test for the DraftGeomUtils module.""" + +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)