| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | __title__ = "FreeCAD Roof" |
| | __author__ = "Yorik van Havre", "Jonathan Wiedemann" |
| | __url__ = "https://www.freecad.org" |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import math |
| |
|
| | import FreeCAD |
| | import ArchComponent |
| | import DraftGeomUtils |
| | import DraftVecUtils |
| | import Part |
| |
|
| | from FreeCAD import Units |
| | from FreeCAD import Vector |
| |
|
| | if FreeCAD.GuiUp: |
| | from PySide import QtCore, QtGui, QtWidgets |
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| | import FreeCADGui |
| | from draftutils.translate import translate |
| | else: |
| | |
| | def translate(ctxt, txt): |
| | return txt |
| |
|
| | def QT_TRANSLATE_NOOP(ctxt, txt): |
| | return txt |
| |
|
| | |
| |
|
| |
|
| | 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): |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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 [] |
| | 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 [] |
| | else: |
| | return [] |
| |
|
| | 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]) |
| | |
| | ptLst = DraftVecUtils.removeDoubles(ptLst) |
| | ln = len(ptLst) |
| | if ln < 4: |
| | 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.Type = "Roof" |
| | self.setProperties(obj) |
| | obj.IfcType = "Roof" |
| |
|
| | 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"), |
| | locked=True, |
| | ) |
| | 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", |
| | ), |
| | locked=True, |
| | ) |
| | 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" |
| | ), |
| | locked=True, |
| | ) |
| | if not "Thickness" in pl: |
| | obj.addProperty( |
| | "App::PropertyFloatList", |
| | "Thickness", |
| | "Roof", |
| | QT_TRANSLATE_NOOP("App::Property", "The list of thicknesses of the roof segments"), |
| | locked=True, |
| | ) |
| | if not "Overhang" in pl: |
| | obj.addProperty( |
| | "App::PropertyFloatList", |
| | "Overhang", |
| | "Roof", |
| | QT_TRANSLATE_NOOP("App::Property", "The list of overhangs of the roof segments"), |
| | locked=True, |
| | ) |
| | 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" |
| | ), |
| | locked=True, |
| | ) |
| | 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" |
| | ), |
| | locked=True, |
| | ) |
| | 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" |
| | ), |
| | locked=True, |
| | ) |
| | 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"), |
| | locked=True, |
| | ) |
| | 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" |
| | ), |
| | locked=True, |
| | ) |
| | 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", |
| | ), |
| | locked=True, |
| | ) |
| |
|
| | def onDocumentRestored(self, obj): |
| | ArchComponent.Component.onDocumentRestored(self, obj) |
| | self.setProperties(obj) |
| |
|
| | def loads(self, state): |
| | self.Type = "Roof" |
| |
|
| | 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"]: |
| | 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: |
| | 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] |
| | |
| | if ( |
| | 0 <= profilRel["idrel"] < numEdges |
| | and rel != profilRel["idrel"] |
| | and (profilRel["angle"] == 0.0 or profilRel["run"] == 0.0) |
| | ): |
| | 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, |
| | ] |
| |
|
| | 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: |
| | ptProjLst = [ptIntLst[0]] |
| | else: |
| | 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: |
| | 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: |
| | 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: |
| | ptProjLst = ptProjLst + [ptEaveCurrLst[0]] |
| | elif ptEaveOtherLst: |
| | ptProjLst = ptProjLst + [ptEaveOtherLst[0], profilCurr["eavePtLst"][i]] |
| | else: |
| | print("Error determining outline") |
| | else: |
| | 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) |
| | |
| | return profilShp |
| |
|
| | def execute(self, obj): |
| |
|
| | if self.clone(obj): |
| | return |
| | if not self.ensureBase(obj): |
| | return |
| |
|
| | pl = obj.Placement |
| | |
| | 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 |
| | |
| | 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) |
| | for i in range(ln): |
| | self.calcMissingData(i, ln) |
| | 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)) |
| |
|
| | |
| | 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, |
| | ) |
| | ) |
| |
|
| | |
| | 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: |
| | |
| | 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"] |
| | ) |
| | self.shps = [face.extrude(Vector(0.0, 0.0, thk))] |
| | self.subVolShps = [face.extrude(Vector(0.0, 0.0, 1000000.0))] |
| |
|
| | |
| | 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) |
| |
|
| | |
| | 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: |
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | faces = [] |
| | solids = [] |
| | for f in obj.Base.Shape.Faces: |
| | p = f.findPlane() |
| | if p: |
| | |
| | |
| | faces.append(f) |
| | else: |
| | |
| | |
| | |
| |
|
| | |
| | |
| | faces.append(f) |
| |
|
| | |
| | |
| | |
| |
|
| | for f in faces: |
| | solid = f.extrude(Vector(0.0, 0.0, 1000000.0)) |
| | if not solid.isNull() and solid.isValid() and solid.Volume > 1e-3: |
| | solids.append(solid) |
| |
|
| | |
| | for s in obj.Base.Shape.Solids: |
| | solids.append(s) |
| |
|
| | compound = Part.Compound(solids) |
| | compound.Placement = obj.Placement |
| | 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 |
| | |
| | if obj.BorderLength.Value != bl: |
| | obj.BorderLength = bl |
| | |
| | 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) |
| |
|
| | |
| | self.tree = QtGui.QTreeWidget(self.form) |
| | self.grid.addWidget(self.tree, 1, 0, 1, 1) |
| | self.tree.setItemDelegate(_RoofTaskPanel_Delegate()) |
| | self.tree.setRootIsDecorated(False) |
| | self.tree.setColumnCount(7) |
| | self.tree.header().resizeSection(0, 37) |
| | self.tree.header().resizeSection(1, 60) |
| | self.tree.header().resizeSection(2, 90) |
| | self.tree.header().resizeSection(3, 37) |
| | self.tree.header().resizeSection(4, 90) |
| | self.tree.header().resizeSection(5, 90) |
| | self.tree.header().resizeSection(6, 90) |
| |
|
| | 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, Units.Quantity(self.obj.Angles[i], Units.Angle).UserString) |
| | item.setText(2, Units.Quantity(self.obj.Runs[i], Units.Length).UserString) |
| | item.setText(3, str(self.obj.IdRel[i])) |
| | item.setText(4, Units.Quantity(self.obj.Thickness[i], Units.Length).UserString) |
| | item.setText(5, Units.Quantity(self.obj.Overhang[i], Units.Length).UserString) |
| | item.setText(6, Units.Quantity(self.obj.Heights[i], Units.Length).UserString) |
| | item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable) |
| | |
| | |
| | self.retranslateUi(self.form) |
| | self.updating = False |
| |
|
| | def _update_value(self, row, prop, str_val): |
| | |
| | str_val = str_val.replace("+", "--") |
| | val_list = getattr(self.obj, prop) |
| | val_list[row] = Units.Quantity(str_val).Value |
| | setattr(self.obj, prop, val_list) |
| |
|
| | def edit(self, item, column): |
| | """transfers an edited value from the widget to the object""" |
| | if self.updating: |
| | return |
| | row = int(item.text(0)) |
| | match column: |
| | case 1: |
| | self._update_value(row, "Angles", item.text(1)) |
| | case 2: |
| | self._update_value(row, "Runs", item.text(2)) |
| | case 3: |
| | val_list = self.obj.IdRel |
| | val_list[row] = int(item.text(3)) |
| | self.obj.IdRel = val_list |
| | case 4: |
| | self._update_value(row, "Thickness", item.text(4)) |
| | case 5: |
| | self._update_value(row, "Overhang", item.text(5)) |
| | case _: |
| | return |
| | 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* IdRel: Id of the relative profile used for automatic calculations.\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---\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", None), |
| | QtGui.QApplication.translate("Arch", "Run", None), |
| | QtGui.QApplication.translate("Arch", "IdRel", None), |
| | QtGui.QApplication.translate("Arch", "Thickness", None), |
| | QtGui.QApplication.translate("Arch", "Overhang", None), |
| | QtGui.QApplication.translate("Arch", "Height", None), |
| | ] |
| | ) |
| |
|
| |
|
| | if FreeCAD.GuiUp: |
| |
|
| | class _RoofTaskPanel_Delegate(QtWidgets.QStyledItemDelegate): |
| | """Model delegate""" |
| |
|
| | def createEditor(self, parent, option, index): |
| | if index.column() in (0, 6): |
| | |
| | return None |
| | editor = QtWidgets.QLineEdit(parent) |
| | if index.column() != 3: |
| | editor.installEventFilter(self) |
| | return editor |
| |
|
| | def setEditorData(self, editor, index): |
| | editor.setText(index.data()) |
| |
|
| | def setModelData(self, editor, model, index): |
| | model.setData(index, editor.text()) |
| |
|
| | def eventFilter(self, widget, event): |
| | if event.type() == QtCore.QEvent.FocusIn: |
| | widget.setSelection(0, FreeCADGui.draftToolBar.number_length(widget.text())) |
| | return super().eventFilter(widget, event) |
| |
|