Arch: Handle rectangle-and circle-based profiles in IFC import/export

This commit is contained in:
Yorik van Havre
2020-05-29 12:12:48 +02:00
parent 2503cf0bda
commit b74a2ba7ef
6 changed files with 187 additions and 41 deletions

View File

@@ -1256,6 +1256,7 @@ def getExtrusionData(shape,sortmethod="area"):
"area" = Of the faces with the smallest area, the one with the lowest z coordinate.
"z" = The face with the lowest z coordinate.
a 3D vector = the face which center is closest to the given 3D point
Parameters
----------
@@ -1314,9 +1315,11 @@ def getExtrusionData(shape,sortmethod="area"):
if valids:
if sortmethod == "z":
valids.sort(key=lambda v: v[0].CenterOfMass.z)
else:
elif sortmethod == "area":
# sort by smallest area
valids.sort(key=lambda v: v[0].Area)
else:
valids.sort(key=lambda v: (v[0].CenterOfMass.sub(sortmethod)).Length)
return valids[0]
return None

View File

@@ -1746,16 +1746,18 @@ def getProfile(ifcfile,p):
pt = ifcbin.createIfcAxis2Placement2D(povc,pxvc)
if isinstance(p.Edges[0].Curve,Part.Circle):
# extruded circle
profile = ifcfile.createIfcCircleProfileDef("AREA",None,pt,p.Edges[0].Curve.Radius)
profile = ifcbin.createIfcCircleProfileDef("AREA",None,pt,p.Edges[0].Curve.Radius)
elif isinstance(p.Edges[0].Curve,Part.Ellipse):
# extruded ellipse
profile = ifcfile.createIfcEllipseProfileDef("AREA",None,pt,p.Edges[0].Curve.MajorRadius,p.Edges[0].Curve.MinorRadius)
profile = ifcbin.createIfcEllipseProfileDef("AREA",None,pt,p.Edges[0].Curve.MajorRadius,p.Edges[0].Curve.MinorRadius)
elif (checkRectangle(p.Edges)):
# arbitrarily use the first edge as the rectangle orientation
d = vec(p.Edges[0])
d.normalize()
pxvc = ifcbin.createIfcDirection(tuple(d)[:2])
povc = ifcbin.createIfcCartesianPoint(tuple(p.CenterOfMass[:2]))
povc = ifcbin.createIfcCartesianPoint((0.0,0.0))
# profile must be located at (0,0) because placement gets added later
#povc = ifcbin.createIfcCartesianPoint(tuple(p.CenterOfMass[:2]))
pt = ifcbin.createIfcAxis2Placement2D(povc,pxvc)
#semiPerimeter = p.Length/2
#diff = math.sqrt(semiPerimeter**2 - 4*p.Area)
@@ -1763,7 +1765,7 @@ def getProfile(ifcfile,p):
#h = min(abs((semiPerimeter + diff)/2),abs((semiPerimeter - diff)/2))
b = p.Edges[0].Length
h = p.Edges[1].Length
profile = ifcfile.createIfcRectangleProfileDef("AREA",'rectangular',pt,b,h)
profile = ifcbin.createIfcRectangleProfileDef("AREA",'rectangular',pt,b,h)
elif (len(p.Faces) == 1) and (len(p.Wires) > 1):
# face with holes
f = p.Faces[0]

View File

