From 7b94a6122b5d4b860cf2057477f72e9274c70bd1 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Fri, 29 May 2020 12:12:48 +0200 Subject: [PATCH] Arch: Handle rectangle-and circle-based profiles in IFC import/export --- src/Mod/Arch/ArchCommands.py | 5 +- src/Mod/Arch/exportIFC.py | 10 +- src/Mod/Arch/exportIFCHelper.py | 32 ++++++ src/Mod/Arch/importIFC.py | 121 ++++++++++++++++------ src/Mod/Arch/importIFCHelper.py | 43 +++++++- src/Mod/Draft/draftmake/make_rectangle.py | 17 ++- 6 files changed, 187 insertions(+), 41 deletions(-) diff --git a/src/Mod/Arch/ArchCommands.py b/src/Mod/Arch/ArchCommands.py index 57d6f2d0ea..7ce091084c 100644 --- a/src/Mod/Arch/ArchCommands.py +++ b/src/Mod/Arch/ArchCommands.py @@ -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 diff --git a/src/Mod/Arch/exportIFC.py b/src/Mod/Arch/exportIFC.py index 39d96ce994..f9ecbb88a3 100644 --- a/src/Mod/Arch/exportIFC.py +++ b/src/Mod/Arch/exportIFC.py @@ -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] diff --git a/src/Mod/Arch/exportIFCHelper.py b/src/Mod/Arch/exportIFCHelper.py index 11ff3b313f..15fc89798a 100644 --- a/src/Mod/Arch/exportIFCHelper.py +++ b/src/Mod/Arch/exportIFCHelper.py @@ -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 diff --git a/src/Mod/Arch/importIFC.py b/src/Mod/Arch/importIFC.py index f9521baa9a..fc5257b405 100644 --- a/src/Mod/Arch/importIFC.py +++ b/src/Mod/Arch/importIFC.py @@ -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") diff --git a/src/Mod/Arch/importIFCHelper.py b/src/Mod/Arch/importIFCHelper.py index 025909960c..fea9ef87a2 100644 --- a/src/Mod/Arch/importIFCHelper.py +++ b/src/Mod/Arch/importIFCHelper.py @@ -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 diff --git a/src/Mod/Draft/draftmake/make_rectangle.py b/src/Mod/Draft/draftmake/make_rectangle.py index a181c46071..df8657c775 100644 --- a/src/Mod/Draft/draftmake/make_rectangle.py +++ b/src/Mod/Draft/draftmake/make_rectangle.py @@ -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")