955 lines
41 KiB
Python
955 lines
41 KiB
Python
#***************************************************************************
|
|
#* Copyright (c) 2012 Yorik van Havre <yorik@uncreated.net> *
|
|
#* *
|
|
#* 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. *
|
|
#* *
|
|
#* This program 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 this program; if not, write to the Free Software *
|
|
#* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
|
|
#* USA *
|
|
#* *
|
|
#***************************************************************************
|
|
|
|
import math
|
|
|
|
import ArchComponent
|
|
import DraftGeomUtils
|
|
import DraftVecUtils
|
|
import FreeCAD
|
|
import Part
|
|
|
|
from FreeCAD import Vector
|
|
|
|
if FreeCAD.GuiUp:
|
|
import FreeCADGui
|
|
from PySide import QtCore, QtGui
|
|
from draftutils.translate import translate
|
|
from PySide.QtCore import QT_TRANSLATE_NOOP
|
|
else:
|
|
# \cond
|
|
def translate(ctxt, txt):
|
|
return txt
|
|
def QT_TRANSLATE_NOOP(ctxt, txt):
|
|
return txt
|
|
# \endcond
|
|
|
|
## @package ArchRoof
|
|
# \ingroup ARCH
|
|
# \brief The Roof object and tools
|
|
#
|
|
# This module provides tools to build Roof objects.
|
|
# Roofs are built from a closed contour and a series of
|
|
# slopes.
|
|
|
|
__title__ = "FreeCAD Roof"
|
|
__author__ = "Yorik van Havre", "Jonathan Wiedemann"
|
|
__url__ = "https://www.freecad.org"
|
|
|
|
|
|
def adjust_list_len (lst, newLn, val):
|
|
'''Returns a clone of lst with length newLn, val is appended if required'''
|
|
ln = len(lst)
|
|
if ln > newLn:
|
|
return lst[0:newLn]
|
|
else:
|
|
return lst[:] + ([val] * (newLn - ln))
|
|
|
|
|
|
def find_inters (edge1, edge2, infinite1=True, infinite2=True):
|
|
'''Future wrapper for DraftGeomUtils.findIntersection. The function now
|
|
contains a modified copy of getLineIntersections from that function.
|
|
'''
|
|
def getLineIntersections(pt1, pt2, pt3, pt4, infinite1, infinite2):
|
|
# if pt1:
|
|
## first check if we don't already have coincident endpoints ######## we do not want that here ########
|
|
# if pt1 in [pt3, pt4]:
|
|
# return [pt1]
|
|
# elif (pt2 in [pt3, pt4]):
|
|
# return [pt2]
|
|
norm1 = pt2.sub(pt1).cross(pt3.sub(pt1))
|
|
norm2 = pt2.sub(pt4).cross(pt3.sub(pt4))
|
|
|
|
if not DraftVecUtils.isNull(norm1):
|
|
try:
|
|
norm1.normalize()
|
|
except Part.OCCError:
|
|
return []
|
|
|
|
if not DraftVecUtils.isNull(norm2):
|
|
try:
|
|
norm2.normalize()
|
|
except Part.OCCError:
|
|
return []
|
|
|
|
if DraftVecUtils.isNull(norm1.cross(norm2)):
|
|
vec1 = pt2.sub(pt1)
|
|
vec2 = pt4.sub(pt3)
|
|
if DraftVecUtils.isNull(vec1) or DraftVecUtils.isNull(vec2):
|
|
return [] # One of the lines has zero-length
|
|
try:
|
|
vec1.normalize()
|
|
vec2.normalize()
|
|
except Part.OCCError:
|
|
return []
|
|
norm3 = vec1.cross(vec2)
|
|
denom = norm3.x + norm3.y + norm3.z
|
|
if not DraftVecUtils.isNull(norm3) and denom != 0:
|
|
k = ((pt3.z - pt1.z) * (vec2.x - vec2.y)
|
|
+ (pt3.y - pt1.y) * (vec2.z - vec2.x)
|
|
+ (pt3.x - pt1.x) * (vec2.y - vec2.z)) / denom
|
|
vec1.scale(k, k, k)
|
|
intp = pt1.add(vec1)
|
|
|
|
if infinite1 is False and not isPtOnEdge(intp, edge1):
|
|
return []
|
|
|
|
if infinite2 is False and not isPtOnEdge(intp, edge2):
|
|
return []
|
|
|
|
return [intp]
|
|
else:
|
|
return [] # Lines have same direction
|
|
else:
|
|
return [] # Lines aren't on same plane
|
|
|
|
pt1, pt2, pt3, pt4 = [edge1.Vertexes[0].Point,
|
|
edge1.Vertexes[1].Point,
|
|
edge2.Vertexes[0].Point,
|
|
edge2.Vertexes[1].Point]
|
|
|
|
return getLineIntersections(pt1, pt2, pt3, pt4, infinite1, infinite2)
|
|
|
|
|
|
def face_from_points(ptLst):
|
|
ptLst.append(ptLst[0])
|
|
# Use DraftVecUtils.removeDouble after append as it does not compare the first and last vector:
|
|
ptLst = DraftVecUtils.removeDoubles(ptLst)
|
|
ln = len(ptLst)
|
|
if ln < 4: # at least 4 points are required for 3 edges
|
|
return None
|
|
edgeLst = []
|
|
for i in range(ln - 1):
|
|
edge = Part.makeLine(ptLst[i], ptLst[i + 1])
|
|
edgeLst.append(edge)
|
|
wire = Part.Wire(edgeLst)
|
|
return Part.Face(wire)
|
|
|
|
|
|
|
|
class _Roof(ArchComponent.Component):
|
|
'''The Roof object'''
|
|
def __init__(self, obj):
|
|
ArchComponent.Component.__init__(self, obj)
|
|
self.setProperties(obj)
|
|
obj.IfcType = "Roof"
|
|
obj.Proxy = self
|
|
|
|
def setProperties(self, obj):
|
|
pl = obj.PropertiesList
|
|
if not "Angles" in pl:
|
|
obj.addProperty("App::PropertyFloatList",
|
|
"Angles",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The list of angles of the roof segments"))
|
|
if not "Runs" in pl:
|
|
obj.addProperty("App::PropertyFloatList",
|
|
"Runs",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The list of horizontal length projections of the roof segments"))
|
|
if not "IdRel" in pl:
|
|
obj.addProperty("App::PropertyIntegerList",
|
|
"IdRel",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The list of IDs of the relative profiles of the roof segments"))
|
|
if not "Thickness" in pl:
|
|
obj.addProperty("App::PropertyFloatList",
|
|
"Thickness",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The list of thicknesses of the roof segments"))
|
|
if not "Overhang" in pl:
|
|
obj.addProperty("App::PropertyFloatList",
|
|
"Overhang",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The list of overhangs of the roof segments"))
|
|
if not "Heights" in pl:
|
|
obj.addProperty("App::PropertyFloatList",
|
|
"Heights",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The list of calculated heights of the roof segments"))
|
|
if not "Face" in pl:
|
|
obj.addProperty("App::PropertyInteger",
|
|
"Face",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The face number of the base object used to build the roof"))
|
|
if not "RidgeLength" in pl:
|
|
obj.addProperty("App::PropertyLength",
|
|
"RidgeLength",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The total length of the ridges and hips of the roof"))
|
|
obj.setEditorMode("RidgeLength",1)
|
|
if not "BorderLength" in pl:
|
|
obj.addProperty("App::PropertyLength",
|
|
"BorderLength",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "The total length of the borders of the roof"))
|
|
obj.setEditorMode("BorderLength",1)
|
|
if not "Flip" in pl:
|
|
obj.addProperty("App::PropertyBool",
|
|
"Flip",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "Specifies if the direction of the roof should be flipped"))
|
|
if not "Subvolume" in pl:
|
|
obj.addProperty("App::PropertyLink",
|
|
"Subvolume",
|
|
"Roof",
|
|
QT_TRANSLATE_NOOP("App::Property", "An optional object that defines a volume to be subtracted from walls. If field is set - it has a priority over auto-generated subvolume"))
|
|
self.Type = "Roof"
|
|
|
|
def onDocumentRestored(self, obj):
|
|
ArchComponent.Component.onDocumentRestored(self, obj)
|
|
self.setProperties(obj)
|
|
|
|
def flipEdges(self, edges):
|
|
edges.reverse()
|
|
newEdges = []
|
|
for edge in edges:
|
|
NewEdge = DraftGeomUtils.edg(edge.Vertexes[1].Point, edge.Vertexes[0].Point)
|
|
newEdges.append(NewEdge)
|
|
return newEdges
|
|
|
|
def calcHeight(self, id):
|
|
'''Get the height from run and angle of the given roof profile'''
|
|
htRel = self.profilsDico[id]["run"] * (math.tan(math.radians(self.profilsDico[id]["angle"])))
|
|
return htRel
|
|
|
|
def calcRun(self, id):
|
|
'''Get the run from height and angle of the given roof profile'''
|
|
runRel = self.profilsDico[id]["height"] / (math.tan(math.radians(self.profilsDico[id]["angle"])))
|
|
return runRel
|
|
|
|
def calcAngle(self, id):
|
|
'''Get the angle from height and run of the given roof profile'''
|
|
ang = math.degrees(math.atan(self.profilsDico[id]["height"] / self.profilsDico[id]["run"]))
|
|
return ang
|
|
|
|
def getPerpendicular(self, vec, rotEdge, l):
|
|
'''Get the perpendicular vec of given edge on xy plane'''
|
|
norm = Vector(0.0, 0.0, 1.0)
|
|
if hasattr(self, "normal"):
|
|
if self.normal:
|
|
norm = self.normal
|
|
per = vec.cross(norm)
|
|
if -180.0 <= rotEdge < -90.0:
|
|
per[0] = -abs(per[0])
|
|
per[1] = -abs(per[1])
|
|
elif -90.0 <= rotEdge <= 0.0:
|
|
per[0] = -abs(per[0])
|
|
per[1] = abs(per[1])
|
|
elif 0.0 < rotEdge <= 90.0:
|
|
per[0] = abs(per[0])
|
|
per[1] = abs(per[1])
|
|
elif 90.0 < rotEdge <= 180.0:
|
|
per[0] = abs(per[0])
|
|
per[1] = -abs(per[1])
|
|
else:
|
|
print("Unknown Angle")
|
|
per[2] = abs(per[2])
|
|
per.normalize()
|
|
per = per.multiply(l)
|
|
return per
|
|
|
|
def makeRoofProfilsDic(self, id, angle, run, idrel, overhang, thickness):
|
|
profilDico = {}
|
|
profilDico["id"] = id
|
|
if angle == 90.0:
|
|
profilDico["name"] = "Gable" + str(id)
|
|
profilDico["run"] = 0.0
|
|
else:
|
|
profilDico["name"] = "Sloped" + str(id)
|
|
profilDico["run"] = run
|
|
profilDico["angle"] = angle
|
|
profilDico["idrel"] = idrel
|
|
profilDico["overhang"] = overhang
|
|
profilDico["thickness"] = thickness
|
|
profilDico["height"] = None
|
|
profilDico["points"] = []
|
|
self.profilsDico.append(profilDico)
|
|
|
|
def calcEdgeGeometry(self, i, edge):
|
|
profilCurr = self.profilsDico[i]
|
|
profilCurr["edge"] = edge
|
|
vec = edge.Vertexes[1].Point.sub(edge.Vertexes[0].Point)
|
|
profilCurr["vec"] = vec
|
|
rot = math.degrees(DraftVecUtils.angle(vec))
|
|
profilCurr["rot"] = rot
|
|
|
|
def helperCalcApex(self, profilCurr, profilOpposite):
|
|
ptCurr = profilCurr["edge"].Vertexes[0].Point
|
|
ptOpposite = profilOpposite["edge"].Vertexes[0].Point
|
|
dis = ptCurr.distanceToLine(ptOpposite, profilOpposite["vec"])
|
|
if dis < profilCurr["run"] + profilOpposite["run"]: # sum of runs is larger than dis
|
|
angCurr = profilCurr["angle"]
|
|
angOpposite = profilOpposite["angle"]
|
|
return dis / (math.tan(math.radians(angCurr)) / math.tan(math.radians(angOpposite)) + 1.0)
|
|
return profilCurr["run"]
|
|
|
|
def calcApex(self, i, numEdges):
|
|
'''Recalculate the run and height if there is an opposite roof segment
|
|
with a parallel edge, and if the sum of the runs of the segments is
|
|
larger than the distance between the edges of the segments.
|
|
'''
|
|
profilCurr = self.findProfil(i)
|
|
if 0 <= profilCurr["idrel"] < numEdges: # no apex calculation if idrel is used
|
|
return
|
|
if not 0.0 < profilCurr["angle"] < 90.0:
|
|
return
|
|
profilNext2 = self.findProfil(i + 2)
|
|
profilBack2 = self.findProfil(i - 2)
|
|
vecCurr = profilCurr["vec"]
|
|
vecNext2 = profilNext2["vec"]
|
|
vecBack2 = profilBack2["vec"]
|
|
runs = []
|
|
if ((not 0 <= profilNext2["idrel"] < numEdges)
|
|
and 0.0 < profilNext2["angle"] < 90.0
|
|
and math.isclose(vecCurr.getAngle(vecNext2), math.pi, abs_tol=1e-7)):
|
|
runs.append((self.helperCalcApex(profilCurr, profilNext2)))
|
|
if ((not 0 <= profilBack2["idrel"] < numEdges)
|
|
and 0.0 < profilBack2["angle"] < 90.0
|
|
and math.isclose(vecCurr.getAngle(vecBack2), math.pi, abs_tol=1e-7)):
|
|
runs.append((self.helperCalcApex(profilCurr, profilBack2)))
|
|
runs.sort()
|
|
if len(runs) != 0 and runs[0] != profilCurr["run"]:
|
|
profilCurr["run"] = runs[0]
|
|
hgt = self.calcHeight(i)
|
|
profilCurr["height"] = hgt
|
|
|
|
def calcMissingData(self, i, numEdges):
|
|
profilCurr = self.profilsDico[i]
|
|
ang = profilCurr["angle"]
|
|
run = profilCurr["run"]
|
|
rel = profilCurr["idrel"]
|
|
if i != rel and 0 <= rel < numEdges:
|
|
profilRel = self.profilsDico[rel]
|
|
# do not use data from the relative profile if it in turn references a relative profile:
|
|
if (0 <= profilRel["idrel"] < numEdges # idrel of profilRel points to a profile
|
|
and rel != profilRel["idrel"] # profilRel does not reference itself
|
|
and (profilRel["angle"] == 0.0 or profilRel["run"] == 0.0)): # run or angle of profilRel is zero
|
|
hgt = self.calcHeight(i)
|
|
profilCurr["height"] = hgt
|
|
elif ang == 0.0 and run == 0.0:
|
|
profilCurr["run"] = profilRel["run"]
|
|
profilCurr["angle"] = profilRel["angle"]
|
|
profilCurr["height"] = self.calcHeight(i)
|
|
elif run == 0.0:
|
|
if ang == 90.0:
|
|
htRel = self.calcHeight(rel)
|
|
profilCurr["height"] = htRel
|
|
else :
|
|
htRel = self.calcHeight(rel)
|
|
profilCurr["height"] = htRel
|
|
run = self.calcRun(i)
|
|
profilCurr["run"] = run
|
|
elif ang == 0.0:
|
|
htRel = self.calcHeight(rel)
|
|
profilCurr["height"] = htRel
|
|
ang = self.calcAngle(i)
|
|
profilCurr["angle"] = ang
|
|
else :
|
|
hgt = self.calcHeight(i)
|
|
profilCurr["height"] = hgt
|
|
else:
|
|
hgt = self.calcHeight(i)
|
|
profilCurr["height"] = hgt
|
|
|
|
def calcDraftEdges(self, i):
|
|
profilCurr = self.profilsDico[i]
|
|
edge = profilCurr["edge"]
|
|
vec = profilCurr["vec"]
|
|
rot = profilCurr["rot"]
|
|
ang = profilCurr["angle"]
|
|
run = profilCurr["run"]
|
|
if ang != 90 and run == 0.0:
|
|
overhang = 0.0
|
|
else:
|
|
overhang = profilCurr["overhang"]
|
|
per = self.getPerpendicular(vec, rot, overhang).negative()
|
|
eaveDraft = DraftGeomUtils.offset(edge, per)
|
|
profilCurr["eaveDraft"] = eaveDraft
|
|
per = self.getPerpendicular(vec, rot, run)
|
|
ridge = DraftGeomUtils.offset(edge, per)
|
|
profilCurr["ridge"] = ridge
|
|
|
|
def calcEave(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
ptInterEaves1Lst = find_inters(profilCurr["eaveDraft"], self.findProfil(i - 1)["eaveDraft"])
|
|
if ptInterEaves1Lst:
|
|
ptInterEaves1 = ptInterEaves1Lst[0]
|
|
else:
|
|
ptInterEaves1 = profilCurr["eaveDraft"].Vertexes[0].Point
|
|
ptInterEaves2Lst = find_inters(profilCurr["eaveDraft"], self.findProfil(i + 1)["eaveDraft"])
|
|
if ptInterEaves2Lst:
|
|
ptInterEaves2 = ptInterEaves2Lst[0]
|
|
else:
|
|
ptInterEaves2 = profilCurr["eaveDraft"].Vertexes[1].Point
|
|
profilCurr["eavePtLst"] = [ptInterEaves1, ptInterEaves2] # list of points instead of edge as points can be identical
|
|
|
|
def findProfil(self, i):
|
|
if 0 <= i < len(self.profilsDico):
|
|
profil = self.profilsDico[i]
|
|
else:
|
|
i = abs(abs(i) - len(self.profilsDico))
|
|
profil = self.profilsDico[i]
|
|
return profil
|
|
|
|
def helperGable(self, profilCurr, profilOther, isBack):
|
|
if isBack:
|
|
i = 0
|
|
else:
|
|
i = 1
|
|
ptIntLst = find_inters(profilCurr["ridge"], profilOther["eaveDraft"])
|
|
if ptIntLst: # the edges of the roof segments are not parallel
|
|
ptProjLst = [ptIntLst[0]]
|
|
else: # the edges of the roof segments are parallel
|
|
ptProjLst = [profilCurr["ridge"].Vertexes[i].Point]
|
|
ptProjLst = ptProjLst + [profilCurr["eavePtLst"][i]]
|
|
if not isBack:
|
|
ptProjLst.reverse()
|
|
for ptProj in ptProjLst:
|
|
self.ptsPaneProject.append(ptProj)
|
|
|
|
def backGable(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
profilBack = self.findProfil(i - 1)
|
|
self.helperGable(profilCurr, profilBack, isBack = True)
|
|
|
|
def nextGable(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
profilNext = self.findProfil(i + 1)
|
|
self.helperGable(profilCurr, profilNext, isBack = False)
|
|
|
|
def helperSloped(self, profilCurr, profilOther, ridgeCurr, ridgeOther, isBack, otherIsLower=False):
|
|
if isBack:
|
|
i = 0
|
|
else:
|
|
i = 1
|
|
ptIntLst = find_inters(ridgeCurr, ridgeOther)
|
|
if ptIntLst: # the edges of the roof segments are not parallel
|
|
ptInt = ptIntLst[0]
|
|
if otherIsLower:
|
|
ptRidgeLst = find_inters(profilCurr["ridge"], profilOther["ridge"])
|
|
ptProjLst = [ptRidgeLst[0], ptInt]
|
|
else:
|
|
ptProjLst = [ptInt]
|
|
hip = DraftGeomUtils.edg(ptInt, profilCurr["edge"].Vertexes[i].Point)
|
|
ptEaveCurrLst = find_inters(hip, profilCurr["eaveDraft"])
|
|
ptEaveOtherLst = find_inters(hip, profilOther["eaveDraft"])
|
|
if ptEaveCurrLst and ptEaveOtherLst: # both roof segments are sloped
|
|
lenToEaveCurr = ptEaveCurrLst[0].sub(ptInt).Length
|
|
lenToEaveOther = ptEaveOtherLst[0].sub(ptInt).Length
|
|
if lenToEaveCurr < lenToEaveOther:
|
|
ptProjLst = ptProjLst + [ptEaveCurrLst[0]]
|
|
else:
|
|
ptProjLst = ptProjLst + [ptEaveOtherLst[0],
|
|
profilCurr["eavePtLst"][i]]
|
|
elif ptEaveCurrLst: # current angle is 0
|
|
ptProjLst = ptProjLst + [ptEaveCurrLst[0]]
|
|
elif ptEaveOtherLst: # other angle is 0
|
|
ptProjLst = ptProjLst + [ptEaveOtherLst[0],
|
|
profilCurr["eavePtLst"][i]]
|
|
else:
|
|
print("Error determining outline")
|
|
else: # the edges of the roof segments are parallel
|
|
ptProjLst = [profilCurr["ridge"].Vertexes[i].Point,
|
|
profilCurr["eavePtLst"][i]]
|
|
if not isBack:
|
|
ptProjLst.reverse()
|
|
for ptProj in ptProjLst:
|
|
self.ptsPaneProject.append(ptProj)
|
|
|
|
def backSameHeight(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
profilBack = self.findProfil(i - 1)
|
|
self.helperSloped(profilCurr,
|
|
profilBack,
|
|
profilCurr["ridge"],
|
|
profilBack["ridge"],
|
|
isBack = True)
|
|
|
|
def nextSameHeight(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
profilNext = self.findProfil(i + 1)
|
|
self.helperSloped(profilCurr,
|
|
profilNext,
|
|
profilCurr["ridge"],
|
|
profilNext["ridge"],
|
|
isBack = False)
|
|
|
|
def backHigher(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
profilBack = self.findProfil(i - 1)
|
|
dec = profilCurr["height"] / math.tan(math.radians(profilBack["angle"]))
|
|
per = self.getPerpendicular(profilBack["vec"], profilBack["rot"], dec)
|
|
edgeRidgeOnPane = DraftGeomUtils.offset(profilBack["edge"], per)
|
|
self.helperSloped(profilCurr,
|
|
profilBack,
|
|
profilCurr["ridge"],
|
|
edgeRidgeOnPane,
|
|
isBack = True)
|
|
|
|
def nextHigher(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
profilNext = self.findProfil(i + 1)
|
|
dec = profilCurr["height"] / math.tan(math.radians(profilNext["angle"]))
|
|
per = self.getPerpendicular(profilNext["vec"], profilNext["rot"], dec)
|
|
edgeRidgeOnPane = DraftGeomUtils.offset(profilNext["edge"], per)
|
|
self.helperSloped(profilCurr,
|
|
profilNext,
|
|
profilCurr["ridge"],
|
|
edgeRidgeOnPane,
|
|
isBack = False)
|
|
|
|
def backLower(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
profilBack = self.findProfil(i - 1)
|
|
dec = profilBack["height"] / math.tan(math.radians(profilCurr["angle"]))
|
|
per = self.getPerpendicular(profilCurr["vec"], profilCurr["rot"], dec)
|
|
edgeRidgeOnPane = DraftGeomUtils.offset(profilCurr["edge"], per)
|
|
self.helperSloped(profilCurr,
|
|
profilBack,
|
|
edgeRidgeOnPane,
|
|
profilBack["ridge"],
|
|
isBack = True,
|
|
otherIsLower = True)
|
|
|
|
def nextLower(self, i):
|
|
profilCurr = self.findProfil(i)
|
|
profilNext = self.findProfil(i + 1)
|
|
dec = profilNext["height"] / math.tan(math.radians(profilCurr["angle"]))
|
|
per = self.getPerpendicular(profilCurr["vec"], profilCurr["rot"], dec)
|
|
edgeRidgeOnPane = DraftGeomUtils.offset(profilCurr["edge"], per)
|
|
self.helperSloped(profilCurr,
|
|
profilNext,
|
|
edgeRidgeOnPane,
|
|
profilNext["ridge"],
|
|
isBack = False,
|
|
otherIsLower = True)
|
|
|
|
def getRoofPaneProject(self, i):
|
|
self.ptsPaneProject = []
|
|
profilCurr = self.findProfil(i)
|
|
profilBack = self.findProfil(i - 1)
|
|
profilNext = self.findProfil(i + 1)
|
|
if profilCurr["angle"] == 90.0 or profilCurr["run"] == 0.0:
|
|
self.ptsPaneProject = []
|
|
else:
|
|
if profilBack["angle"] == 90.0 or profilBack["run"] == 0.0:
|
|
self.backGable(i)
|
|
elif profilBack["height"] == profilCurr["height"]:
|
|
self.backSameHeight(i)
|
|
elif profilBack["height"] < profilCurr["height"]:
|
|
self.backLower(i)
|
|
elif profilBack["height"] > profilCurr["height"]:
|
|
self.backHigher(i)
|
|
else:
|
|
print("Arch Roof: Case not implemented")
|
|
|
|
if profilNext["angle"] == 90.0 or profilNext["run"] == 0.0:
|
|
self.nextGable(i)
|
|
elif profilNext["height"] == profilCurr["height"]:
|
|
self.nextSameHeight(i)
|
|
elif profilNext["height"] < profilCurr["height"]:
|
|
self.nextLower(i)
|
|
elif profilNext["height"] > profilCurr["height"]:
|
|
self.nextHigher(i)
|
|
else:
|
|
print("Arch Roof: Case not implemented")
|
|
|
|
profilCurr["points"] = self.ptsPaneProject
|
|
|
|
def createProfilShape (self, points, midpoint, rot, vec, run, diag, sol):
|
|
lp = len(points)
|
|
points.append(points[0])
|
|
edgesWire = []
|
|
for i in range(lp):
|
|
edge = Part.makeLine(points[i],points[i + 1])
|
|
edgesWire.append(edge)
|
|
profil = Part.Wire(edgesWire)
|
|
profil.translate(midpoint)
|
|
profil.rotate(midpoint, Vector(0.0, 0.0, 1.0), 90.0 - rot)
|
|
per = self.getPerpendicular(vec, rot, run)
|
|
profil.rotate(midpoint, per, 90.0)
|
|
vecT = vec.normalize()
|
|
vecT.multiply(diag)
|
|
profil.translate(vecT)
|
|
vecE = vecT.multiply(-2.0)
|
|
profilFace = Part.Face(profil)
|
|
profilShp = profilFace.extrude(vecE)
|
|
profilShp = sol.common(profilShp)
|
|
#shapesList.append(profilShp)
|
|
return profilShp
|
|
|
|
def execute(self, obj):
|
|
|
|
if self.clone(obj):
|
|
return
|
|
|
|
pl = obj.Placement
|
|
#self.baseface = None
|
|
self.flip = False
|
|
if hasattr(obj, "Flip"):
|
|
if obj.Flip:
|
|
self.flip = True
|
|
base = None
|
|
baseWire = None
|
|
if obj.Base:
|
|
if hasattr(obj.Base, "Shape"):
|
|
if obj.Base.Shape.Solids:
|
|
base = obj.Base.Shape
|
|
#pl = obj.Base.Placement
|
|
else:
|
|
if (obj.Base.Shape.Faces and obj.Face):
|
|
baseWire = obj.Base.Shape.Faces[obj.Face-1].Wires[0]
|
|
elif obj.Base.Shape.Wires:
|
|
baseWire = obj.Base.Shape.Wires[0]
|
|
if baseWire:
|
|
if baseWire.isClosed():
|
|
self.profilsDico = []
|
|
self.shps = []
|
|
self.subVolShps = []
|
|
heights = []
|
|
edges = Part.__sortEdges__(baseWire.Edges)
|
|
if self.flip:
|
|
edges = self.flipEdges(edges)
|
|
|
|
ln = len(edges)
|
|
|
|
obj.Angles = adjust_list_len(obj.Angles, ln, obj.Angles[0])
|
|
obj.Runs = adjust_list_len(obj.Runs, ln, obj.Runs[0])
|
|
obj.IdRel = adjust_list_len(obj.IdRel, ln, obj.IdRel[0])
|
|
obj.Thickness = adjust_list_len(obj.Thickness, ln, obj.Thickness[0])
|
|
obj.Overhang = adjust_list_len(obj.Overhang, ln, obj.Overhang[0])
|
|
|
|
for i in range(ln):
|
|
self.makeRoofProfilsDic(i, obj.Angles[i], obj.Runs[i], obj.IdRel[i], obj.Overhang[i], obj.Thickness[i])
|
|
for i in range(ln):
|
|
self.calcEdgeGeometry(i, edges[i])
|
|
for i in range(ln):
|
|
self.calcApex(i, ln) # after calcEdgeGeometry as it uses vec data
|
|
for i in range(ln):
|
|
self.calcMissingData(i, ln) # after calcApex so it can use recalculated heights
|
|
for i in range(ln):
|
|
self.calcDraftEdges(i)
|
|
for i in range(ln):
|
|
self.calcEave(i)
|
|
for profil in self.profilsDico:
|
|
heights.append(profil["height"])
|
|
obj.Heights = heights
|
|
for i in range(ln):
|
|
self.getRoofPaneProject(i)
|
|
profilCurr = self.profilsDico[i]
|
|
ptsPaneProject = profilCurr["points"]
|
|
if len(ptsPaneProject) == 0:
|
|
continue
|
|
face = face_from_points(ptsPaneProject)
|
|
if face:
|
|
diag = face.BoundBox.DiagonalLength
|
|
midpoint = DraftGeomUtils.findMidpoint(profilCurr["edge"])
|
|
thicknessV = profilCurr["thickness"] / (math.cos(math.radians(profilCurr["angle"])))
|
|
overhangV = profilCurr["overhang"] * math.tan(math.radians(profilCurr["angle"]))
|
|
sol = face.extrude(Vector(0.0, 0.0, profilCurr["height"] + 1000000.0))
|
|
sol.translate(Vector(0.0, 0.0, -2.0 * overhangV))
|
|
|
|
## baseVolume shape
|
|
ptsPaneProfil = [Vector(-profilCurr["overhang"], -overhangV, 0.0),
|
|
Vector(profilCurr["run"], profilCurr["height"], 0.0),
|
|
Vector(profilCurr["run"], profilCurr["height"] + thicknessV, 0.0),
|
|
Vector(-profilCurr["overhang"], -overhangV + thicknessV, 0.0)]
|
|
self.shps.append(self.createProfilShape(ptsPaneProfil,
|
|
midpoint,
|
|
profilCurr["rot"],
|
|
profilCurr["vec"],
|
|
profilCurr["run"],
|
|
diag,
|
|
sol))
|
|
|
|
## subVolume shape
|
|
ptsSubVolProfil = [Vector(-profilCurr["overhang"], -overhangV, 0.0),
|
|
Vector(profilCurr["run"], profilCurr["height"], 0.0),
|
|
Vector(profilCurr["run"], profilCurr["height"] + 900000.0, 0.0),
|
|
Vector(-profilCurr["overhang"], profilCurr["height"] + 900000.0, 0.0)]
|
|
self.subVolShps.append(self.createProfilShape(ptsSubVolProfil,
|
|
midpoint,
|
|
profilCurr["rot"],
|
|
profilCurr["vec"],
|
|
profilCurr["run"],
|
|
diag,
|
|
sol))
|
|
|
|
if len(self.shps) == 0: # occurs if all segments have angle=90 or run=0.
|
|
# create a flat roof using the eavePtLst outline:
|
|
ptsPaneProject = []
|
|
for i in range(ln):
|
|
ptsPaneProject.append(self.profilsDico[i]["eavePtLst"][0])
|
|
face = face_from_points(ptsPaneProject)
|
|
if face:
|
|
thk = max(1.0, self.profilsDico[0]["thickness"]) # FreeCAD will crash when extruding with a null vector here
|
|
self.shps = [face.extrude(Vector(0.0, 0.0, thk))]
|
|
self.subVolShps = [face.extrude(Vector(0.0, 0.0, 1000000.0))]
|
|
|
|
## baseVolume
|
|
base = self.shps.pop()
|
|
for s in self.shps:
|
|
base = base.fuse(s)
|
|
base = self.processSubShapes(obj, base, pl)
|
|
self.applyShape(obj, base, pl, allownosolid = True)
|
|
|
|
## subVolume
|
|
self.sub = self.subVolShps.pop()
|
|
for s in self.subVolShps:
|
|
self.sub = self.sub.fuse(s)
|
|
self.sub = self.sub.removeSplitter()
|
|
if not self.sub.isNull():
|
|
if not DraftGeomUtils.isNull(pl):
|
|
self.sub.Placement = pl
|
|
|
|
elif base:
|
|
base = self.processSubShapes(obj, base, pl)
|
|
self.applyShape(obj, base, pl, allownosolid = True)
|
|
else:
|
|
FreeCAD.Console.PrintMessage(translate("Arch", "Unable to create a roof"))
|
|
|
|
def getSubVolume(self, obj):
|
|
'''returns a volume to be subtracted'''
|
|
custom_subvolume = getattr(obj, 'Subvolume', None)
|
|
if custom_subvolume:
|
|
return custom_subvolume.Shape
|
|
|
|
if not obj.Base:
|
|
return None
|
|
|
|
if not hasattr(obj.Base, "Shape"):
|
|
return None
|
|
|
|
if obj.Base.Shape.Solids:
|
|
# For roof created from Base object as solids:
|
|
# Not only the solid of the base object itself be subtracted from
|
|
# a Wall, but all portion of the wall above the roof solid would be
|
|
# subtracted as well.
|
|
#
|
|
# FC forum discussion : Sketch based Arch_Roof and wall substraction
|
|
# - https://forum.freecad.org/viewtopic.php?t=84389
|
|
#
|
|
faces = []
|
|
solids = []
|
|
for f in obj.Base.Shape.Faces: # obj.Base.Shape.Solids.Faces
|
|
p = f.findPlane() # Curve face (surface) seems return no Plane
|
|
if p:
|
|
if p.Axis[2] < 0: # z<0, i.e. normal pointing below horizon
|
|
faces.append(f)
|
|
else:
|
|
# Not sure if it is pointing towards and/or above horizon
|
|
# (upward or downward), or it is curve surface, just add.
|
|
faces.append(f)
|
|
|
|
# Attempt to find normal at non-planar face to verify if
|
|
# it is pointing downward, but cannot conclude even all test
|
|
# points happens to be pointing upward. So add in any rate.
|
|
|
|
for f in faces:
|
|
solid = f.extrude(Vector(0.0, 0.0, 1000000.0))
|
|
solids.append(solid)
|
|
compound = Part.Compound(solids)
|
|
return compound
|
|
|
|
sub_field = getattr(self, 'sub', None)
|
|
if not sub_field:
|
|
self.execute(obj)
|
|
return self.sub
|
|
|
|
|
|
def computeAreas(self, obj):
|
|
'''computes border and ridge roof edges length'''
|
|
if hasattr(obj, "RidgeLength") and hasattr(obj, "BorderLength"):
|
|
rl = 0
|
|
bl = 0
|
|
rn = 0
|
|
bn = 0
|
|
if obj.Shape:
|
|
if obj.Shape.Faces:
|
|
faceLst = []
|
|
for face in obj.Shape.Faces:
|
|
if face.normalAt(0, 0).getAngle(Vector(0.0, 0.0, 1.0)) < math.pi / 2.0:
|
|
faceLst.append(face)
|
|
if faceLst:
|
|
try:
|
|
shell = Part.Shell(faceLst)
|
|
except Exception:
|
|
pass
|
|
else:
|
|
lut={}
|
|
if shell.Faces:
|
|
for face in shell.Faces:
|
|
for edge in face.Edges:
|
|
hc = edge.hashCode()
|
|
if hc in lut:
|
|
lut[hc] = lut[hc] + 1
|
|
else:
|
|
lut[hc] = 1
|
|
for edge in shell.Edges:
|
|
if lut[edge.hashCode()] == 1:
|
|
bl += edge.Length
|
|
bn += 1
|
|
elif lut[edge.hashCode()] == 2:
|
|
rl += edge.Length
|
|
rn += 1
|
|
if obj.RidgeLength.Value != rl:
|
|
obj.RidgeLength = rl
|
|
#print(str(rn)+" ridge edges in roof "+obj.Name)
|
|
if obj.BorderLength.Value != bl:
|
|
obj.BorderLength = bl
|
|
#print(str(bn)+" border edges in roof "+obj.Name)
|
|
ArchComponent.Component.computeAreas(self, obj)
|
|
|
|
|
|
class _ViewProviderRoof(ArchComponent.ViewProviderComponent):
|
|
'''A View Provider for the Roof object'''
|
|
def __init__(self, vobj):
|
|
ArchComponent.ViewProviderComponent.__init__(self, vobj)
|
|
|
|
def getIcon(self):
|
|
return ":/icons/Arch_Roof_Tree.svg"
|
|
|
|
def attach(self, vobj):
|
|
self.Object = vobj.Object
|
|
return
|
|
|
|
def setEdit(self, vobj, mode=0):
|
|
if mode != 0:
|
|
return None
|
|
|
|
if vobj.Object.Base.Shape.Solids:
|
|
taskd = ArchComponent.ComponentTaskPanel()
|
|
taskd.obj = self.Object
|
|
taskd.update()
|
|
FreeCADGui.Control.showDialog(taskd)
|
|
else:
|
|
taskd = _RoofTaskPanel()
|
|
taskd.obj = self.Object
|
|
taskd.update()
|
|
FreeCADGui.Control.showDialog(taskd)
|
|
return True
|
|
|
|
|
|
class _RoofTaskPanel:
|
|
'''The editmode TaskPanel for Roof objects'''
|
|
def __init__(self):
|
|
self.updating = False
|
|
self.obj = None
|
|
self.form = QtGui.QWidget()
|
|
self.form.setObjectName("TaskPanel")
|
|
self.grid = QtGui.QGridLayout(self.form)
|
|
self.grid.setObjectName("grid")
|
|
self.title = QtGui.QLabel(self.form)
|
|
self.grid.addWidget(self.title, 0, 0, 1, 1)
|
|
|
|
# tree
|
|
self.tree = QtGui.QTreeWidget(self.form)
|
|
self.grid.addWidget(self.tree, 1, 0, 1, 1)
|
|
self.tree.setRootIsDecorated(False) # remove 1st column's extra left margin
|
|
self.tree.setColumnCount(7)
|
|
self.tree.header().resizeSection(0, 37) # 37px seems to be the minimum size
|
|
self.tree.header().resizeSection(1, 70)
|
|
self.tree.header().resizeSection(2, 62)
|
|
self.tree.header().resizeSection(3, 37)
|
|
self.tree.header().resizeSection(4, 60)
|
|
self.tree.header().resizeSection(5, 60)
|
|
self.tree.header().resizeSection(6, 70)
|
|
|
|
QtCore.QObject.connect(self.tree, QtCore.SIGNAL("itemChanged(QTreeWidgetItem *, int)"), self.edit)
|
|
self.update()
|
|
|
|
def isAllowedAlterSelection(self):
|
|
return False
|
|
|
|
def isAllowedAlterView(self):
|
|
return True
|
|
|
|
def getStandardButtons(self):
|
|
return QtGui.QDialogButtonBox.Close
|
|
|
|
def update(self):
|
|
'''fills the treewidget'''
|
|
self.updating = True
|
|
if self.obj:
|
|
root = self.tree.invisibleRootItem()
|
|
if root.childCount() == 0:
|
|
for i in range(len(self.obj.Angles)):
|
|
QtGui.QTreeWidgetItem(self.tree)
|
|
for i in range(len(self.obj.Angles)):
|
|
item = root.child(i)
|
|
item.setText(0, str(i))
|
|
item.setText(1, str(self.obj.Angles[i]))
|
|
item.setText(2, str(self.obj.Runs[i]))
|
|
item.setText(3, str(self.obj.IdRel[i]))
|
|
item.setText(4, str(self.obj.Thickness[i]))
|
|
item.setText(5, str(self.obj.Overhang[i]))
|
|
item.setText(6, str(self.obj.Heights[i]))
|
|
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
|
|
# treeHgt = 1 + 23 + (len(self.obj.Angles) * 17) + 1 # 1px borders, 23px header, 17px rows
|
|
# self.tree.setMinimumSize(QtCore.QSize(445, treeHgt))
|
|
self.retranslateUi(self.form)
|
|
self.updating = False
|
|
|
|
def edit(self, item, column):
|
|
if not self.updating:
|
|
self.resetObject()
|
|
|
|
def resetObject(self, remove=None):
|
|
'''transfers the values from the widget to the object'''
|
|
ang = []
|
|
run = []
|
|
rel = []
|
|
thick = []
|
|
over = []
|
|
root = self.tree.invisibleRootItem()
|
|
for it in root.takeChildren():
|
|
ang.append(float(it.text(1)))
|
|
run.append(float(it.text(2)))
|
|
rel.append(int(it.text(3)))
|
|
thick.append(float(it.text(4)))
|
|
over.append(float(it.text(5)))
|
|
self.obj.Runs = run
|
|
self.obj.Angles = ang
|
|
self.obj.IdRel = rel
|
|
self.obj.Thickness = thick
|
|
self.obj.Overhang = over
|
|
self.obj.touch()
|
|
FreeCAD.ActiveDocument.recompute()
|
|
self.update()
|
|
|
|
def reject(self):
|
|
FreeCAD.ActiveDocument.recompute()
|
|
FreeCADGui.ActiveDocument.resetEdit()
|
|
return True
|
|
|
|
def retranslateUi(self, TaskPanel):
|
|
TaskPanel.setWindowTitle(QtGui.QApplication.translate("Arch", "Roof", None))
|
|
self.title.setText(QtGui.QApplication.translate("Arch", "Parameters of the roof profiles :\n* Angle : slope in degrees relative to the horizontal.\n* Run : horizontal distance between the wall and the ridge.\n* Thickness : thickness of the roof.\n* Overhang : horizontal distance between the eave and the wall.\n* Height : height of the ridge above the base (calculated automatically).\n* IdRel : Id of the relative profile used for automatic calculations.\n---\nIf Angle = 0 and Run = 0 then the profile is identical to the relative profile.\nIf Angle = 0 then the angle is calculated so that the height is the same as the relative profile.\nIf Run = 0 then the run is calculated so that the height is the same as the relative profile.", None))
|
|
self.tree.setHeaderLabels([QtGui.QApplication.translate("Arch", "Id", None),
|
|
QtGui.QApplication.translate("Arch", "Angle (deg)", None),
|
|
QtGui.QApplication.translate("Arch", "Run (mm)", None),
|
|
QtGui.QApplication.translate("Arch", "IdRel", None),
|
|
QtGui.QApplication.translate("Arch", "Thickness (mm)", None),
|
|
QtGui.QApplication.translate("Arch", "Overhang (mm)", None),
|
|
QtGui.QApplication.translate("Arch", "Height (mm)", None)])
|
|
|