@@ -208,6 +208,7 @@ class recycler:
self.ifcfile = ifcfile
self.compress = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetBool("ifcCompress",True)
self.mergeProfiles = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch").GetBool("ifcMergeProfiles",False)
self.cartesianpoints = {(0,0,0):self.ifcfile[8]} # from template
self.directions = {(1,0,0):self.ifcfile[6],(0,0,1):self.ifcfile[7],(0,1,0):self.ifcfile[10]} # from template
self.polylines = {}
@@ -222,6 +223,7 @@ class recycler:
self.transformationoperators = {}
self.psas = {}
self.spared = 0
self.profiledefs = {}
def createIfcCartesianPoint(self,points):
if self.compress and points in self.cartesianpoints:
@@ -387,3 +389,33 @@ class recycler:
if self.compress:
self.psas[key] = c
return c
def createIfcRectangleProfileDef(self,name,mode,pt,b,h):
key = "RECT"+str(name)+str(mode)+str(pt)+str(b)+str(h)
if self.compress and self.mergeProfiles and key in self.profiledefs:
return self.profiledefs[key]
else:
c = self.ifcfile.createIfcRectangleProfileDef(name,mode,pt,b,h)
if self.compress and self.mergeProfiles:
self.profiledefs[key] = c
return c
def createIfcCircleProfileDef(self,name,mode,pt,r):
key = "CIRC"+str(name)+str(mode)+str(pt)+str(r)
if self.compress and self.mergeProfiles and key in self.profiledefs:
return self.profiledefs[key]
else:
c = self.ifcfile.createIfcCircleProfileDef(name,mode,pt,r)
if self.compress and self.mergeProfiles:
self.profiledefs[key] = c
return c
def createIfcEllipseProfileDef(self,name,mode,pt,majr,minr):
key = "ELLI"+str(name)+str(mode)+str(pt)+str(majr)+str(minr)
if self.compress and self.mergeProfiles and key in self.profiledefs:
return self.profiledefs[key]
else:
c = self.ifcfile.createIfcEllipseProfileDef(name,mode,pt,majr,minr)
if self.compress and self.mergeProfiles:
self.profiledefs[key] = c
return c

View File

