Arch: [WIP] Curtain Wall tool

This commit is contained in:
Yorik van Havre
2020-04-27 14:32:30 +02:00
parent efd384a17b
commit 1ecde67d8b
8 changed files with 1060 additions and 10 deletions

View File

@@ -65,3 +65,4 @@ from ArchPipe import *
from ArchBuildingPart import *
from ArchReference import *
from ArchTruss import *
from ArchCurtainWall import *

View File

@@ -0,0 +1,583 @@
# -*- coding: utf8 -*-
#***************************************************************************
#* Copyright (c) 2020 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 *
#* *
#***************************************************************************
__title__="FreeCAD Arch Curtain Wall"
__author__ = "Yorik van Havre"
__url__ = "http://www.freecadweb.org"
import math,sys
import FreeCAD
import Draft
import ArchComponent
import ArchCommands
import DraftVecUtils
if FreeCAD.GuiUp:
import FreeCADGui
from PySide import QtCore, QtGui
from DraftTools 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 ArchCurtainWall
# \ingroup ARCH
# \brief The Curtain Wall object and tools
#
# This module provides tools to build Curtain wall objects.
"""
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.
The vertical lines can then receive one type of profile
(vertical mullions), the horizontal ones another
(horizontal mullions), and the facets 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
panel filling is rectangular, or they don't, in which case
the facet is triangulated and receives a third mullion
(diagonal mullion).
"""
def makeCurtainWall(baseobj=None,name="Curtain Wall"):
"""
makeCurtainWall([baseobj]): Creates a curtain wall in the active document
"""
if not FreeCAD.ActiveDocument:
FreeCAD.Console.PrintError("No active document. Aborting\n")
return
obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","CurtainWall")
obj.Label = translate("Arch","Curtain Wall")
CurtainWall(obj)
if FreeCAD.GuiUp:
ViewProviderCurtainWall(obj.ViewObject)
if baseobj:
obj.Base = baseobj
if FreeCAD.GuiUp:
baseobj.ViewObject.hide()
return obj
class CommandArchCurtainWall:
"the Arch Curtain Wall command definition"
def GetResources(self):
return {'Pixmap' : 'Arch_CurtainWall',
'MenuText': QT_TRANSLATE_NOOP("Arch_CurtainWall","Curtain Wall"),
'Accel': "C, W",
'ToolTip': QT_TRANSLATE_NOOP("Arch_CurtainWall","Creates a curtain wall object from selected line or from scratch")}
def IsActive(self):
return not FreeCAD.ActiveDocument is None
def Activated(self):
sel = FreeCADGui.Selection.getSelection()
if len(sel) > 1:
FreeCAD.Console.PrintError(translate("Arch","Please select only one base object or none")+"\n")
elif len(sel) == 1:
# build on selection
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.openTransaction(translate("Arch","Create Curtain Wall"))
FreeCADGui.addModule("Draft")
FreeCADGui.addModule("Arch")
FreeCADGui.doCommand("obj = Arch.makeCurtainWall(FreeCAD.ActiveDocument."+FreeCADGui.Selection.getSelection()[0].Name+")")
FreeCADGui.doCommand("Draft.autogroup(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
else:
# interactive line drawing
self.points = []
if hasattr(FreeCAD,"DraftWorkingPlane"):
FreeCAD.DraftWorkingPlane.setup()
if hasattr(FreeCADGui,"Snapper"):
FreeCADGui.Snapper.getPoint(callback=self.getPoint)
def getPoint(self,point=None,obj=None):
"""Callback for clicks during interactive mode"""
if point is None:
# cancelled
return
self.points.append(point)
if len(self.points) == 1:
FreeCADGui.Snapper.getPoint(last=self.points[0],callback=self.getPoint)
elif len(self.points) == 2:
FreeCADGui.Control.closeDialog()
FreeCAD.ActiveDocument.openTransaction(translate("Arch","Create Curtain Wall"))
FreeCADGui.addModule("Draft")
FreeCADGui.addModule("Arch")
FreeCADGui.doCommand("baseline = Draft.makeLine(FreeCAD."+str(self.points[0])+",FreeCAD."+str(self.points[1])+")")
FreeCADGui.doCommand("base = FreeCAD.ActiveDocument.addObject('Part::Extrusion','Extrude')")
FreeCADGui.doCommand("base.Base = baseline")
FreeCADGui.doCommand("base.DirMode = 'Custom'")
FreeCADGui.doCommand("base.Dir = App.Vector(FreeCAD.DraftWorkingPlane.axis)")
FreeCADGui.doCommand("base.LengthFwd = 1000")
FreeCADGui.doCommand("obj = Arch.makeCurtainWall(base)")
FreeCADGui.doCommand("Draft.autogroup(obj)")
FreeCAD.ActiveDocument.commitTransaction()
FreeCAD.ActiveDocument.recompute()
class CurtainWall(ArchComponent.Component):
"The curtain wall object"
def __init__(self,obj):
ArchComponent.Component.__init__(self,obj)
self.setProperties(obj)
obj.IfcType = "Curtain Wall"
def setProperties(self,obj):
pl = obj.PropertiesList
if not "VerticalMullionNumber" in pl:
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 "VerticalSections" in pl:
obj.addProperty("App::PropertyInteger","VerticalSections","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","The number of vertical sections of this curtain wall"))
obj.VerticalSections = 4
if not "VerticalMullionSize" in pl:
obj.addProperty("App::PropertyLength","VerticalMullionSize","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","The size of the vertical mullions, if no profile is used"))
obj.VerticalMullionSize = 100
if not "VerticalMullionProfile" in pl:
obj.addProperty("App::PropertyLink","VerticalMullionProfile","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","A profile for vertical mullions (disables vertical mullion size)"))
if not "HorizontalMullionNumber" in pl:
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 "HorizontalSections" in pl:
obj.addProperty("App::PropertyInteger","HorizontalSections","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","The number of horizontal sections of this curtain wall"))
obj.HorizontalSections = 4
if not "HorizontalMullionSize" in pl:
obj.addProperty("App::PropertyLength","HorizontalMullionSize","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","The size of the horizontal mullions, if no profile is used"))
obj.HorizontalMullionSize = 50
if not "HorizontalMullionProfile" in pl:
obj.addProperty("App::PropertyLink","HorizontalMullionProfile","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","A profile for horizontal mullions (disables horizontal mullion size)"))
if not "DiagonalMullionNumber" in pl:
obj.addProperty("App::PropertyInteger","DiagonalMullionNumber","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","The number of diagonal mullions"))
obj.setEditorMode("DiagonalMullionNumber",1)
if not "DiagonalMullionSize" in pl:
obj.addProperty("App::PropertyLength","DiagonalMullionSize","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","The size of the diagonal mullions, if any, if no profile is used"))
obj.DiagonalMullionSize = 50
if not "DiagonalMullionProfile" in pl:
obj.addProperty("App::PropertyLink","DiagonalMullionProfile","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","A profile for diagonal mullions, if any (disables horizontal mullion size)"))
if not "PanelNumber" in pl:
obj.addProperty("App::PropertyInteger","PanelNumber","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","The number of panels"))
obj.setEditorMode("PanelNumber",1)
if not "PanelThickness" in pl:
obj.addProperty("App::PropertyLength","PanelThickness","CurtainWall",
QT_TRANSLATE_NOOP("App::Property","The thickness of the panels"))
obj.PanelThickness = 20
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"))
def onDocumentRestored(self,obj):
ArchComponent.Component.onDocumentRestored(self,obj)
self.setProperties(obj)
def onChanged(self,obj,prop):
ArchComponent.Component.onChanged(self,obj,prop)
def execute(self,obj):
if self.clone(obj):
return
import Part,DraftGeomUtils
pl = obj.Placement
# test properties
if not obj.Base:
FreeCAD.Console.PrintLog(obj.Label+": no base\n")
return
if not hasattr(obj.Base,"Shape"):
FreeCAD.Console.PrintLog(obj.Label+": invalid base\n")
return
if not obj.Base.Shape.Faces:
FreeCAD.Console.PrintLog(obj.Label+": no faces in base\n")
return
if obj.VerticalMullionProfile:
if not hasattr(obj.VerticalMullionProfile,"Shape"):
FreeCAD.Console.PrintLog(obj.Label+": invalid vertical mullion profile\n")
return
if obj.HorizontalMullionProfile:
if not hasattr(obj.HorizontalMullionProfile,"Shape"):
FreeCAD.Console.PrintLog(obj.Label+": invalid horizontal mullion profile\n")
return
if obj.DiagonalMullionProfile:
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")
return
else:
# set the normal if not yet set
obj.Normal = normal
normal.normalize()
if obj.VerticalDirection.Length:
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
else:
FreeCAD.Console.PrintLog(obj.Label+": unable to calculate horizontal direction\n")
return
hdir.normalize()
#print(hdir,vdir,normal)
# 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())
# 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())
# apply sections
baseshape = obj.Base.Shape.copy()
for plane in vplanes+hplanes:
baseshape = baseshape.cut(plane)
# make edge/normal relation table
edgetable = {}
for face in baseshape.Faces:
for edge in face.Edges:
ec = edge.hashCode()
if ec in edgetable:
edgetable[ec].append(face)
else:
edgetable[ec] = [face]
self.edgenormals = {}
for ec,faces in edgetable.items():
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.normalize()
else:
n = faces[0].normalAt(0,0)
self.edgenormals[ec] = n
# construct vertical mullions
vmullions = []
vprofile = self.getMullionProfile(obj,"Vertical")
if vprofile:
vedges = self.getNormalEdges(baseshape.Edges,hdir)
vmullions = self.makeMullions(vedges,vprofile,normal)
# construct horizontal mullions
hmullions = []
hprofile = self.getMullionProfile(obj,"Horizontal")
if hprofile:
hedges = self.getNormalEdges(baseshape.Edges,vdir)
hmullions = self.makeMullions(hedges,hprofile,normal)
# construct panels
panels = []
dedges = []
if obj.PanelThickness.Value:
for face in baseshape.Faces:
verts = [v.Point for v in face.OuterWire.OrderedVertexes]
if len(verts) == 4:
if DraftGeomUtils.isPlanar(verts):
panel = self.makePanel(verts,obj.PanelThickness.Value)
panels.append(panel)
else:
verts1 = [verts[0],verts[1],verts[2]]
panel = self.makePanel(verts1,obj.PanelThickness.Value)
panels.append(panel)
verts2 = [verts[0],verts[2],verts[3]]
panel = self.makePanel(verts2,obj.PanelThickness.Value)
panels.append(panel)
dedges.append(Part.makeLine(verts[0],verts[2]))
# construct diagonal mullions
dmullions = []
if dedges:
n = (dedges[0].Vertexes[-1].Point.sub(dedges[0].Point))
dprofile = self.getMullionProfile(obj,"Diagonal")
if dprofile:
dmullions = self.makeMullions(dedges,dprofile,normal)
# perform subtractions
if obj.Refine:
subvmullion = None
subhmullion = None
subdmullion = None
if vmullions:
subvmullion = vmullions[0].copy()
for m in vmullions[1:]:
subvmullion = subvmullion.fuse(m)
if hmullions:
subhmullion = hmullions[0].copy()
for m in hmullions[1:]:
subhmullion = subhmullion.fuse(m)
if dmullions:
subdmullion = dmullions[0].copy()
for m in dmullions[1:]:
subdmullion = subdmullion.fuse(m)
if subvmullion:
hmullions = [m.cut(subvmullion) for m in hmullions]
if subhmullion:
dmullions = [m.cut(subvmullion) for m in dmullions]
dmullions = [m.cut(subhmullion) for m in dmullions]
panels = [m.cut(subvmullion) for m in panels]
panels = [m.cut(subhmullion) for m in panels]
if subdmullion:
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)
def makePanel(self,verts,thickness):
"""creates a panel from face points and thickness"""
import Part
panel = Part.Face(Part.makePolygon(verts+[verts[0]]))
n = panel.normalAt(0,0)
n.multiply(thickness)
panel = panel.extrude(n)
return panel
def makeMullions(self,edges,profile,upvec):
"""creates a list of mullions from a list of edges 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
def getMullionProfile(self,obj,direction):
"""returns a profile shape already properly oriented, ready for extrude"""
import Part,DraftGeomUtils
prop1 = getattr(obj,direction+"MullionProfile")
prop2 = getattr(obj,direction+"MullionSize").Value
if prop1:
profile = prop1.Shape.copy()
else:
if not prop2:
return None
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"""
proj = DraftVecUtils.project(v,ref)
if proj.getAngle(ref) < 1:
return proj.Length
else:
return -proj.Length
class ViewProviderCurtainWall(ArchComponent.ViewProviderComponent):
"A View Provider for the CurtainWall object"
def __init__(self,vobj):
ArchComponent.ViewProviderComponent.__init__(self,vobj)
def getIcon(self):
import Arch_rc
return ":/icons/Arch_CurtainWall_Tree.svg"
if FreeCAD.GuiUp:
FreeCADGui.addCommand('Arch_CurtainWall',CommandArchCurtainWall())

