From 7c12c8688cc1a15e68655ef163a15294a774d0f3 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Tue, 28 Apr 2020 16:27:06 +0200 Subject: [PATCH] Arch: Curtain wall --- src/Mod/Arch/ArchCurtainWall.py | 334 ++++++++++++++++---------------- 1 file changed, 169 insertions(+), 165 deletions(-) diff --git a/src/Mod/Arch/ArchCurtainWall.py b/src/Mod/Arch/ArchCurtainWall.py index 89246344e8..9190b59105 100644 --- a/src/Mod/Arch/ArchCurtainWall.py +++ b/src/Mod/Arch/ArchCurtainWall.py @@ -45,6 +45,8 @@ else: return txt # \endcond +ANGLETOLERANCE = 0.67 # vectors with angles below this are considered going in same dir + ## @package ArchCurtainWall # \ingroup ARCH # \brief The Curtain Wall object and tools @@ -55,19 +57,15 @@ else: Curtain wall tool Abstract: Curtain walls need a surface to work on (base). -They then divide that surface into a grid, by intersecting -it with a grid of planes, forming pseudorectangular facets. +They then divide each face of that surface into quads, +using the face parameters grid. The vertical lines can then receive one type of profile (vertical mullions), the horizontal ones another -(horizontal mullions), and the facets a third (panels). +(horizontal mullions), and the quads a third (panels). -The surface can be prepared before applying the curtain wall -tool on it, which allow for more complex panel/mullion -configuration. - -We then have two cases, depending on the surface: Either the -four corners of each facet form a plane, in which case the +We then have two cases, depending on each quad: Either the +four corners of each quad are coplanar, in which case the panel filling is rectangular, or they don't, in which case the facet is triangulated and receives a third mullion (diagonal mullion). @@ -179,10 +177,9 @@ class CurtainWall(ArchComponent.Component): obj.addProperty("App::PropertyInteger","VerticalMullionNumber","CurtainWall", QT_TRANSLATE_NOOP("App::Property","The number of vertical mullions")) obj.setEditorMode("VerticalMullionNumber",1) - if not "VerticalDirection" in pl: - obj.addProperty("App::PropertyVector","VerticalDirection","CurtainWall", - QT_TRANSLATE_NOOP("App::Property","The vertical direction of this curtain wall")) - obj.VerticalDirection = FreeCAD.Vector(0,0,1) + if not "VerticalMullionAlignment" in pl: + obj.addProperty("App::PropertyBool","VerticalMullionAlignment","CurtainWall", + QT_TRANSLATE_NOOP("App::Property","If the profile of the vertical mullions get aligned with the surface or not")) if not "VerticalSections" in pl: obj.addProperty("App::PropertyInteger","VerticalSections","CurtainWall", QT_TRANSLATE_NOOP("App::Property","The number of vertical sections of this curtain wall")) @@ -198,9 +195,9 @@ class CurtainWall(ArchComponent.Component): obj.addProperty("App::PropertyInteger","HorizontalMullionNumber","CurtainWall", QT_TRANSLATE_NOOP("App::Property","The number of horizontal mullions")) obj.setEditorMode("HorizontalMullionNumber",1) - if not "HorizontalDirection" in pl: - obj.addProperty("App::PropertyVector","HorizontalDirection","CurtainWall", - QT_TRANSLATE_NOOP("App::Property","The horizontal direction of this curtain wall")) + if not "HorizontalMullionAlignment" in pl: + obj.addProperty("App::PropertyBool","HorizontalMullionAlignment","CurtainWall", + QT_TRANSLATE_NOOP("App::Property","If the profile of the horizontal mullions gets aligned with the surface or not")) if not "HorizontalSections" in pl: obj.addProperty("App::PropertyInteger","HorizontalSections","CurtainWall", QT_TRANSLATE_NOOP("App::Property","The number of horizontal sections of this curtain wall")) @@ -234,13 +231,17 @@ class CurtainWall(ArchComponent.Component): if not "SwapHorizontalVertical" in pl: obj.addProperty("App::PropertyBool","SwapHorizontalVertical","CurtainWall", QT_TRANSLATE_NOOP("App::Property","Swaps horizontal and vertical lines")) - if not "Normal" in pl: - obj.addProperty("App::PropertyVector","Normal","CurtainWall", - QT_TRANSLATE_NOOP("App::Property","The normal direction of this curtain wall")) if not "Refine" in pl: obj.addProperty("App::PropertyBool","Refine","CurtainWall", QT_TRANSLATE_NOOP("App::Property","Perform subtractions between components so none overlap")) - + if not "CenterProfiles" in pl: + obj.addProperty("App::PropertyBool","CenterProfiles","CurtainWall", + QT_TRANSLATE_NOOP("App::Property","Centers the profile over the edges or not")) + obj.CenterProfiles = True + if not "VerticalDirection" in pl: + obj.addProperty("App::PropertyVector","VerticalDirection","CurtainWall", + QT_TRANSLATE_NOOP("App::Property","The vertical direction reference to be used by this object to deduce vertical/horizontal directions. Keep it close to the actual vertical direction of your curtain wall")) + obj.VerticalDirection = FreeCAD.Vector(0,0,1) def onDocumentRestored(self,obj): @@ -282,91 +283,50 @@ class CurtainWall(ArchComponent.Component): if not hasattr(obj.DiagonalMullionProfile,"Shape"): FreeCAD.Console.PrintLog(obj.Label+": invalid diagonal mullion profile\n") return - - # identify normal, vdir and hdir directions - normal = obj.Normal - if not normal.Length: - normal = DraftGeomUtils.getNormal(obj.Base.Shape) - if not normal: - FreeCAD.Console.PrintLog(obj.Label+": unable to calculate normal\n") + if (not obj.HorizontalSections) or (not obj.VerticalSections): return - else: - # set the normal if not yet set - obj.Normal = normal - normal.normalize() - if obj.VerticalDirection.Length: + + facets = [] + + # subdivide the faces into quads + for face in obj.Base.Shape.Faces: + + fp = face.ParameterRange + + # guessing horizontal/vertical directions vdir = obj.VerticalDirection - else: - FreeCAD.Console.PrintLog(obj.Label+": vertical direction not set\n") - return - vdir.normalize() - if obj.HorizontalDirection.Length: - hdir = obj.HorizontalDirection - else: - hdir = normal.cross(obj.VerticalDirection) - if hdir and hdir.Length: - obj.HorizontalDirection = hdir + if not vdir.Length: + vdir = FreeCAD.Vector(0,0,1) + vdir.normalize() + basevector = face.valueAt(fp[1],fp[3]).sub(face.valueAt(fp[0],fp[2])) + a = basevector.getAngle(vdir) + if (a <= math.pi/2+ANGLETOLERANCE) and (a >= math.pi/2-ANGLETOLERANCE): + facedir = True + vertsec = obj.VerticalSections + horizsec = obj.HorizontalSections else: - FreeCAD.Console.PrintLog(obj.Label+": unable to calculate horizontal direction\n") - return - hdir.normalize() - #print(hdir,vdir,normal) + facedir = False + vertsec = obj.HorizontalSections + horizsec = obj.VerticalSections - # calculate boundbox points - verts = [v.Point for v in obj.Base.Shape.Vertexes] - hverts = [self.getProjectedLength(v,hdir) for v in verts] - vverts = [self.getProjectedLength(v,vdir) for v in verts] - nverts = [self.getProjectedLength(v,normal) for v in verts] - #print(hverts,vverts,nverts) - MinH = min(hverts) - MaxH = max(hverts) - MinV = min(vverts) - MaxV = max(vverts) - MinN = min(nverts) - MaxN = max(nverts) - - # also define extended bbox to better boolean ops results - ExtMinH = MinH-5 - ExtMaxH = MaxH+5 - ExtMinV = MinV-5 - ExtMaxV = MaxV+5 - ExtMinN = MinN-5 - ExtMaxN = MaxN+5 - - # construct vertical planes - vplanes = [] - if obj.VerticalSections > 1: - p0 = FreeCAD.Vector(normal).multiply(ExtMinN) - p0 = p0.add(FreeCAD.Vector(hdir).multiply(MinH)) - p0 = p0.add(FreeCAD.Vector(vdir).multiply(ExtMinV)) - p1 = p0.add(FreeCAD.Vector(normal).multiply(ExtMaxN-ExtMinN)) - p2 = p1.add(FreeCAD.Vector(vdir).multiply(ExtMaxV-ExtMinV)) - p3 = p0.add(FreeCAD.Vector(vdir).multiply(ExtMaxV-ExtMinV)) - vplane = Part.Face(Part.makePolygon([p0,p1,p2,p3,p0])) - vstep = FreeCAD.Vector(hdir).multiply((MaxH-MinH)/obj.VerticalSections) - for i in range(1,obj.VerticalSections): - vplane = vplane.translate(vstep) - vplanes.append(vplane.copy()) + hstep = (fp[1]-fp[0])/vertsec + vstep = (fp[3]-fp[2])/horizsec - # construct horizontal planes - hplanes = [] - if obj.HorizontalSections > 1: - p4 = FreeCAD.Vector(normal).multiply(ExtMinN) - p4 = p4.add(FreeCAD.Vector(hdir).multiply(ExtMinH)) - p4 = p4.add(FreeCAD.Vector(vdir).multiply(MinV)) - p5 = p4.add(FreeCAD.Vector(normal).multiply(ExtMaxN-ExtMinN)) - p6 = p5.add(FreeCAD.Vector(hdir).multiply(ExtMaxH-ExtMinH)) - p7 = p4.add(FreeCAD.Vector(hdir).multiply(ExtMaxH-ExtMinH)) - hplane = Part.Face(Part.makePolygon([p4,p5,p6,p7,p4])) - hstep = FreeCAD.Vector(vdir).multiply((MaxV-MinV)/obj.HorizontalSections) - for i in range(1,obj.HorizontalSections): - hplane = hplane.translate(hstep) - hplanes.append(hplane.copy()) + # construct facets + for i in range(vertsec): + for j in range(horizsec): + p0 = face.valueAt(fp[0]+i*hstep,fp[2]+j*vstep) + p1 = face.valueAt(fp[0]+(i+1)*hstep,fp[2]+j*vstep) + p2 = face.valueAt(fp[0]+(i+1)*hstep,fp[2]+(j+1)*vstep) + p3 = face.valueAt(fp[0]+i*hstep,fp[2]+(j+1)*vstep) + facet = Part.Face(Part.makePolygon([p0,p1,p2,p3,p0])) + facets.append(facet) - # apply sections - baseshape = obj.Base.Shape.copy() - for plane in vplanes+hplanes: - baseshape = baseshape.cut(plane) + if not facets: + FreeCAD.Console.PrintLog(obj.Label+": failed to subdivide shape\n") + return + + baseshape = Part.makeShell(facets) # make edge/normal relation table edgetable = {} @@ -382,26 +342,54 @@ class CurtainWall(ArchComponent.Component): if len(faces) == 1: self.edgenormals[ec] = faces[0].normalAt(0,0) else: - n = faces[0].normalAt(0,0).cross(faces[1].normalAt(0,0)) - if n.Length: + n = faces[0].normalAt(0,0).add(faces[1].normalAt(0,0)) + if n.Length > 0.001: n.normalize() else: + # adjacent faces have same normals n = faces[0].normalAt(0,0) self.edgenormals[ec] = n + # sort edges between vertical/horizontal + hedges = [] + vedges = [] + for edge in baseshape.Edges: + v = edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) + a = v.getAngle(vdir) + if (a <= math.pi/2+ANGLETOLERANCE) and (a >= math.pi/2-ANGLETOLERANCE): + hedges.append(edge) + else: + vedges.append(edge) + # construct vertical mullions vmullions = [] vprofile = self.getMullionProfile(obj,"Vertical") if vprofile: - vedges = self.getNormalEdges(baseshape.Edges,hdir) - vmullions = self.makeMullions(vedges,vprofile,normal) + for vedge in vedges: + vn = self.edgenormals[vedge.hashCode()] + if (vn.x != 0) or (vn.y != 0): + avn = FreeCAD.Vector(vn.x,vn.y,0) + rot = FreeCAD.Rotation(FreeCAD.Vector(0,-1,0),avn) + else: + rot = FreeCAD.Rotation() + if obj.VerticalMullionAlignment: + ev = vedge.Vertexes[-1].Point.sub(vedge.Vertexes[0].Point) + rot = FreeCAD.Rotation(FreeCAD.Vector(1,0,0),ev).multiply(rot) + vmullions.append(self.makeMullion(vedge,vprofile,rot,obj.CenterProfiles)) # construct horizontal mullions hmullions = [] hprofile = self.getMullionProfile(obj,"Horizontal") if hprofile: - hedges = self.getNormalEdges(baseshape.Edges,vdir) - hmullions = self.makeMullions(hedges,hprofile,normal) + for hedge in hedges: + rot = FreeCAD.Rotation(FreeCAD.Vector(0,1,0),-90) + vn = self.edgenormals[hedge.hashCode()] + if (vn.x != 0) or (vn.y != 0): + avn = FreeCAD.Vector(vn.x,vn.y,0) + rot = FreeCAD.Rotation(FreeCAD.Vector(0,-1,0),avn).multiply(rot) + if obj.HorizontalMullionAlignment: + rot = FreeCAD.Rotation(avn,vn).multiply(rot) + hmullions.append(self.makeMullion(hedge,hprofile,rot,obj.CenterProfiles)) # construct panels panels = [] @@ -428,7 +416,9 @@ class CurtainWall(ArchComponent.Component): n = (dedges[0].Vertexes[-1].Point.sub(dedges[0].Point)) dprofile = self.getMullionProfile(obj,"Diagonal") if dprofile: - dmullions = self.makeMullions(dedges,dprofile,normal) + for dedge in dedges: + rot = FreeCAD.Rotation(FreeCAD.Vector(0,0,1),dedge.Vertexes[-1].Point.sub(dedge.Vertexes[0].Point)) + dmullions.append(self.makeMullion(dedge,dprofile,rot,obj.CenterProfiles)) # perform subtractions if obj.Refine: @@ -458,13 +448,13 @@ class CurtainWall(ArchComponent.Component): panels = [m.cut(subdmullion) for m in panels] # mount shape - shape = Part.makeCompound(vmullions+hmullions+dmullions+panels) - shape = self.processSubShapes(obj,shape,pl) - self.applyShape(obj,shape,pl) obj.VerticalMullionNumber = len(vmullions) obj.HorizontalMullionNumber = len(hmullions) obj.DiagonalMullionNumber = len(dmullions) obj.PanelNumber = len(panels) + shape = Part.makeCompound(vmullions+hmullions+dmullions+panels) + shape = self.processSubShapes(obj,shape,pl) + self.applyShape(obj,shape,pl) def makePanel(self,verts,thickness): @@ -478,27 +468,24 @@ class CurtainWall(ArchComponent.Component): panel = panel.extrude(n) return panel - def makeMullions(self,edges,profile,upvec): + def makeMullion(self,edge,profile,rotation,recenter=False): - """creates a list of mullions from a list of edges and a profile""" + """creates a mullions from an edge and a profile""" - mullions = [] - pcenter = FreeCAD.Vector(0,0,0) - if hasattr(profile,"CenterOfMass"): - center = profile.CenterOfMass - for edge in edges: - p0 = edge.Vertexes[0].Point - p1 = edge.Vertexes[-1].Point - axis = p1.sub(p0) - if edge.hashCode() in self.edgenormals: - normal = self.edgenormals[edge.hashCode()] - else: - normal = self.normal - mullion = self.rotateProfile(profile,axis,normal) - mullion = mullion.translate(p0.sub(center)) - mullion = mullion.extrude(p1.sub(p0)) - mullions.append(mullion) - return mullions + center = FreeCAD.Vector(0,0,0) + if recenter: + if hasattr(profile,"CenterOfMass"): + center = profile.CenterOfMass + elif hasattr(profile,"BoundBox"): + center = profile.BoundBox.Center + p0 = edge.Vertexes[0].Point + p1 = edge.Vertexes[-1].Point + mullion = profile.copy() + if rotation: + mullion = mullion.rotate(center,rotation.Axis,math.degrees(rotation.Angle)) + mullion = mullion.translate(p0.sub(center)) + mullion = mullion.extrude(p1.sub(p0)) + return mullion def getMullionProfile(self,obj,direction): @@ -516,43 +503,6 @@ class CurtainWall(ArchComponent.Component): profile = Part.Face(Part.makePlane(prop2,prop2,FreeCAD.Vector(-prop2/2,-prop2/2,0))) return profile - def rotateProfile(self,profile,axis,normal): - - """returns a rotated profile""" - - import Part,DraftGeomUtils - - oaxis = DraftGeomUtils.getNormal(profile) - if len(profile.Edges[0].Vertexes) > 1: - oxvec = profile.Edges[0].Vertexes[-1].Point.sub(profile.Edges[0].Vertexes[0].Point) - elif hasattr(profile.Curve,"Center"): - oxvec = profile.Edges[0].Vertexes[-1].Point.sub(profile.Curve.Center) - orot = FreeCAD.Rotation(oxvec,oaxis.cross(oxvec),oaxis,"XYZ") - nrot = FreeCAD.Rotation(normal,axis.cross(normal),axis,"XYZ") - r = nrot.multiply(orot.inverted()) - if hasattr(profile,"CenterOfMass"): - c = profile.CenterOfMass - elif hasattr(profile,"BoundBox"): - c = profile.BoundBox.Center - else: - c = FreeCAD.Vector(0,0,0) - rprofile = profile.copy().rotate(c,r.Axis,math.degrees(r.Angle)) - return rprofile - - def getNormalEdges(self,edges,reference): - - """returns a list of edges normal to the given reference""" - - result = [] - tolerance = 0.67 # we try to get all edges with angle > 45deg - for edge in edges: - if len(edge.Vertexes) > 1: - v = edge.Vertexes[-1].Point.sub(edge.Vertexes[0].Point) - a = v.getAngle(reference) - if (a <= math.pi/2+tolerance) and (a >= math.pi/2-tolerance): - result.append(edge) - return result - def getProjectedLength(self,v,ref): """gets a signed length from projecting a vector on another""" @@ -578,6 +528,60 @@ class ViewProviderCurtainWall(ArchComponent.ViewProviderComponent): import Arch_rc return ":/icons/Arch_CurtainWall_Tree.svg" + def updateData(self,obj,prop): + + if prop == "Shape": + self.colorize(obj,force=True) + + def onChanged(self,vobj,prop): + + if (prop in ["DiffuseColor","Transparency"]) and vobj.Object: + self.colorize(vobj.Object) + elif prop == "ShapeColor": + self.colorize(vobj.Object,force=True) + ArchComponent.ViewProviderComponent.onChanged(self,vobj,prop) + + def colorize(self,obj,force=False): + + "setting different part colors" + + if not obj.Shape or not obj.Shape.Solids: + return + basecolor = obj.ViewObject.ShapeColor + basetransparency = obj.ViewObject.Transparency/100.0 + panelcolor = ArchCommands.getDefaultColor("WindowGlass") + paneltransparency = 0.7 + if hasattr(obj,"Material") and obj.Material and hasattr(obj.Material,"Materials"): + if obj.Material.Names: + if "Frame" in obj.Material.Names: + mat = obj.Material.Materials[obj.Material.Names.index("Frame")] + if ('DiffuseColor' in mat.Material) and ("(" in mat.Material['DiffuseColor']): + basecolor = tuple([float(f) for f in mat.Material['DiffuseColor'].strip("()").split(",")]) + if "Glass panel" in obj.Material.Names: + mat = obj.Material.Materials[obj.Material.Names.index("Glass panel")] + if ('DiffuseColor' in mat.Material) and ("(" in mat.Material['DiffuseColor']): + panelcolor = tuple([float(f) for f in mat.Material['DiffuseColor'].strip("()").split(",")]) + if ('Transparency' in mat.Material): + paneltransparency = float(mat.Material['Transparency'])/100.0 + elif "Solid panel" in obj.Material.Names: + mat = obj.Material.Materials[obj.Material.Names.index("Solid panel")] + if ('DiffuseColor' in mat.Material) and ("(" in mat.Material['DiffuseColor']): + panelcolor = tuple([float(f) for f in mat.Material['DiffuseColor'].strip("()").split(",")]) + paneltransparency = 0 + basecolor = basecolor[:3]+(basetransparency,) + panelcolor = panelcolor[:3]+(paneltransparency,) + colors = [] + nmullions = obj.VerticalMullionNumber + obj.HorizontalMullionNumber + obj.DiagonalMullionNumber + for i,solid in enumerate(obj.Shape.Solids): + for f in solid.Faces: + if i < nmullions: + colors.append(basecolor) + else: + colors.append(panelcolor) + if self.areDifferentColors(colors,obj.ViewObject.DiffuseColor) or force: + obj.ViewObject.DiffuseColor = colors + + if FreeCAD.GuiUp: FreeCADGui.addCommand('Arch_CurtainWall',CommandArchCurtainWall())