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 @@
+
+
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)