* Add BIM workbench to .pre-commit-config.yaml * pre-commit: ignore translations * pre-commit: add additional ignore pattern * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
421 lines
16 KiB
Python
421 lines
16 KiB
Python
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
|
|
# ***************************************************************************
|
|
# * *
|
|
# * Copyright (c) 2020 Yorik van Havre <yorik@uncreated.net> *
|
|
# * *
|
|
# * This file is part of FreeCAD. *
|
|
# * *
|
|
# * FreeCAD is free software: you can redistribute it and/or modify it *
|
|
# * under the terms of the GNU Lesser General Public License as *
|
|
# * published by the Free Software Foundation, either version 2.1 of the *
|
|
# * License, or (at your option) any later version. *
|
|
# * *
|
|
# * 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 *
|
|
# * Lesser General Public License for more details. *
|
|
# * *
|
|
# * You should have received a copy of the GNU Lesser General Public *
|
|
# * License along with FreeCAD. If not, see *
|
|
# * <https://www.gnu.org/licenses/>. *
|
|
# * *
|
|
# ***************************************************************************
|
|
|
|
__title__ = "FreeCAD Arch Truss"
|
|
__author__ = "Yorik van Havre"
|
|
__url__ = "https://www.freecad.org"
|
|
|
|
## @package ArchTruss
|
|
# \ingroup ARCH
|
|
# \brief The Truss object and tools
|
|
#
|
|
# This module provides tools to build Truss objects.
|
|
|
|
import math
|
|
|
|
import FreeCAD
|
|
import ArchComponent
|
|
|
|
if FreeCAD.GuiUp:
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
import FreeCADGui
|
|
from draftutils.translate import translate
|
|
else:
|
|
# \cond
|
|
def translate(ctxt, txt):
|
|
return txt
|
|
|
|
def QT_TRANSLATE_NOOP(ctxt, txt):
|
|
return txt
|
|
|
|
# \endcond
|
|
|
|
rodmodes = (
|
|
"/|/|/|",
|
|
"/\\/\\/\\",
|
|
"/|\\|/|\\",
|
|
)
|
|
|
|
|
|
class Truss(ArchComponent.Component):
|
|
"The truss object"
|
|
|
|
def __init__(self, obj):
|
|
|
|
ArchComponent.Component.__init__(self, obj)
|
|
self.Type = "Truss"
|
|
self.setProperties(obj)
|
|
obj.IfcType = "Beam"
|
|
|
|
def setProperties(self, obj):
|
|
|
|
pl = obj.PropertiesList
|
|
if not "TrussAngle" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyAngle",
|
|
"TrussAngle",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The angle of the truss"),
|
|
locked=True,
|
|
)
|
|
obj.setEditorMode("TrussAngle", 1)
|
|
if not "SlantType" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyEnumeration",
|
|
"SlantType",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The slant type of this truss"),
|
|
locked=True,
|
|
)
|
|
obj.SlantType = ["Simple", "Double"]
|
|
if not "Normal" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyVector",
|
|
"Normal",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The normal direction of this truss"),
|
|
locked=True,
|
|
)
|
|
obj.Normal = FreeCAD.Vector(0, 0, 1)
|
|
if not "HeightStart" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyLength",
|
|
"HeightStart",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The height of the truss at the start position"),
|
|
locked=True,
|
|
)
|
|
obj.HeightStart = 100
|
|
if not "HeightEnd" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyLength",
|
|
"HeightEnd",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The height of the truss at the end position"),
|
|
locked=True,
|
|
)
|
|
obj.HeightEnd = 200
|
|
if not "StrutStartOffset" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyDistance",
|
|
"StrutStartOffset",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "An optional start offset for the top strut"),
|
|
locked=True,
|
|
)
|
|
if not "StrutEndOffset" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyDistance",
|
|
"StrutEndOffset",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "An optional end offset for the top strut"),
|
|
locked=True,
|
|
)
|
|
if not "StrutHeight" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyLength",
|
|
"StrutHeight",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property", "The height of the main top and bottom elements of the truss"
|
|
),
|
|
locked=True,
|
|
)
|
|
obj.StrutHeight = 10
|
|
if not "StrutWidth" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyLength",
|
|
"StrutWidth",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP(
|
|
"App::Property", "The width of the main top and bottom elements of the truss"
|
|
),
|
|
locked=True,
|
|
)
|
|
obj.StrutWidth = 5
|
|
if not "RodType" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyEnumeration",
|
|
"RodType",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The type of the middle element of the truss"),
|
|
locked=True,
|
|
)
|
|
obj.RodType = ["Round", "Square"]
|
|
if not "RodDirection" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyEnumeration",
|
|
"RodDirection",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The direction of the rods"),
|
|
locked=True,
|
|
)
|
|
obj.RodDirection = ["Forward", "Backward"]
|
|
if not "RodSize" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyLength",
|
|
"RodSize",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The diameter or side of the rods"),
|
|
locked=True,
|
|
)
|
|
obj.RodSize = 2
|
|
if not "RodSections" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyInteger",
|
|
"RodSections",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "The number of rod sections"),
|
|
locked=True,
|
|
)
|
|
obj.RodSections = 3
|
|
if not "RodEnd" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyBool",
|
|
"RodEnd",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "If the truss has a rod at its endpoint or not"),
|
|
locked=True,
|
|
)
|
|
if not "RodMode" in pl:
|
|
obj.addProperty(
|
|
"App::PropertyEnumeration",
|
|
"RodMode",
|
|
"Truss",
|
|
QT_TRANSLATE_NOOP("App::Property", "How to draw the rods"),
|
|
locked=True,
|
|
)
|
|
obj.RodMode = rodmodes
|
|
|
|
def onDocumentRestored(self, obj):
|
|
|
|
ArchComponent.Component.onDocumentRestored(self, obj)
|
|
self.setProperties(obj)
|
|
|
|
def loads(self, state):
|
|
|
|
self.Type = "Truss"
|
|
|
|
def onChanged(self, obj, prop):
|
|
|
|
ArchComponent.Component.onChanged(self, obj, prop)
|
|
|
|
def execute(self, obj):
|
|
|
|
if self.clone(obj):
|
|
return
|
|
if not self.ensureBase(obj):
|
|
return
|
|
|
|
import Part
|
|
|
|
pl = obj.Placement
|
|
|
|
# test properties
|
|
if not obj.StrutWidth.Value or not obj.StrutHeight.Value:
|
|
FreeCAD.Console.PrintLog(obj.Label + ": Invalid strut size\n")
|
|
return
|
|
if not obj.HeightStart.Value or not obj.HeightEnd.Value:
|
|
FreeCAD.Console.PrintLog(obj.Label + ": Invalid truss heights\n")
|
|
return
|
|
if not obj.RodSections or not obj.RodSize.Value:
|
|
FreeCAD.Console.PrintLog(obj.Label + ": Invalid rod config\n")
|
|
return
|
|
if not obj.Base:
|
|
FreeCAD.Console.PrintLog(obj.Label + ": No base\n")
|
|
return
|
|
if (not hasattr(obj.Base, "Shape")) or (len(obj.Base.Shape.Vertexes) > 2):
|
|
FreeCAD.Console.PrintLog(obj.Label + ": No base edge\n")
|
|
return
|
|
|
|
# baseline
|
|
v0 = obj.Base.Shape.Vertexes[0].Point
|
|
v1 = obj.Base.Shape.Vertexes[-1].Point
|
|
|
|
if obj.SlantType == "Simple":
|
|
topstrut, bottomstrut, rods, angle = self.makeTruss(obj, v0, v1)
|
|
else:
|
|
v3 = v0.add((v1.sub(v0)).multiply(0.5))
|
|
topstrut1, bottomstrut1, rods1, angle = self.makeTruss(obj, v0, v3)
|
|
topstrut2, bottomstrut2, rods2, angle = self.makeTruss(obj, v1, v3)
|
|
topstrut = topstrut1.fuse(topstrut2)
|
|
topstrut = topstrut.removeSplitter()
|
|
bottomstrut = bottomstrut1.fuse(bottomstrut2)
|
|
bottomstrut = bottomstrut.removeSplitter()
|
|
if obj.RodEnd:
|
|
rods2 = rods2[:-1]
|
|
rods = rods1 + rods2
|
|
|
|
# mount shape
|
|
shape = Part.makeCompound([topstrut, bottomstrut] + rods)
|
|
shape = self.processSubShapes(obj, shape, pl)
|
|
self.applyShape(obj, shape, pl)
|
|
obj.TrussAngle = angle
|
|
|
|
def makeTruss(self, obj, v0, v1):
|
|
|
|
import Part
|
|
|
|
# get normal direction
|
|
normal = obj.Normal
|
|
if not normal.Length:
|
|
normal = FreeCAD.Vector(0, 0, 1)
|
|
|
|
# create base profile
|
|
maindir = v1.sub(v0)
|
|
sidedir = normal.cross(maindir)
|
|
if not sidedir.Length:
|
|
FreeCAD.Console.PrintLog(obj.Label + ": normal and base are parallel\n")
|
|
return
|
|
sidedir.normalize()
|
|
p0 = v0.add(FreeCAD.Vector(sidedir).negative().multiply(obj.StrutWidth.Value / 2))
|
|
p1 = p0.add(FreeCAD.Vector(sidedir).multiply(obj.StrutWidth.Value / 2).multiply(2))
|
|
p2 = p1.add(FreeCAD.Vector(normal).multiply(obj.StrutHeight))
|
|
p3 = p0.add(FreeCAD.Vector(normal).multiply(obj.StrutHeight))
|
|
trussprofile = Part.Face(Part.makePolygon([p0, p1, p2, p3, p0]))
|
|
|
|
# create bottom strut
|
|
bottomstrut = trussprofile.extrude(maindir)
|
|
|
|
# create top strut
|
|
v2 = v0.add(FreeCAD.Vector(normal).multiply(obj.HeightStart.Value))
|
|
v3 = v1.add(FreeCAD.Vector(normal).multiply(obj.HeightEnd.Value))
|
|
topdir = v3.sub(v2)
|
|
if obj.StrutStartOffset.Value:
|
|
v2f = v2.add((v2.sub(v3)).normalize().multiply(obj.StrutStartOffset.Value))
|
|
else:
|
|
v2f = v2
|
|
if obj.StrutEndOffset.Value:
|
|
v3f = v3.add((v3.sub(v2)).normalize().multiply(obj.StrutEndOffset.Value))
|
|
else:
|
|
v3f = v3
|
|
offtopdir = v3f.sub(v2f)
|
|
topstrut = trussprofile.extrude(offtopdir)
|
|
topstrut.translate(v2f.sub(v0))
|
|
topstrut.translate(FreeCAD.Vector(normal).multiply(-obj.StrutHeight.Value))
|
|
angle = math.degrees(topdir.getAngle(maindir))
|
|
|
|
# create rod profile on the XY plane
|
|
if obj.RodType == "Round":
|
|
rodprofile = Part.Face(Part.Wire([Part.makeCircle(obj.RodSize / 2)]))
|
|
else:
|
|
rodprofile = Part.Face(
|
|
Part.makePlane(
|
|
obj.RodSize, obj.RodSize, FreeCAD.Vector(-obj.RodSize / 2, -obj.RodSize / 2, 0)
|
|
)
|
|
)
|
|
|
|
# create rods
|
|
rods = []
|
|
bottomrodstart = v0.add(FreeCAD.Vector(normal).multiply(obj.StrutHeight.Value / 2))
|
|
toprodstart = v2.add(FreeCAD.Vector(normal).multiply(obj.StrutHeight.Value / 2).negative())
|
|
bottomrodvec = FreeCAD.Vector(maindir).multiply(1 / obj.RodSections)
|
|
toprodvec = FreeCAD.Vector(topdir).multiply(1 / obj.RodSections)
|
|
bottomrodpos = [bottomrodstart]
|
|
toprodpos = [toprodstart]
|
|
|
|
if obj.RodMode == rodmodes[0]:
|
|
# /|/|/|
|
|
for i in range(1, obj.RodSections + 1):
|
|
if (i > 1) or (obj.StrutStartOffset.Value >= 0):
|
|
# do not add first vert rod if negative offset
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-1], toprodpos[-1]))
|
|
bottomrodpos.append(bottomrodpos[-1].add(bottomrodvec))
|
|
toprodpos.append(toprodpos[-1].add(toprodvec))
|
|
if obj.RodDirection == "Forward":
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-2], toprodpos[-1]))
|
|
else:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-1], toprodpos[-2]))
|
|
|
|
elif obj.RodMode == rodmodes[1]:
|
|
# /\/\/\
|
|
fw = True
|
|
for i in range(1, obj.RodSections + 1):
|
|
bottomrodpos.append(bottomrodpos[-1].add(bottomrodvec))
|
|
toprodpos.append(toprodpos[-1].add(toprodvec))
|
|
if obj.RodDirection == "Forward":
|
|
if fw:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-2], toprodpos[-1]))
|
|
else:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-1], toprodpos[-2]))
|
|
else:
|
|
if fw:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-1], toprodpos[-2]))
|
|
else:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-2], toprodpos[-1]))
|
|
fw = not fw
|
|
|
|
elif obj.RodMode == rodmodes[2]:
|
|
# /|\|/|\
|
|
fw = True
|
|
for i in range(1, obj.RodSections + 1):
|
|
if (i > 1) or (obj.StrutStartOffset.Value >= 0):
|
|
# do not add first vert rod if negative offset
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-1], toprodpos[-1]))
|
|
bottomrodpos.append(bottomrodpos[-1].add(bottomrodvec))
|
|
toprodpos.append(toprodpos[-1].add(toprodvec))
|
|
if obj.RodDirection == "Forward":
|
|
if fw:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-2], toprodpos[-1]))
|
|
else:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-1], toprodpos[-2]))
|
|
else:
|
|
if fw:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-1], toprodpos[-2]))
|
|
else:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-2], toprodpos[-1]))
|
|
fw = not fw
|
|
|
|
# add end rod
|
|
if obj.RodEnd:
|
|
rods.append(self.makeRod(rodprofile, bottomrodpos[-1], toprodpos[-1]))
|
|
|
|
# trim rods
|
|
rods = [rod.cut(topstrut).cut(bottomstrut) for rod in rods]
|
|
|
|
return topstrut, bottomstrut, rods, angle
|
|
|
|
def makeRod(self, profile, p1, p2):
|
|
"""makes a rod by extruding profile between p1 and p2"""
|
|
|
|
rot = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), p2.sub(p1))
|
|
rod = profile.copy().translate(p1)
|
|
rod = rod.rotate(p1, rot.Axis, math.degrees(rot.Angle))
|
|
rod = rod.extrude(p2.sub(p1))
|
|
return rod
|
|
|
|
|
|
class ViewProviderTruss(ArchComponent.ViewProviderComponent):
|
|
"A View Provider for the Truss object"
|
|
|
|
def __init__(self, vobj):
|
|
|
|
ArchComponent.ViewProviderComponent.__init__(self, vobj)
|
|
|
|
def getIcon(self):
|
|
|
|
import Arch_rc
|
|
|
|
return ":/icons/Arch_Truss_Tree.svg"
|