Arch: Curtain wall

This commit is contained in:
Yorik van Havre
2020-04-28 16:27:06 +02:00
parent 1ecde67d8b
commit dda0510e48

View File

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