@@ -139,7 +139,14 @@ structuralifcobjects = (
# ********** get the prefs, available in import and export ****************
def getPreferences():
"""retrieves IFC preferences"""
"""retrieves IFC preferences.
MERGE_MODE_ARCH:
0 = parametric arch objects
1 = non-parametric arch objects
2 = Part shapes
3 = One compound per storey
"""
p = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/Arch")
@@ -274,6 +281,7 @@ def insert(filename,docname,skip=[],only=[],root=None,preferences=None):
subtractions = importIFCHelper.buildRelSubtractions(ifcfile)
mattable = importIFCHelper.buildRelMattable(ifcfile)
colors = importIFCHelper.buildRelProductColors(ifcfile, prodrepr)
colordict = {} # { objname:color tuple } for non-GUI use
if preferences['DEBUG']: print("done.")
# only import a list of IDs and their children, if defined
@@ -485,24 +493,29 @@ def insert(filename,docname,skip=[],only=[],root=None,preferences=None):
else:
if preferences['GET_EXTRUSIONS'] and (preferences['MERGE_MODE_ARCH'] != 1):
# get IFC profile
profileid = None
sortmethod = None
if product.Representation:
if product.Representation.Representations:
if product.Representation.Representations[0].is_a("IfcShapeRepresentation"):
if product.Representation.Representations[0].Items:
if product.Representation.Representations[0].Items[0].is_a("IfcExtrudedAreaSolid"):
profileid = product.Representation.Representations[0].Items[0].SweptArea.id()
sortmethod = importIFCHelper.getProfileCenterPoint(product.Representation.Representations[0].Items[0])
# recompose extrusions from a shape
if ptype in ["IfcWall","IfcWallStandardCase","IfcSpace"]:
sortmethod = "z"
else:
sortmethod = "area"
if not sortmethod:
if ptype in ["IfcWall","IfcWallStandardCase","IfcSpace"]:
sortmethod = "z"
else:
sortmethod = "area"
ex = Arch.getExtrusionData(shape,sortmethod) # is this an extrusion?
if ex:
# check for extrusion profile
baseface = None
profileid = None
addplacement = None
if product.Representation:
if product.Representation.Representations:
if product.Representation.Representations[0].is_a("IfcShapeRepresentation"):
if product.Representation.Representations[0].Items:
if product.Representation.Representations[0].Items[0].is_a("IfcExtrudedAreaSolid"):
profileid = product.Representation.Representations[0].Items[0].SweptArea.id()
if profileid and (profileid in profiles):
# reuse existing profile if existing
@@ -526,23 +539,34 @@ def insert(filename,docname,skip=[],only=[],root=None,preferences=None):
print("extrusion ",end="")
import DraftGeomUtils
if DraftGeomUtils.hasCurves(ex[0]) or len(ex[0].Wires) != 1:
# curves or holes? We just make a Part face
baseface = FreeCAD.ActiveDocument.addObject("Part::Feature",name+"_footprint")
# bug/feature in ifcopenshell? Some faces of a shell may have non-null placement
# workaround to remove the bad placement: exporting/reimporting as step
if not ex[0].Placement.isNull():
import tempfile
fd, tf = tempfile.mkstemp(suffix=".stp")
ex[0].exportStep(tf)
f = Part.read(tf)
os.close(fd)
os.remove(tf)
# is this a circle?
if (len(ex[0].Edges) == 1) and isinstance(ex[0].Edges[0].Curve,Part.Circle):
baseface = Draft.makeCircle(ex[0].Edges[0])
else:
f = ex[0]
baseface.Shape = f
# curves or holes? We just make a Part face
baseface = FreeCAD.ActiveDocument.addObject("Part::Feature",name+"_footprint")
# bug/feature in ifcopenshell? Some faces of a shell may have non-null placement
# workaround to remove the bad placement: exporting/reimporting as step
if not ex[0].Placement.isNull():
import tempfile
fd, tf = tempfile.mkstemp(suffix=".stp")
ex[0].exportStep(tf)
f = Part.read(tf)
os.close(fd)
os.remove(tf)
else:
f = ex[0]
baseface.Shape = f
else:
# no hole and no curves, we make a Draft Wire instead
baseface = Draft.makeWire([v.Point for v in ex[0].Wires[0].OrderedVertexes],closed=True)
# no curve and no hole, we can make a draft object
verts = [v.Point for v in ex[0].Wires[0].OrderedVertexes]
# TODO verts are different if shape is made of RectangleProfileDef or not
# is this a rectangle?
if importIFCHelper.isRectangle(verts):
baseface = Draft.makeRectangle(verts,face=True)
else:
# no hole and no curves, we make a Draft Wire instead
baseface = Draft.makeWire(verts,closed=True)
if profileid:
# store for possible shared use
profiles[profileid] = baseface
@@ -818,12 +842,15 @@ def insert(filename,docname,skip=[],only=[],root=None,preferences=None):
# color
if FreeCAD.GuiUp and (pid in colors) and colors[pid]:
# if preferences['DEBUG']: print(" setting color: ",int(colors[pid][0]*255),"/",int(colors[pid][1]*255),"/",int(colors[pid][2]*255))
if hasattr(obj.ViewObject,"ShapeColor"):
obj.ViewObject.ShapeColor = tuple(colors[pid][0:3])
if hasattr(obj.ViewObject,"Transparency"):
obj.ViewObject.Transparency = colors[pid][3]
if (pid in colors) and colors[pid]:
colordict[obj.Name] = colors[pid]
if FreeCAD.GuiUp:
# if preferences['DEBUG']: print(" setting color: ",int(colors[pid][0]*255),"/",int(colors[pid][1]*255),"/",int(colors[pid][2]*255))
if hasattr(obj.ViewObject,"ShapeColor"):
obj.ViewObject.ShapeColor = tuple(colors[pid][0:3])
if hasattr(obj.ViewObject,"Transparency"):
obj.ViewObject.Transparency = colors[pid][3]
# if preferences['DEBUG'] is on, recompute after each shape
if preferences['DEBUG']: FreeCAD.ActiveDocument.recompute()
@@ -1248,6 +1275,13 @@ def insert(filename,docname,skip=[],only=[],root=None,preferences=None):
if not obj.InList:
rootgroup.addObject(obj)
# Save colordict in non-GUI mode
if colordict and not FreeCAD.GuiUp:
import json
d = doc.Meta
d["colordict"] = json.dumps(colordict)
doc.Meta = d
FreeCAD.ActiveDocument.recompute()
if ZOOMOUT and FreeCAD.GuiUp:
@@ -1344,3 +1378,24 @@ def createFromProperties(propsets,ifcfile):
else:
print("Unhandled FreeCAD property:",name," of type:",ptype)
return obj
def applyColorDict(doc,colordict=None):
"""applies the contents of a color dict to the objects in the given doc.
If no colordict is given, the doc Meta property is searched for a "colordict" entry."""
if not colordict:
if "colordict" in doc.Meta:
import json
colordict = json.loads(doc.Meta["colordict"])
if colordict:
for obj in doc.Objects:
if obj.Name in colordict:
color = colordict[obj.Name]
if hasattr(obj.ViewObject,"ShapeColor"):
obj.ViewObject.ShapeColor = tuple(color[0:3])
if hasattr(obj.ViewObject,"Transparency") and (len(color) >= 4):
obj.ViewObject.Transparency = color[3]
else:
print("No valid color dict to apply")

View File

@@ -607,6 +607,9 @@ def get2DShape(representation,scaling=1000):
pts.append(c)
return Part.makePolygon(pts)
def getRectangle(ent):
return Part.makePlane(ent.XDim,ent.YDim)
def getLine(ent):
pts = []
p1 = getVector(ent.Pnt)
@@ -629,11 +632,13 @@ def get2DShape(representation,scaling=1000):
result = []
if ent.is_a() in ["IfcGeometricCurveSet","IfcGeometricSet"]:
elts = ent.Elements
elif ent.is_a() in ["IfcLine","IfcPolyline","IfcCircle","IfcTrimmedCurve"]:
elif ent.is_a() in ["IfcLine","IfcPolyline","IfcCircle","IfcTrimmedCurve","IfcRectangleProfileDef"]:
elts = [ent]
for el in elts:
if el.is_a("IfcPolyline"):
result.append(getPolyline(el))
if el.is_a("IfcRectangleProfileDef"):
result.append(getRectangle(el))
elif el.is_a("IfcLine"):
result.append(getLine(el))
elif el.is_a("IfcCircle"):
@@ -691,6 +696,40 @@ def get2DShape(representation,scaling=1000):
elif item.is_a("IfcTextLiteral"):
t = Draft.makeText([item.Literal],point=getPlacement(item.Placement,scaling).Base)
return t # dirty hack... Object creation should not be done here
elif representation.is_a() in ["IfcPolyline","IfcCircle","IfcTrimmedCurve"]:
elif representation.is_a() in ["IfcPolyline","IfcCircle","IfcTrimmedCurve","IfcRectangleProfileDef"]:
result = getCurveSet(representation)
return result
def getProfileCenterPoint(sweptsolid):
"""returns the center point of the profile of an extrusion"""
v = FreeCAD.Vector(0,0,0)
if hasattr(sweptsolid,"SweptArea"):
profile = get2DShape(sweptsolid.SweptArea)
if profile:
profile = profile[0]
if hasattr(profile,"CenterOfMass"):
v = profile.CenterOfMass
elif hasattr(profile,"BoundBox"):
v = profile.BoundBox.Center
if hasattr(sweptsolid,"Position"):
pos = getPlacement(sweptsolid.Position)
v = pos.multVec(v)
return v
def isRectangle(verts):
"""returns True if the given 4 vertices form a rectangle"""
if len(verts) != 4:
return False
v1 = verts[1].sub(verts[0])
v2 = verts[2].sub(verts[1])
v3 = verts[3].sub(verts[2])
v4 = verts[0].sub(verts[3])
if abs(v2.getAngle(v1)-math.pi/2) > 0.01:
return False
if abs(v3.getAngle(v2)-math.pi/2) > 0.01:
return False
if abs(v4.getAngle(v3)-math.pi/2) > 0.01:
return False
return True

View File

@@ -38,7 +38,7 @@ if App.GuiUp:
from draftviewproviders.view_rectangle import ViewProviderRectangle
def make_rectangle(length, height, placement=None, face=None, support=None):
def make_rectangle(length, height=0, placement=None, face=None, support=None):
"""makeRectangle(length, width, [placement], [face])
Creates a Rectangle object with length in X direction and height in Y
@@ -54,12 +54,27 @@ def make_rectangle(length, height, placement=None, face=None, support=None):
face : Bool
If face is False, the rectangle is shown as a wireframe,
otherwise as a face.
Rectangles can also be constructed by giving them a list of four vertices
as first argument: makeRectangle(list_of_vertices,face=...)
but you are responsible to check yourself that these 4 vertices are ordered
and actually form a rectangle, otherwise the result might be wrong. Placement
is ignored when constructing a rectangle this way (face argument is kept).
"""
if not App.ActiveDocument:
App.Console.PrintError("No active document. Aborting\n")
return
if isinstance(length,(list,tuple)) and (len(length) == 4):
verts = length
xv = verts[1].sub(verts[0])
yv = verts[3].sub(verts[0])
zv = xv.cross(yv)
rr = App.Rotation(xv,yv,zv,"XYZ")
rp = App.Placement(verts[0],rr)
return makeRectangle(xv.Length,yv.Length,rp,face,support)
if placement: type_check([(placement,App.Placement)], "make_rectangle")
obj = App.ActiveDocument.addObject("Part::Part2DObjectPython","Rectangle")