View File

@@ -52,6 +52,7 @@ SET(Arch_SRCS
OfflineRenderingUtils.py
exportIFC.py
ArchTruss.py
ArchCurtainWall.py
)
SET(Dice3DS_SRCS

View File

@@ -58,7 +58,7 @@ class ArchWorkbench(FreeCADGui.Workbench):
# Set up command lists
self.archtools = ["Arch_Wall", "Arch_Structure", "Arch_Rebar",
"Arch_BuildingPart",
"Arch_CurtainWall","Arch_BuildingPart",
"Arch_Project", "Arch_Site", "Arch_Building",
"Arch_Floor", "Arch_Reference",
"Arch_Window", "Arch_Roof", "Arch_AxisTools",

View File

@@ -17,6 +17,8 @@
<file>icons/Arch_CloseHoles.svg</file>
<file>icons/Arch_Component.svg</file>
<file>icons/Arch_Component_Clone.svg</file>
<file>icons/Arch_CurtainWall.svg</file>
<file>icons/Arch_CurtainWall_Tree.svg</file>
<file>icons/Arch_CutLine.svg</file>
<file>icons/Arch_CutPlane.svg</file>
<file>icons/Arch_Equipment.svg</file>

View File

@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64px"
height="64px"
id="svg2985"
version="1.1"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="Arch_CurtainWall.svg">
<defs
id="defs2987">
<linearGradient
id="linearGradient3794">
<stop
style="stop-color:#edd400;stop-opacity:1"
offset="0"
id="stop3796" />
<stop
style="stop-color:#fce94f;stop-opacity:1"
offset="1"
id="stop3798" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3794"
id="linearGradient3867"
gradientUnits="userSpaceOnUse"
x1="32.714748"
y1="27.398352"
x2="38.997726"
y2="3.6523125"
gradientTransform="matrix(-0.36199711,-0.85553613,0.73810354,-0.31230866,22.253893,65.739554)"
spreadMethod="reflect" />
<linearGradient
id="linearGradient3794-8">
<stop
style="stop-color:#ffb400;stop-opacity:1;"
offset="0"
id="stop3796-5" />
<stop
style="stop-color:#ffea00;stop-opacity:1;"
offset="1"
id="stop3798-8" />
</linearGradient>
<linearGradient
y2="23.848686"
x2="62.65237"
y1="23.848686"
x1="15.184971"
gradientTransform="matrix(1.0265568,0,0,0.91490626,-3.236706,-1.8027032)"
gradientUnits="userSpaceOnUse"
id="linearGradient3886"
xlink:href="#linearGradient3794-8"
inkscape:collect="always" />
<linearGradient
id="linearGradient3794-1">
<stop
style="stop-color:#ffb400;stop-opacity:1;"
offset="0"
id="stop3796-2" />
<stop
style="stop-color:#ffea00;stop-opacity:1;"
offset="1"
id="stop3798-2" />
</linearGradient>
<linearGradient
y2="23.848686"
x2="62.65237"
y1="23.848686"
x1="15.184971"
gradientTransform="matrix(1.0265568,0,0,0.91490626,-3.236706,-1.8027032)"
gradientUnits="userSpaceOnUse"
id="linearGradient3886-0"
xlink:href="#linearGradient3794-1"
inkscape:collect="always" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3794"
id="linearGradient3867-3"
gradientUnits="userSpaceOnUse"
x1="32.714748"
y1="27.398352"
x2="38.997726"
y2="3.6523125"
gradientTransform="matrix(-0.36199711,-0.85553613,0.73810354,-0.31230866,20.172664,63.335227)"
spreadMethod="reflect" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.096831"
inkscape:cx="35.741065"
inkscape:cy="31.523595"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:document-units="px"
inkscape:grid-bbox="true"
inkscape:window-width="1360"
inkscape:window-height="739"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:snap-global="false">
<inkscape:grid
type="xygrid"
id="grid2997"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" />
</sodipodi:namedview>
<metadata
id="metadata2990">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<dc:creator>
<cc:Agent>
<dc:title>[wmayer]</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>Arch_Site</dc:title>
<dc:date>2011-10-10</dc:date>
<dc:relation>http://www.freecadweb.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Arch/Resources/icons/Arch_Site.svg</dc:identifier>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<path
style="fill:url(#linearGradient3867);fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 6.6299713,45.573855 20.908004,52.532318 39.550895,52.411814 56.057784,60.629029 58.547233,14.153933 39.859557,8.7712607 18.416343,9.7795791 3.081208,3.4042685 Z"
id="path3763"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#c9a138;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="m 4.0627415,16.390392 14.7150265,6.474611 20.404836,-0.981002 18.639032,5.886011"
id="path890-5"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#c9a138;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="m 5.2974093,30.364138 14.5188257,6.670812 19.227634,-0.784802 17.26563,7.259413"
id="path892-9"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#c9a138;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 6.7565544,43.481706 21.277372,50.578904 38.047346,50.493083 54.4155,57.77384 56.62748,14.385588"
id="path3763-7-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="opacity:1;vector-effect:none;fill:url(#linearGradient3867);fill-opacity:1;fill-rule:evenodd;stroke:#372c0f;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 18.639033,9.8487048 20.601037,52.227979"
id="path854"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:url(#linearGradient3867);fill-rule:evenodd;stroke:#372c0f;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;font-variant-east_asian:normal;opacity:1;vector-effect:none;fill-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
d="M 39.63247,8.4753024 39.436269,52.62038"
id="path856"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#c9a138;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="M 16.639033,9.8487048 18.53167,49.696073"
id="path854-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#c9a138;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="M 37.63247,8.4753024 37.436269,52.62038"
id="path856-9"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#372c0f;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 4.0627415,18.390392 14.7150265,6.474611 20.404836,-0.981002 18.639032,5.886011"
id="path890"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#372c0f;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
d="m 5.2974093,32.364138 14.5188257,6.670812 19.227634,-0.784802 17.26563,7.259413"
id="path892"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-opacity:1;stroke:#372c0f;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 6.6299713,45.573855 20.908004,52.532318 39.550895,52.411814 56.057784,60.629029 58.547233,14.153933 39.859557,8.7712608 18.416343,9.7795792 3.081208,3.4042686 Z"
id="path3763-7"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="64px"
height="64px"
id="svg2985"
version="1.1"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="Arch_CurtainWall_Tree.svg">
<defs
id="defs2987">
<linearGradient
id="linearGradient3794">
<stop
style="stop-color:#edd400;stop-opacity:1"
offset="0"
id="stop3796" />
<stop
style="stop-color:#fce94f;stop-opacity:1"
offset="1"
id="stop3798" />
</linearGradient>
<linearGradient
id="linearGradient3794-8">
<stop
style="stop-color:#ffb400;stop-opacity:1;"
offset="0"
id="stop3796-5" />
<stop
style="stop-color:#ffea00;stop-opacity:1;"
offset="1"
id="stop3798-8" />
</linearGradient>
<linearGradient
y2="23.848686"
x2="62.65237"
y1="23.848686"
x1="15.184971"
gradientTransform="matrix(1.0265568,0,0,0.91490626,-3.236706,-1.8027032)"
gradientUnits="userSpaceOnUse"
id="linearGradient3886"
xlink:href="#linearGradient3794-8"
inkscape:collect="always" />
<linearGradient
id="linearGradient3794-1">
<stop
style="stop-color:#ffb400;stop-opacity:1;"
offset="0"
id="stop3796-2" />
<stop
style="stop-color:#ffea00;stop-opacity:1;"
offset="1"
id="stop3798-2" />
</linearGradient>
<linearGradient
y2="23.848686"
x2="62.65237"
y1="23.848686"
x1="15.184971"
gradientTransform="matrix(1.0265568,0,0,0.91490626,-3.236706,-1.8027032)"
gradientUnits="userSpaceOnUse"
id="linearGradient3886-0"
xlink:href="#linearGradient3794-1"
inkscape:collect="always" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3794"
id="linearGradient3867-3"
gradientUnits="userSpaceOnUse"
x1="32.714748"
y1="27.398352"
x2="38.997726"
y2="3.6523125"
gradientTransform="matrix(-0.36199711,-0.85553613,0.73810354,-0.31230866,20.172664,63.335227)"
spreadMethod="reflect" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.096831"
inkscape:cx="53.636136"
inkscape:cy="32.727474"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:document-units="px"
inkscape:grid-bbox="true"
inkscape:window-width="1360"
inkscape:window-height="739"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:snap-global="false">
<inkscape:grid
type="xygrid"
id="grid2997"
empspacing="2"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true" />
</sodipodi:namedview>
<metadata
id="metadata2990">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
<dc:creator>
<cc:Agent>
<dc:title>[wmayer]</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>Arch_Site</dc:title>
<dc:date>2011-10-10</dc:date>
<dc:relation>http://www.freecadweb.org/wiki/index.php?title=Artwork</dc:relation>
<dc:publisher>
<cc:Agent>
<dc:title>FreeCAD</dc:title>
</cc:Agent>
</dc:publisher>
<dc:identifier>FreeCAD/src/Mod/Arch/Resources/icons/Arch_Site.svg</dc:identifier>
<dc:rights>
<cc:Agent>
<dc:title>FreeCAD LGPL2+</dc:title>
</cc:Agent>
</dc:rights>
<cc:license>https://www.gnu.org/copyleft/lesser.html</cc:license>
<dc:contributor>
<cc:Agent>
<dc:title>[agryson] Alexander Gryson</dc:title>
</cc:Agent>
</dc:contributor>
</cc:Work>
</rdf:RDF>
</metadata>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer">
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 6.6299713,45.573855 20.908004,52.532318 39.550895,52.411814 56.057784,60.629029 58.547233,14.153933 39.859557,8.7712607 18.416343,9.7795791 3.081208,3.4042685 Z"
id="path3763"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b8b8b8;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="m 4.0627415,16.390392 14.7150265,6.474611 20.404836,-0.981002 18.639032,5.886011"
id="path890-5"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b8b8b8;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="m 5.2974093,30.364138 14.5188257,6.670812 19.227634,-0.784802 17.26563,7.259413"
id="path892-9"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;stroke:#b8b8b8;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="M 6.7565544,43.481706 21.277372,50.578904 38.047346,50.493083 54.4155,57.77384 56.62748,14.385588"
id="path3763-7-8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccc" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#232323;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="M 18.639033,9.8487048 20.601037,52.227979"
id="path854"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:none;fill-rule:evenodd;stroke:#232323;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;font-variant-east_asian:normal;opacity:1;vector-effect:none;fill-opacity:1;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
d="M 39.63247,8.4753024 39.436269,52.62038"
id="path856"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b8b8b8;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="M 16.639033,9.8487048 18.53167,49.696073"
id="path854-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#b8b8b8;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="M 37.63247,8.4753024 37.436269,52.62038"
id="path856-9"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#232323;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="m 4.0627415,18.390392 14.7150265,6.474611 20.404836,-0.981002 18.639032,5.886011"
id="path890"
inkscape:connector-curvature="0" />
<path
style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#232323;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;font-variant-east_asian:normal"
d="m 5.2974093,32.364138 14.5188257,6.670812 19.227634,-0.784802 17.26563,7.259413"
id="path892"
inkscape:connector-curvature="0" />
<path
style="fill:none;fill-opacity:1;stroke:#232323;stroke-width:2;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 6.6299713,45.573855 20.908004,52.532318 39.550895,52.411814 56.057784,60.629029 58.547233,14.153933 39.859557,8.7712608 18.416343,9.7795792 3.081208,3.4042686 Z"
id="path3763-7"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -1191,7 +1191,15 @@ def getSplineNormal(edge):
return n
def getNormal(shape):
"""Find the normal of a shape, if possible."""
"""Find the normal of a shape or list of points, if possible."""
if isinstance(shape,(list,tuple)):
if len(shape) >= 3:
v1 = shape[1].sub(shape[0])
v2 = shape[2].sub(shape[0])
n = v2.cross(v1)
if n.Length:
return n
return None
n = Vector(0,0,1)
if shape.isNull():
return n
@@ -1709,15 +1717,27 @@ def isCoplanar(faces, tolerance=0):
def isPlanar(shape):
"""Check if the given shape is planar."""
if len(shape.Vertexes) <= 3:
return True
"""Check if the given shape or list of points is planar."""
n = getNormal(shape)
for p in shape.Vertexes[1:]:
pv = p.Point.sub(shape.Vertexes[0].Point)
rv = DraftVecUtils.project(pv,n)
if not DraftVecUtils.isNull(rv):
return False
if not n:
return False
if isinstance(shape,list):
if len(shape) <= 3:
return True
else:
for v in shape[3:]:
pv = v.sub(shape[0])
rv = DraftVecUtils.project(pv,n)
if not DraftVecUtils.isNull(rv):
return False
else:
if len(shape.Vertexes) <= 3:
return True
for p in shape.Vertexes[1:]:
pv = p.Point.sub(shape.Vertexes[0].Point)
rv = DraftVecUtils.project(pv,n)
if not DraftVecUtils.isNull(rv):
return False
return True