From af84424a775b2694efa2e992f3cdb516b3d1f944 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Mon, 30 Sep 2024 14:58:11 +0200 Subject: [PATCH] BIM: NativeIFC 2D support - section planes --- src/Mod/BIM/ArchSectionPlane.py | 8 ++ src/Mod/BIM/bimcommands/BimSectionPlane.py | 6 ++ src/Mod/BIM/bimcommands/BimTDView.py | 5 ++ src/Mod/BIM/importers/exportIFC.py | 26 ++++++ src/Mod/BIM/nativeifc/ifc_export.py | 56 +++++++++++-- src/Mod/BIM/nativeifc/ifc_objects.py | 78 ++++++++++++++++-- src/Mod/BIM/nativeifc/ifc_tools.py | 95 ++++++++++++++-------- src/Mod/Draft/draftfunctions/svg.py | 8 +- src/Mod/Draft/draftobjects/shape2dview.py | 6 +- 9 files changed, 240 insertions(+), 48 deletions(-) diff --git a/src/Mod/BIM/ArchSectionPlane.py b/src/Mod/BIM/ArchSectionPlane.py index d30f0dab4a..8a6bd08d73 100644 --- a/src/Mod/BIM/ArchSectionPlane.py +++ b/src/Mod/BIM/ArchSectionPlane.py @@ -843,6 +843,10 @@ class _SectionPlane: # old objects l = obj.ViewObject.DisplaySize.Value h = obj.ViewObject.DisplaySize.Value + if not l: + l = 1 + if not h: + h = 1 p = Part.makePlane(l,h,Vector(l/2,-h/2,0),Vector(0,0,-1)) # make sure the normal direction is pointing outwards, you never know what OCC will decide... if p.normalAt(0,0).getAngle(obj.Placement.Rotation.multVec(FreeCAD.Vector(0,0,1))) > 1: @@ -1018,6 +1022,10 @@ class _ViewProviderSectionPlane: if hasattr(vobj,"Transparency"): self.mat2.transparency.setValue(vobj.Transparency/100.0) elif prop in ["DisplayLength","DisplayHeight","ArrowSize"]: + # for IFC objects: propagate to the object + if prop in ["DisplayLength","DisplayHeight"]: + if hasattr(vobj.Object.Proxy, "onChanged"): + vobj.Object.Proxy.onChanged(vobj.Object, prop) if hasattr(vobj,"DisplayLength") and hasattr(vobj,"DisplayHeight"): ld = vobj.DisplayLength.Value/2 hd = vobj.DisplayHeight.Value/2 diff --git a/src/Mod/BIM/bimcommands/BimSectionPlane.py b/src/Mod/BIM/bimcommands/BimSectionPlane.py index 549b1021a1..c6a326b27a 100644 --- a/src/Mod/BIM/bimcommands/BimSectionPlane.py +++ b/src/Mod/BIM/bimcommands/BimSectionPlane.py @@ -62,6 +62,12 @@ class Arch_SectionPlane: FreeCADGui.doCommand("section = Arch.makeSectionPlane("+ss+")") FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() + if len(sel) == 1 and getattr(sel[0], "IfcClass", None) == "IfcProject": + # remove the IFC project, otherwise we can't aggregate (circular loop) + FreeCADGui.doCommand("section.Objects = []") + #FreeCADGui.addModule("nativeifc.ifc_tools") + #p = "FreeCAD.ActiveDocument."+sel[0].Name + #FreeCADGui.doCommand("nativeifc.ifc_tools.aggregate(section,"+p+")") diff --git a/src/Mod/BIM/bimcommands/BimTDView.py b/src/Mod/BIM/bimcommands/BimTDView.py index 254bec5b98..c3d1230d01 100644 --- a/src/Mod/BIM/bimcommands/BimTDView.py +++ b/src/Mod/BIM/bimcommands/BimTDView.py @@ -93,6 +93,11 @@ class BIM_TDView: page.addView(view) if page.Scale: view.Scale = page.Scale + if "ShapeMode" in draft.PropertiesList: + draft.ShapeMode = "Shape" + for child in draft.OutListRecursive: + if "ShapeMode" in child.PropertiesList: + child.ShapeMode = "Shape" FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute() diff --git a/src/Mod/BIM/importers/exportIFC.py b/src/Mod/BIM/importers/exportIFC.py index 8f2dfc1929..df25562c90 100644 --- a/src/Mod/BIM/importers/exportIFC.py +++ b/src/Mod/BIM/importers/exportIFC.py @@ -2464,6 +2464,8 @@ def create_annotation(anno, ifcfile, context, history, preferences): ovc = None zvc = None xvc = None + repid = "Annotation" + reptype = "Annotation2D" if anno.isDerivedFrom("Part::Feature"): if Draft.getType(anno) == "Hatch": objectType = "HATCH" @@ -2548,6 +2550,30 @@ def create_annotation(anno, ifcfile, context, history, preferences): tpl = ifcbin.createIfcAxis2Placement3D(pos,zdir,xdir) txt = ifcfile.createIfcTextLiteral(vp.string,tpl,"LEFT") reps.append(txt) + elif Draft.getType(anno) == "SectionPlane": + p = FreeCAD.Vector(anno.Placement.Base).multiply(preferences['SCALE_FACTOR']) + ovc = ifcbin.createIfcCartesianPoint((p.x,p.y,p.z)) + zvc = ifcbin.createIfcDirection(tuple(anno.Placement.Rotation.multVec(FreeCAD.Vector(0,0,1)))) + xvc = ifcbin.createIfcDirection(tuple(anno.Placement.Rotation.multVec(FreeCAD.Vector(1,0,0)))) + objectType = "DRAWING" + l = w = h = 1000 + if anno.ViewObject: + if anno.ViewObject.DisplayLength.Value: + l = anno.ViewObject.DisplayLength.Value + if anno.ViewObject.DisplayHeight.Value: + w = anno.ViewObject.DisplayHeight.Value + if anno.Depth.Value: + h = anno.Depth.Value + l = FreeCAD.Vector(l, w, h).multiply(preferences['SCALE_FACTOR']) + zdir = ifcbin.createIfcDirection((0.0,0.0,1.0)) + xdir = ifcbin.createIfcDirection((1.0,0.0,0.0)) + pos = ifcbin.createIfcCartesianPoint((-l.x/2,-l.y/2,-l.z)) + tpl = ifcbin.createIfcAxis2Placement3D(pos,zdir,xdir) + blk = ifcfile.createIfcBlock(tpl, l.x, l.y, l.z) + csg = ifcfile.createIfcCsgSolid(blk) + reps = [csg] + repid = "Body" + reptype = "CSG" else: print("Unable to handle object",anno.Label) return None diff --git a/src/Mod/BIM/nativeifc/ifc_export.py b/src/Mod/BIM/nativeifc/ifc_export.py index d957333204..403f597c97 100644 --- a/src/Mod/BIM/nativeifc/ifc_export.py +++ b/src/Mod/BIM/nativeifc/ifc_export.py @@ -89,6 +89,20 @@ def create_representation(obj, ifcfile): return representation, placement +def get_object_type(ifcentity, objecttype=None): + """Determines a creation type for this object""" + + if not objecttype: + if ifcentity.is_a("IfcAnnotation"): + if get_sectionplane(ifcentity): + objecttype = "sectionplane" + elif get_dimension(ifcentity): + objecttype = "dimension" + elif get_text(ifcentity): + objecttype = "text" + return objecttype + + def is_annotation(obj): """Determines if the given FreeCAD object should be saved as an IfcAnnotation""" @@ -104,7 +118,8 @@ def is_annotation(obj): "Text", "Dimension", "LinearDimension", - "AngularDimension"]: + "AngularDimension", + "SectionPlane"]: return True elif obj.isDerivedFrom("Part::Feature"): if obj.Shape and (not obj.Shape.Solids) and obj.Shape.Edges: @@ -153,6 +168,26 @@ def get_dimension(annotation): return None +def get_sectionplane(annotation): + """Determines if an IfcAnnotation is representing a section plane. + Returns a list containing a placement, and optionally an X dimension, + an Y dimension and a depth dimension""" + + if annotation.is_a("IfcAnnotation"): + if annotation.ObjectType == "DRAWING": + s = ifcopenshell.util.unit.calculate_unit_scale(annotation.file) * 1000 + result = [get_placement(annotation.ObjectPlacement, scale=s)] + for rep in annotation.Representation.Representations: + for item in rep.Items: + if item.is_a("IfcCsgSolid"): + if item.TreeRootExpression.is_a("IfcBlock"): + result.append(item.TreeRootExpression.XLength*s) + result.append(item.TreeRootExpression.YLength*s) + result.append(item.TreeRootExpression.ZLength*s) + return result + return None + + def create_annotation(obj, ifcfile): """Adds an IfcAnnotation from the given object to the given IFC file""" @@ -183,16 +218,21 @@ def get_history(ifcfile): return history -def get_placement(ifcelement, ifcfile): +def get_placement(ifcelement, ifcfile=None, scale=None): """Returns a FreeCAD placement from an IFC placement""" - s = 0.001 / ifcopenshell.util.unit.calculate_unit_scale(ifcfile) - return importIFCHelper.getPlacement(ifcelement, scaling=s) + if not scale: + if not ifcfile: + ifcfile = ifcelement.file + scale = 0.001 / ifcopenshell.util.unit.calculate_unit_scale(ifcfile) + return importIFCHelper.getPlacement(ifcelement, scaling=scale) -def get_scaled_point(point, ifcfile, is2d=False): +def get_scaled_point(point, ifcfile=None, is2d=False): """Returns a scaled 2d or 3d point tuple form a FreeCAD point""" + if not ifcfile: + ifcfile = ifcelement.file s = 0.001 / ifcopenshell.util.unit.calculate_unit_scale(ifcfile) v = FreeCAD.Vector(point) v.multiply(s) @@ -200,3 +240,9 @@ def get_scaled_point(point, ifcfile, is2d=False): if is2d: v = v[:2] return v + +def get_scaled_value(value, ifcfile): + """Returns a scaled dimension value""" + + s = 0.001 / ifcopenshell.util.unit.calculate_unit_scale(ifcfile) + return value * s diff --git a/src/Mod/BIM/nativeifc/ifc_objects.py b/src/Mod/BIM/nativeifc/ifc_objects.py index 71ef0de337..d5ce797180 100644 --- a/src/Mod/BIM/nativeifc/ifc_objects.py +++ b/src/Mod/BIM/nativeifc/ifc_objects.py @@ -26,7 +26,7 @@ import FreeCAD translate = FreeCAD.Qt.translate # the property groups below should not be treated as psets -NON_PSETS = ["Base", "IFC", "", "Geometry", "Dimension", "Linear/radial dimension", "PhysicalProperties"] +NON_PSETS = ["Base", "IFC", "", "Geometry", "Dimension", "Linear/radial dimension", "SectionPlane", "PhysicalProperties"] class ifc_object: """Base class for all IFC-based objects""" @@ -61,7 +61,7 @@ class ifc_object: self.edit_type(obj) elif prop == "Group": self.edit_group(obj) - elif obj.getGroupOfProperty(prop) == "IFC": + elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "IFC": if prop not in ["StepId"]: self.edit_attribute(obj, prop) elif prop == "Label": @@ -70,6 +70,8 @@ class ifc_object: self.edit_annotation(obj, "Text", "\n".join(obj.Text)) elif prop in ["Start", "End"]: self.edit_annotation(obj, prop) + elif prop in ["DisplayLength","DisplayHeight","Depth"]: + self.edit_annotation(obj, prop) elif prop == "Placement": if getattr(self, "virgin_placement", False): self.virgin_placement = False @@ -79,9 +81,9 @@ class ifc_object: elif prop == "Modified": if obj.ViewObject: obj.ViewObject.signalChangeIcon() - elif obj.getGroupOfProperty(prop) == "Geometry": + elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "Geometry": self.edit_geometry(obj, prop) - elif obj.getGroupOfProperty(prop) not in NON_PSETS: + elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) not in NON_PSETS: # Treat all property groups outside the default ones as Psets # print("DEBUG: editinog pset prop",prop) self.edit_pset(obj, prop) @@ -182,14 +184,15 @@ class ifc_object: from nativeifc import ifc_export if not value: - value = obj.getPropertyByName(attribute) + if hasattr(obj, attribute): + value = obj.getPropertyByName(attribute) ifcfile = ifc_tools.get_ifcfile(obj) elt = ifc_tools.get_ifc_element(obj, ifcfile) if elt: if attribute == "Text": text = ifc_export.get_text(elt) if text: - result = ifc_tools.set_attribute(ifcfile, text, "Literal", value) + ifc_tools.set_attribute(ifcfile, text, "Literal", value) elif attribute in ["Start", "End"]: dim = ifc_export.get_dimension(elt) if dim: @@ -204,9 +207,29 @@ class ifc_object: value[0] = ifc_export.get_scaled_point(obj.Start, ifcfile, is2d) else: value[-1] = ifc_export.get_scaled_point(obj.End, ifcfile, is2d) - result = ifc_tools.set_attribute(ifcfile, points, "CoordList", value) + ifc_tools.set_attribute(ifcfile, points, "CoordList", value) else: print("DEBUG: unknown dimension curve type:",sub) + elif attribute in ["DisplayLength","DisplayHeight","Depth"]: + l = w = h = 1000 + if obj.ViewObject: + if obj.ViewObject.DisplayLength.Value: + l = ifc_export.get_scaled_value(obj.ViewObject.DisplayLength.Value) + if obj.ViewObject.DisplayHeight.Value: + w = ifc_export.get_scaled_value(obj.ViewObject.DisplayHeight.Value) + if obj.Depth.Value: + h = ifc_export.get_scaled_value(obj.Depth.Value) + if elt.Representation.Representations: + for rep in elt.Representation.Representations: + for item in rep.Items: + if item.is_a("IfcCsgSolid"): + if item.TreeRootExpression.is_a("IfcBlock"): + block = item.TreeRootExpression + loc = block.Position.Location + ifc_tools.set_attribute(ifcfile, block, "XLength", l) + ifc_tools.set_attribute(ifcfile, block, "YLength", w) + ifc_tools.set_attribute(ifcfile, block, "ZLength", h) + ifc_tools.set_attribute(ifcfile, loc, "Coordinates", (-l/2, -h/2, -h)) def edit_geometry(self, obj, prop): """Edits a geometry property of an object""" @@ -326,6 +349,47 @@ class ifc_object: # Not doing anything right now because an unset Type property could screw the ifc file pass + def get_section_data(self, obj): + """Returns two things: a list of objects and a cut plane""" + + from nativeifc import ifc_tools # lazy import + import Part + + if not obj.IfcClass == "IfcAnnotation": + return None, None + if obj.ObjectType != "DRAWING": + return None, None + objs = getattr(obj, "Objects", []) + if not objs: + # no object defined, we automatically use the project + objs = [] + proj = ifc_tools.get_project(obj) + if isinstance(proj, FreeCAD.DocumentObject): + objs.append(proj) + objs.extend(ifc_tools.get_freecad_children(proj)) + if objs: + s = [] + for o in objs: + # TODO print a better message + if o.ShapeMode != "Shape": + s.append(o) + if s: + FreeCAD.Console.PrintLog("DEBUG: Generating shapes. This might take some time...\n") + for o in s: + o.ShapeMode = "Shape" + o.recompute() + l = 1 + h = 1 + if obj.ViewObject: + if hasattr(obj.ViewObject,"DisplayLength"): + l = obj.ViewObject.DisplayLength.Value + h = obj.ViewObject.DisplayHeight.Value + plane = Part.makePlane(l,h,FreeCAD.Vector(l/2,-h/2,0),FreeCAD.Vector(0,0,1)) + plane.Placement = obj.Placement + return objs, plane + else: + return None, None + class document_object: """Holder for the document's IFC objects""" diff --git a/src/Mod/BIM/nativeifc/ifc_tools.py b/src/Mod/BIM/nativeifc/ifc_tools.py index 436fda8d3e..eaaa283947 100644 --- a/src/Mod/BIM/nativeifc/ifc_tools.py +++ b/src/Mod/BIM/nativeifc/ifc_tools.py @@ -255,12 +255,7 @@ def create_object(ifcentity, document, ifcfile, shapemode=0, objecttype=None): s = "IFC: Created #{}: {}, '{}'\n".format( ifcentity.id(), ifcentity.is_a(), ifcentity.Name ) - if not objecttype: - if ifcentity.is_a("IfcAnnotation"): - if ifc_export.get_dimension(ifcentity): - objecttype = "dimension" - elif ifc_export.get_text(ifcentity): - objecttype = "text" + objecttype = ifc_export.get_object_type(ifcentity, objecttype) FreeCAD.Console.PrintLog(s) obj = add_object(document, otype=objecttype) add_properties(obj, ifcfile, ifcentity, shapemode=shapemode) @@ -399,6 +394,18 @@ def get_children( return result +def get_freecad_children(obj): + """Returns the childen of this object that exist in the documemt""" + + objs = [] + children = get_children(obj) + for child in children: + childobj = get_object(child) + if childobj: + objs.extend(get_freecad_children(childobj)) + return objs + + def get_object(element, document=None, ifcfile=None): """Returns the object that references this element, if any""" @@ -486,11 +493,15 @@ def add_object(document, otype=None, oname="IfcObject"): 'layer', 'text', 'dimension', + 'sectionplane', or anything else for a standard IFC object""" if not document: return None - if otype == "dimension": + if otype == "sectionplane": + obj = Arch.makeSectionPlane() + obj.Proxy = ifc_objects.ifc_object(otype) + elif otype == "dimension": obj = Draft.make_dimension(FreeCAD.Vector(), FreeCAD.Vector(1,0,0)) obj.Proxy = ifc_objects.ifc_object(otype) obj.removeProperty("Diameter") @@ -656,31 +667,51 @@ def add_properties( setattr(obj, attr, str(value)) # annotation properties if ifcentity.is_a("IfcAnnotation"): - dim = ifc_export.get_dimension(ifcentity) - if dim and len(dim) >= 3: - if "Start" not in obj.PropertiesList: - obj.addProperty("App::PropertyVectorDistance", "Start", "Base") - if "End" not in obj.PropertiesList: - obj.addProperty("App::PropertyVectorDistance", "End", "Base") - if "Dimline" not in obj.PropertiesList: - obj.addProperty("App::PropertyVectorDistance", "Dimline", "Base") - obj.Start = dim[1] - obj.End = dim[2] - if len(dim) > 3: - obj.Dimline = dim[3] - else: - mid = obj.End.sub(obj.Start) - mid.multiply(0.5) - obj.Dimline = obj.Start.add(mid) + sectionplane = ifc_export.get_sectionplane(ifcentity) + if sectionplane: + if "Placement" not in obj.PropertiesList: + obj.addProperty("App::PropertyPlacement", "Placement", "Base") + if "Depth" not in obj.PropertiesList: + obj.addProperty("App::PropertyLength","Depth","SectionPlane") + obj.Placement = sectionplane[0] + if len(sectionplane) > 3: + obj.Depth = sectionplane[3] + vobj = obj.ViewObject + if vobj: + if "DisplayLength" not in vobj.PropertiesList: + vobj.addProperty("App::PropertyLength","DisplayLength","SectionPlane") + if "DisplayHeight" not in vobj.PropertiesList: + vobj.addProperty("App::PropertyLength","DisplayHeight","SectionPlane") + if len(sectionplane) > 1: + vobj.DisplayLength = sectionplane[1] + if len(sectionplane) > 2: + vobj.DisplayHeight = sectionplane[2] else: - text = ifc_export.get_text(ifcentity) - if text: - if "Placement" not in obj.PropertiesList: - obj.addProperty("App::PropertyPlacement", "Placement", "Base") - if "Text" not in obj.PropertiesList: - obj.addProperty("App::PropertyStringList", "Text", "Base") - obj.Text = [text.Literal] - obj.Placement = ifc_export.get_placement(ifcentity.ObjectPlacement, ifcfile) + dim = ifc_export.get_dimension(ifcentity) + if dim and len(dim) >= 3: + if "Start" not in obj.PropertiesList: + obj.addProperty("App::PropertyVectorDistance", "Start", "Base") + if "End" not in obj.PropertiesList: + obj.addProperty("App::PropertyVectorDistance", "End", "Base") + if "Dimline" not in obj.PropertiesList: + obj.addProperty("App::PropertyVectorDistance", "Dimline", "Base") + obj.Start = dim[1] + obj.End = dim[2] + if len(dim) > 3: + obj.Dimline = dim[3] + else: + mid = obj.End.sub(obj.Start) + mid.multiply(0.5) + obj.Dimline = obj.Start.add(mid) + else: + text = ifc_export.get_text(ifcentity) + if text: + if "Placement" not in obj.PropertiesList: + obj.addProperty("App::PropertyPlacement", "Placement", "Base") + if "Text" not in obj.PropertiesList: + obj.addProperty("App::PropertyStringList", "Text", "Base") + obj.Text = [text.Literal] + obj.Placement = ifc_export.get_placement(ifcentity.ObjectPlacement, ifcfile) # link Label2 and Description if "Description" in obj.PropertiesList and hasattr(obj, "setExpression"): @@ -1141,7 +1172,7 @@ def get_ifctype(obj): if dtype in ["App::Part","Part::Compound","Array"]: return "IfcElementAssembly" if dtype in ["App::DocumentObjectGroup"]: - ifctype = "IfcGroup" + return "IfcGroup" return "IfcBuildingElementProxy" diff --git a/src/Mod/Draft/draftfunctions/svg.py b/src/Mod/Draft/draftfunctions/svg.py index c2137cf1a3..7be6ebccee 100644 --- a/src/Mod/Draft/draftfunctions/svg.py +++ b/src/Mod/Draft/draftfunctions/svg.py @@ -427,7 +427,7 @@ def get_svg(obj, # all the SVG strings from the contents of the group if hasattr(obj, "isDerivedFrom"): if (obj.isDerivedFrom("App::DocumentObjectGroup") - or utils.get_type(obj) in ["Layer", "BuildingPart"] + or utils.get_type(obj) in ["Layer", "BuildingPart", "IfcGroup"] or obj.isDerivedFrom("App::LinkGroup") or (obj.isDerivedFrom("App::Link") and obj.LinkedObject.isDerivedFrom("App::DocumentObjectGroup"))): @@ -546,7 +546,8 @@ def get_svg(obj, svg = _svg_shape(svg, obj, plane, fillstyle, pathdata, stroke, linewidth, lstyle) - elif utils.get_type(obj) in ["Dimension", "LinearDimension"]: + elif (utils.get_type(obj) in ["Dimension", "LinearDimension"] + or (utils.get_type(obj) == "IfcAnnotation" and obj.ObjectType == "DIMENSION")): svg = _svg_dimension(obj, plane, scale, linewidth, fontsize, stroke, tstroke, pointratio, techdraw, rotation) @@ -690,7 +691,8 @@ def get_svg(obj, rotation, position, text, linespacing, justification) - elif utils.get_type(obj) in ["Annotation", "DraftText", "Text"]: + elif (utils.get_type(obj) in ["Annotation", "DraftText", "Text"] + or (utils.get_type(obj) == "IfcAnnotation" and obj.ObjectType == "TEXT")): # returns an svg representation of a document annotation if not App.GuiUp: _wrn("Export of texts to SVG is only available in GUI mode") diff --git a/src/Mod/Draft/draftobjects/shape2dview.py b/src/Mod/Draft/draftobjects/shape2dview.py index 6a52fb59ca..0e09b2bee8 100644 --- a/src/Mod/Draft/draftobjects/shape2dview.py +++ b/src/Mod/Draft/draftobjects/shape2dview.py @@ -213,11 +213,15 @@ class Shape2DView(DraftObject): import DraftGeomUtils pl = obj.Placement if obj.Base: - if utils.get_type(obj.Base) in ["BuildingPart","SectionPlane"]: + if utils.get_type(obj.Base) in ["BuildingPart","SectionPlane","IfcAnnotation"]: objs = [] if utils.get_type(obj.Base) == "SectionPlane": objs = self.excludeNames(obj,obj.Base.Objects) cutplane = obj.Base.Shape + elif utils.get_type(obj.Base) == "IfcAnnotation": + # this is a NativeIFC section plane + objs, cutplane = obj.Base.Proxy.get_section_data(obj.Base) + objs = self.excludeNames(obj, objs) else: objs = self.excludeNames(obj,obj.Base.Group) cutplane = Part.makePlane(1000, 1000, App.Vector(-500, -500, 0))