From 7829cab969d2cde93531acf419458cf78a0b4cd5 Mon Sep 17 00:00:00 2001 From: Yorik van Havre Date: Mon, 10 Feb 2025 10:40:46 +0100 Subject: [PATCH] Bim project manager upgrade (#17909) * BIM: NativeIFC 2D support - basic import/export + linework annotations * BIM: NativeIFC 2D support - texts * BIM: NativeIFC 2D support - dimensions * BIM: NativeIFC 2D support - optimized export of FreeCAD dimensions * BIM: NativeIFC 2D support - section planes * BIM: NativeIFC 2D support - misc fixes cf comment #2383181661 * BIM: NativeIFC 2D support - axes * BIM: Project manager upgrade * BIM: Fixed rebase leftover --- src/Mod/BIM/ArchBuildingPart.py | 20 +- .../BIM/Resources/ui/dialogProjectManager.ui | 388 +++++++++------- src/Mod/BIM/bimcommands/BimProjectManager.py | 418 +++++++++++------- src/Mod/BIM/importers/exportIFC.py | 20 +- src/Mod/BIM/nativeifc/ifc_export.py | 10 +- src/Mod/BIM/nativeifc/ifc_objects.py | 4 +- src/Mod/BIM/nativeifc/ifc_observer.py | 10 +- src/Mod/BIM/nativeifc/ifc_tools.py | 39 +- src/Mod/BIM/nativeifc/ifc_viewproviders.py | 49 +- 9 files changed, 596 insertions(+), 362 deletions(-) diff --git a/src/Mod/BIM/ArchBuildingPart.py b/src/Mod/BIM/ArchBuildingPart.py index 9d4f9521fe..14f340e1fa 100644 --- a/src/Mod/BIM/ArchBuildingPart.py +++ b/src/Mod/BIM/ArchBuildingPart.py @@ -427,10 +427,12 @@ class ViewProviderBuildingPart: def __init__(self,vobj): - vobj.addExtension("Gui::ViewProviderGroupExtensionPython") - vobj.Proxy = self - self.setProperties(vobj) - vobj.ShapeColor = ArchCommands.getDefaultColor("Helpers") + if vobj: + vobj.addExtension("Gui::ViewProviderGroupExtensionPython") + vobj.Proxy = self + self.setProperties(vobj) + vobj.ShapeColor = ArchCommands.getDefaultColor("Helpers") + self.Object = vobj.Object def setProperties(self,vobj): @@ -532,6 +534,9 @@ class ViewProviderBuildingPart: return ":/icons/Arch_Building_Tree.svg" elif self.Object.IfcType == "Annotation": return ":/icons/BIM_ArchView.svg" + elif hasattr(self.Object,"IfcClass"): + from nativeifc import ifc_viewproviders + return ifc_viewproviders.get_icon(self) return ":/icons/Arch_BuildingPart_Tree.svg" def attach(self,vobj): @@ -661,7 +666,10 @@ class ViewProviderBuildingPart: self.lco.point.setValues([[-fs,0,0],[fs,0,0],[0,-fs,0],[0,fs,0],[0,0,-fs],[0,0,fs]]) elif prop in ["OverrideUnit","ShowUnit","ShowLevel","ShowLabel"]: if hasattr(vobj,"OverrideUnit") and hasattr(vobj,"ShowUnit") and hasattr(vobj,"ShowLevel") and hasattr(vobj,"ShowLabel") and hasattr(self,"txt"): - z = vobj.Object.Placement.Base.z + vobj.Object.LevelOffset.Value + offset = getattr(vobj.Object, "LevelOffset", 0) + if hasattr(offset, "Value"): + offset = offset.Value + z = vobj.Object.Placement.Base.z + offset q = FreeCAD.Units.Quantity(z,FreeCAD.Units.Length) txt = "" if vobj.ShowLabel: @@ -729,7 +737,7 @@ class ViewProviderBuildingPart: self.clip.plane.setValue(plane) sg.insertChild(self.clip,0) else: - if self.clip: + if getattr(self,"clip",None): sg.removeChild(self.clip) self.clip = None for o in Draft.get_group_contents(vobj.Object.Group, diff --git a/src/Mod/BIM/Resources/ui/dialogProjectManager.ui b/src/Mod/BIM/Resources/ui/dialogProjectManager.ui index bc6111c044..ccbb337bc7 100644 --- a/src/Mod/BIM/Resources/ui/dialogProjectManager.ui +++ b/src/Mod/BIM/Resources/ui/dialogProjectManager.ui @@ -6,8 +6,8 @@ 0 0 - 402 - 470 + 443 + 840 @@ -17,7 +17,7 @@ - This screen allows you to configure a new BIM project in FreeCAD. + This screen allows you to create and configure a new BIM project in FreeCAD. true @@ -25,43 +25,42 @@ - + - - Use preset... - - - - - - - - + + + + 0 + 0 + + - Saves the current document as a template, including all the current BIM settings - - - Save template... - - - - - + Fill this dialog with preset values + + + Use preset... + + - + + + + 0 + 0 + + - Loads the contents of a FCStd file into the active document, applying all the BIM settings stored in it if any + The settings below can be saved as a preset. Presets are stored as .txt files in your FreeCAD user folder - Load template... + Save preset - - - + + ../../../../../../../.designer/backup../../../../../../../.designer/backup @@ -76,58 +75,108 @@ 0 - 0 - 369 - 1214 + -1041 + 428 + 1740 - + + + Create a new BIM project + - Create new document + Create a new BIM project true - false + true - - - + + + - Project name + A new BIM project will be created, either as a new FreeCAD document or as a Native IFC project + + + true - - - - Unnamed - - + + + + + + This will create a new FreeCAD docment that allows you to build a BIM model, but with no specific IFC structure yet. This is the most flexible option when you start working ona BIM project. You can convert this project to IFC anytime later. + + + Create a new document witout IFC support yet + + + false + + + + + + + This will create an IFC project. All the BIM objects you will add to that IFC project will immediately become IFC objects. This is less flexible, but allows you to stick more strictly to the IFC standard. + + + Create a Native IFC project in the current dopcument + + + true + + + + + + + The new IFC project will be created as a new FreeCAD document. In that mode, the IFC project is the FreeCAD document, anything you create in that document becomes part of the IFC project. This is extremely restrictive as no non-IFC object can be added to the document. + + + Create a locked Native IFC project as a new document + + + + + + + + + + + Project name + + + + + + + A name for this BIM or IFC project + + + Unnamed + + + + - - - - If this is checked, a human figure will be added, which helps greatly to give a sense of scale when viewing the model - - - Add a human figure - - - true - - - + + Create a new site + - Create Site + Create site false @@ -150,17 +199,20 @@ + + The East longitude of this site + E 4 + + -180.000000000000000 + - 180.0 - - - -180.0 + 180.000000000000000 @@ -180,6 +232,9 @@ + + A name for this site + Default Site @@ -194,6 +249,9 @@ + + The difference between the up direction of this site and the true North direction + ° @@ -211,13 +269,20 @@ + + The elevation of this site + - + + + The physical (postal) address of this site + + @@ -235,17 +300,20 @@ + + The North latitude of this site + N 4 - - 90.0 - - -90.0 + -90.000000000000000 + + + 90.000000000000000 @@ -256,8 +324,11 @@ + + Create a new building + - Create Building + Create building true @@ -332,6 +403,9 @@ 16777215 + + The main use of this building + QComboBox::AdjustToMinimumContentsLength @@ -352,6 +426,9 @@ + + Number of vertical axes + 0 @@ -366,6 +443,9 @@ + + Number of horizontal axes + 0 @@ -373,8 +453,11 @@ + + An estimate building width. Keep 0 if you don't want to specify this now + - 0 + 0 @@ -383,6 +466,9 @@ + + The line width of axes + 2 @@ -390,8 +476,11 @@ + + Distance between vertical axes + - 0 + 0 @@ -400,8 +489,11 @@ + + An estimate building length. Keep 0 if you don't want to specify this now + - 0 + 0 @@ -410,6 +502,9 @@ + + Distance between horizontal axes + 0 @@ -427,6 +522,9 @@ + + The color of axes + 33 @@ -448,16 +546,54 @@ + + + + Add a human figure to the document + + + Add a human figure + + + true + + + + + + A human figure will be added to the document, which helps giving a sense of scale + + + true + + + + + + Levels + + + + BIM projects are typically organized into levels, that represents the different storeys of a building. Although it is not mandatory to work with levels in FreeCAD, you can set up default levels here + + + true + + + + + The number of levels to create + 1 @@ -475,6 +611,9 @@ + + The vertical distance between each level + 0 @@ -492,29 +631,21 @@ - - - - Bind levels to vertical axes - - - - - - - Define a working plane for each level - - - - Default groups to be added to each level + Default groups to be added to each level. Defautl groups such as walls, windows,... are useful to organize the different building elements inside a level + + + true + + A list of grouls to add under each level + true @@ -530,25 +661,29 @@ + + Add a new group + Add - - + ../../../../../../../.designer/backup../../../../../../../.designer/backup + + Delete a selected group + Del - - + ../../../../../../../.designer/backup../../../../../../../.designer/backup @@ -561,30 +696,8 @@ - - - - The above settings can be saved as a preset. Presets are stored as .txt files in your FreeCAD user folder - - - true - - - - - - - Save preset - - - - - - - - @@ -600,25 +713,29 @@ + + Accept the values fo this form + OK - - + ../../../../../../../.designer/backup../../../../../../../.designer/backup + + Cancel + Cancel - - + ../../../../../../../.designer/backup../../../../../../../.designer/backup @@ -638,43 +755,6 @@
Gui/Widgets.h
- - presets - buttonSaveTemplate - buttonLoadTemplate - scrollArea - groupNewDocument - projectName - addHumanFigure - groupSite - siteName - siteAddress - siteLatitude - siteLongitude - siteDeviation - siteElevation - groupBuilding - buildingName - buildingUse - buildingLength - buildingWidth - countVAxes - distVAxes - countHAxes - distHAxes - lineWidth - lineColor - countLevels - levelHeight - levelsAxis - levelsWP - groupsList - buttonAdd - buttonDel - buttonSave - buttonOK - buttonCancel - diff --git a/src/Mod/BIM/bimcommands/BimProjectManager.py b/src/Mod/BIM/bimcommands/BimProjectManager.py index 24fc94c299..3c33f44b7a 100644 --- a/src/Mod/BIM/bimcommands/BimProjectManager.py +++ b/src/Mod/BIM/bimcommands/BimProjectManager.py @@ -25,6 +25,7 @@ import os import sys +import math import FreeCAD import FreeCADGui @@ -33,22 +34,27 @@ translate = FreeCAD.Qt.translate class BIM_ProjectManager: + def GetResources(self): + return { "Pixmap": "BIM_ProjectManager", - "MenuText": QT_TRANSLATE_NOOP("BIM_ProjectManager", "Manage project..."), + "MenuText": QT_TRANSLATE_NOOP("BIM_ProjectManager", "Setup project..."), "ToolTip": QT_TRANSLATE_NOOP( - "BIM_ProjectManager", "Setup your BIM project" + "BIM_ProjectManager", "Create or manage a BIM project" ), } def Activated(self): - import FreeCADGui - # load dialog + import FreeCADGui + import ArchBuildingPart from PySide import QtCore, QtGui self.form = FreeCADGui.PySideUic.loadUi(":/ui/dialogProjectManager.ui") + self.project = None + self.site = None + self.building = None # center the dialog over FreeCAD window mw = FreeCADGui.getMainWindow() @@ -59,75 +65,130 @@ class BIM_ProjectManager: ) # set things up - import ArchBuildingPart - self.form.buildingUse.addItems(ArchBuildingPart.BuildingTypes) self.form.setWindowIcon(QtGui.QIcon(":/icons/BIM_ProjectManager.svg")) - QtCore.QObject.connect( - self.form.buttonAdd, QtCore.SIGNAL("clicked()"), self.addGroup - ) - QtCore.QObject.connect( - self.form.buttonDel, QtCore.SIGNAL("clicked()"), self.delGroup - ) - QtCore.QObject.connect( - self.form.buttonSave, QtCore.SIGNAL("clicked()"), self.savePreset - ) - QtCore.QObject.connect( - self.form.presets, - QtCore.SIGNAL("currentIndexChanged(QString)"), - self.getPreset, - ) - QtCore.QObject.connect( - self.form.buttonOK, QtCore.SIGNAL("clicked()"), self.accept - ) - QtCore.QObject.connect( - self.form.buttonCancel, QtCore.SIGNAL("clicked()"), self.reject - ) - QtCore.QObject.connect( - self.form.buttonSaveTemplate, QtCore.SIGNAL("clicked()"), self.saveTemplate - ) - QtCore.QObject.connect( - self.form.buttonLoadTemplate, QtCore.SIGNAL("clicked()"), self.loadTemplate - ) + self.form.buttonAdd.clicked.connect(self.addGroup) + self.form.buttonDel.clicked.connect(self.delGroup) + self.form.buttonSave.clicked.connect(self.savePreset) + self.form.presets.currentIndexChanged.connect(self.getPreset) + self.form.buttonOK.clicked.connect(self.accept) + self.form.buttonCancel.clicked.connect(self.reject) self.fillPresets() + # Detect existing objects + sel = FreeCADGui.Selection.getSelection() + doc = FreeCAD.ActiveDocument + if doc: + if len(sel) == 1: + if hasattr(sel[0], "Proxy") and hasattr(sel[0].Proxy, "ifcfile"): + # case 1: a project is selected + self.project = sel[0] + self.form.groupNewProject.setEnabled(False) + if hasattr(doc, "Proxy"): + # case 2: the actuve document is a project + if hasattr(doc.Proxy, "ifcfile"): + self.project = doc + self.form.groupNewProject.setEnabled(False) + if self.project: + from nativeifc import ifc_tools + sites = ifc_tools.get_children(self.project, ifctype="IfcSite") + sites = list(filter(None, [ifc_tools.get_object(s) for s in sites])) + self.form.projectName.setText(self.project.Label) + else: + sites = [o for o in doc.Objects if getattr(o, "IfcType", "") == "Site"] + if sites: + self.site = sites[0] + self.form.siteName.setText(self.site.Label) + if hasattr(self.site,"Address"): + self.form.siteAddress.setText(self.site.Address) + elif hasattr(self.site,"SiteAddress"): + self.form.siteAddress.setText(self.site.SiteAddress) + if hasattr(self.site,"Longitude"): + self.form.siteLongitude.setValue(self.site.Longitude) + elif hasattr(self.site,"RefLongitude"): + self.form.siteLongitude.setValue(self.site.RefLongitude) + if hasattr(self.site,"Latitude"): + self.form.siteLatitude.setValue(self.site.Latitude) + elif hasattr(self.site,"RefLatitude"): + self.form.siteLatitude.setValue(self.site.RefLatitude) + if hasattr(self.site,"Elevation"): + self.form.siteElevation.setText(self.site.Elevation.UserString) + elif hasattr(self.site,"RefElevation"): + self.form.siteElevation.setText(self.site.RefElevation.UserString) + if hasattr(self.site, "Declination"): + self.form.siteElevation.setText(str(self.site.Declination)) + buildings = [] + if self.site and self.project: + from nativeifc import ifc_tools + buildings = ifc_tools.get_children(self.site, ifctype="IfcBuilding") + buildings = list(filter(None, [ifc_tools.get_object(b) for b in buildings])) + if not buildings: + buildings = [o for o in doc.Objects if getattr(o, "IfcType", "") == "Building"] + if buildings: + self.building = buildings[0] + self.form.buildingName.setText(self.building.Label) + levels = ifc_tools.get_children(self.building, ifctype="IfcBuildingStorey") + if levels: + self.form.countLevels.setValue(len(levels)) + # show dialog self.form.show() def reject(self): + self.form.hide() return True def accept(self): + import Arch import Draft import FreeCADGui import Part - if self.form.groupNewDocument.isChecked() or (FreeCAD.ActiveDocument is None): - doc = FreeCAD.newDocument() - if self.form.projectName.text(): - doc.Label = self.form.projectName.text() - FreeCAD.ActiveDocument = doc - if not FreeCAD.ActiveDocument: - FreeCAD.Console.PrintError( - translate("BIM", "No active document, aborting.") + "\n" - ) - site = None + vaxes = [] + haxes = [] outline = None - if self.form.groupSite.isChecked(): - site = Arch.makeSite() - site.Label = self.form.siteName.text() - site.Address = self.form.siteAddress.text() - site.Longitude = self.form.siteLongitude.value() - site.Latitude = self.form.siteLatitude.value() - if hasattr(site, "NorthDeviation"): - site.NorthDeviation = self.form.siteDeviation.value() - elif hasattr(site, "Declination"): - site.Declination = self.form.siteDeviation.value() - site.Elevation = FreeCAD.Units.Quantity( - self.form.siteElevation.text() - ).Value + outtext = None + human = None + grp = None + + if self.form.groupNewProject.isChecked(): + self.project = None + self.site = None + self.building = None + + # reading form values + buildingWidth = FreeCAD.Units.Quantity(self.form.buildingWidth.text()).Value + buildingLength = FreeCAD.Units.Quantity(self.form.buildingLength.text()).Value + distVAxes = FreeCAD.Units.Quantity(self.form.distVAxes.text()).Value + distHAxes = FreeCAD.Units.Quantity(self.form.distHAxes.text()).Value + levelHeight = FreeCAD.Units.Quantity(self.form.levelHeight.text()).Value + color = self.form.lineColor.property("color").getRgbF()[:3] + + # Document creation + doc = FreeCAD.ActiveDocument + if self.form.groupNewProject.isChecked(): + if self.form.radioNative1.isChecked() or \ + self.form.radioNative3.isChecked() or \ + (self.form.radioNative2.isChecked() and doc is None): + doc = FreeCAD.newDocument() + if self.form.projectName.text(): + doc.Label = self.form.projectName.text() + if doc: + FreeCAD.setActiveDocument(doc.Name) + + # Project creation + if self.form.groupNewProject.isChecked(): + from nativeifc import ifc_tools + if self.form.radioNative2.isChecked(): + self.project = ifc_tools.create_document_object(doc, silent=True) + if self.form.projectName.text(): + self.project.Label = self.form.projectName.text() + elif self.form.radioNative3.isChecked(): + self.project = ifc_tools.convert_document(doc, silent=True) + + # human human = None if self.form.addHumanFigure.isChecked(): humanshape = Part.Shape() @@ -135,47 +196,127 @@ class BIM_ProjectManager: human = FreeCAD.ActiveDocument.addObject("Part::Feature", "Human") human.Shape = humanshape human.Placement.move(FreeCAD.Vector(500, 500, 0)) + + # Site creation or edition + outline = None + if self.form.groupSite.isChecked(): + if not self.site: + self.site = Arch.makeSite() + if self.project: + from nativeifc import ifc_tools + self.site = ifc_tools.aggregate(self.site, self.project) + self.site.Label = self.form.siteName.text() + if hasattr(self.site,"Address"): + self.site.Address = self.form.siteAddress.text() + elif hasattr(self.site,"SiteAddress"): + self.site.SiteAddress = self.form.siteAddress.text() + if hasattr(self.site,"Longitude"): + self.site.Longitude = self.form.siteLongitude.value() + elif hasattr(self.site,"RefLongitude"): + self.site.RefLongitude = self.form.siteLongitude.value() + if hasattr(self.site,"Latitude"): + self.site.Latitude = self.form.siteLatitude.value() + elif hasattr(self.site,"RefLatitude"): + self.site.RefLatitude = self.form.siteLatitude.value() + if hasattr(self.site, "NorthDeviation"): + self.site.NorthDeviation = self.form.siteDeviation.value() + elif hasattr(self.site, "Declination"): + self.site.Declination = self.form.siteDeviation.value() + elev = FreeCAD.Units.Quantity(self.form.siteElevation.text()).Value + if hasattr(self.site, "Elevation"): + self.site.Elevation = elev + elif hasattr(self.site, "RefElevation"): + self.site.RefElevation = elev + + # Building creation or edition if self.form.groupBuilding.isChecked(): - building = Arch.makeBuilding() - if site: - site.Group = [building] - building.Label = self.form.buildingName.text() - building.BuildingType = self.form.buildingUse.currentText() - buildingWidth = FreeCAD.Units.Quantity(self.form.buildingWidth.text()).Value - buildingLength = FreeCAD.Units.Quantity( - self.form.buildingLength.text() - ).Value - distVAxes = FreeCAD.Units.Quantity(self.form.distVAxes.text()).Value - distHAxes = FreeCAD.Units.Quantity(self.form.distHAxes.text()).Value - levelHeight = FreeCAD.Units.Quantity(self.form.levelHeight.text()).Value - color = self.form.lineColor.property("color").getRgbF()[:3] - grp = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup") - grp.Label = translate("BIM", "Building Layout") - building.addObject(grp) + if not self.building: + self.building = Arch.makeBuilding() + if self.project: + from nativeifc import ifc_tools + if self.site: + self.building = ifc_tools.aggregate(self.building, self.site) + else: + self.building = ifc_tools.aggregate(self.building, self.project) + elif self.site: + self.site.Group = [self.building] + self.building.Label = self.form.buildingName.text() + if hasattr(self.building, "BuildingType"): + self.building.BuildingType = self.form.buildingUse.currentText() + + # Detecting existing contents + if self.building: + grp = [o for o in self.building.Group if o.Name.startswith("BuildingLayout")] + if grp: + grp = grp[0] + if grp: + axes = [o for o in grp.Group if o.Name.startswith("Axis")] + if axes: + for ax in axes: + if round(math.degrees(ax.Placement.Rotation.Angle),0) in [0, 180, 360]: + vaxes.append(ax) + elif round(math.degrees(ax.Placement.Rotation.Angle),0) in [90, 270]: + haxes.append(ax) + outline = [o for o in grp.Group if o.Name.startswith("Rectangle")] + if outline: + outline = outline[0] + human = [o for o in grp.Group if o.Name.startswith("Human")] + if human: + human = human[0] + else: + if self.form.addHumanFigure.isChecked() or self.form.countVAxes.value() \ + or self.form.countHAxes.value() or (buildingWidth and buildingLength): + grp = doc.addObject("App::DocumentObjectGroup", "BuildingLayout") + grp.Label = translate("BIM", "Building Layout") + if self.building: + if hasattr(self.building, "addObject"): + self.building.addObject(grp) + + # Human figure + if self.form.addHumanFigure.isChecked(): + if not human: + # TODO embed this + humanpath = os.path.join( + os.path.dirname(__file__), "geometry", "human figure.brep" + ) + if os.path.exists(humanpath): + humanshape = Part.Shape() + humanshape.importBrep(humanpath) + human = FreeCAD.ActiveDocument.addObject("Part::Feature", "Human") + human.Shape = humanshape + human.Placement.move(FreeCAD.Vector(500, 500, 0)) + if human: + grp.addObject(human) + # TODO: nativeifc + + # Outline if buildingWidth and buildingLength: - outline = Draft.makeRectangle(buildingLength, buildingWidth, face=False) - outline.Label = translate("BIM", "Building Outline") - outline.ViewObject.DrawStyle = "Dashed" + if not outline: + outline = Draft.makeRectangle(buildingLength, buildingWidth, face=False) + outline.Label = translate("BIM", "Building Outline") + outline.ViewObject.DrawStyle = "Dashed" + grp.addObject(outline) outline.ViewObject.LineColor = color outline.ViewObject.LineWidth = self.form.lineWidth.value() * 2 - grp.addObject(outline) - if self.form.buildingName.text(): - buildingname = self.form.buildingName.text() - if sys.version_info.major == 2: - buildingname = unicode(buildingname) - outtext = Draft.makeText( - [buildingname], - point=FreeCAD.Vector( - Draft.getParam("textheight", 0.20) * 0.3, - -Draft.getParam("textheight", 0.20) * 1.43, - 0, - ), - ) - outtext.Label = translate("BIM", "Building Label") - outtext.ViewObject.TextColor = color - grp.addObject(outtext) - if human: - grp.addObject(human) + outline.Length = buildingLength + outline.Height = buildingWidth + + # Label + if self.form.buildingName.text(): + buildingname = self.form.buildingName.text() + outtext = Draft.make_text( + [buildingname], + FreeCAD.Vector( + Draft.getParam("textheight", 0.20) * 0.3, + -Draft.getParam("textheight", 0.20) * 1.43, + 0, + ), + ) + outtext.Label = translate("BIM", "Building Label") + outtext.ViewObject.TextColor = color + grp.addObject(outtext) + + # Axes axisV = None if self.form.countVAxes.value() and distVAxes: axisV = Arch.makeAxis( @@ -187,18 +328,6 @@ class BIM_ProjectManager: axisV.ViewObject.FontSize = Draft.getParam("textheight", 0.20) axisV.ViewObject.BubbleSize = Draft.getParam("textheight", 0.20) * 1.43 axisV.ViewObject.LineColor = color - if outline: - axisV.setExpression("Length", outline.Name + ".Height * 1.1") - axisV.setExpression( - "Placement.Base.y", - outline.Name - + ".Placement.Base.y - " - + axisV.Name - + ".Length * 0.05", - ) - axisV.setExpression( - "Placement.Base.x", outline.Name + ".Placement.Base.x" - ) axisH = None if self.form.countHAxes.value() and distHAxes: axisH = Arch.makeAxis( @@ -212,18 +341,6 @@ class BIM_ProjectManager: axisH.ViewObject.BubbleSize = Draft.getParam("textheight", 0.20) * 1.43 axisH.Placement.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 90) axisH.ViewObject.LineColor = color - if outline: - axisH.setExpression("Length", outline.Name + ".Length * 1.1") - axisH.setExpression( - "Placement.Base.x", - outline.Name - + ".Placement.Base.x + " - + axisH.Name - + ".Length * 0.945", - ) - axisH.setExpression( - "Placement.Base.y", outline.Name + ".Placement.Base.y" - ) if axisV and axisH: axisG = Arch.makeAxisSystem([axisH, axisV]) axisG.Label = translate("BIM", "Axes") @@ -245,67 +362,41 @@ class BIM_ProjectManager: alabels.append(lev.Label) lev.Height = levelHeight lev.Placement.move(FreeCAD.Vector(0, 0, h)) - building.addObject(lev) - if self.form.levelsWP.isChecked(): - prx = Draft.makeWorkingPlaneProxy(FreeCAD.Placement()) - prx.Placement.move(FreeCAD.Vector(0, 0, h)) - lev.addObject(prx) + if self.project and self.building: + from nativeifc import ifc_tools + lev = ifc_tools.aggregate(lev, self.building) + elif self.building: + self.building.addObject(lev) h += levelHeight for group in groups: levGroup = FreeCAD.ActiveDocument.addObject( "App::DocumentObjectGroup" ) levGroup.Label = group - lev.addObject(levGroup) - if self.form.levelsAxis.isChecked(): - axisL = Arch.makeAxis( - num=self.form.countLevels.value(), - size=levelHeight, - name="laxis", - ) - axisL.Label = translate("BIM", "Level Axes") - axisL.ViewObject.BubblePosition = "None" - axisL.ViewObject.LineWidth = self.form.lineWidth.value() - axisL.ViewObject.FontSize = Draft.getParam("textheight", 0.20) - axisL.Placement.Rotation = FreeCAD.Rotation( - FreeCAD.Vector( - 0.577350269189626, -0.5773502691896257, 0.5773502691896257 - ), - 120, - ) - axisL.ViewObject.LineColor = color - axisL.ViewObject.LabelOffset.Rotation = FreeCAD.Rotation( - FreeCAD.Vector(1, 0, 0), 90 - ) - axisL.Labels = alabels - axisL.ViewObject.ShowLabel = True - if outline: - axisL.setExpression("Length", outline.Name + ".Length * 1.1") - axisL.setExpression( - "Placement.Base.x", - outline.Name - + ".Placement.Base.x + " - + axisL.Name - + ".Length * 0.945", - ) - axisL.setExpression( - "Placement.Base.y", outline.Name + ".Placement.Base.y" - ) - grp.addObject(axisL) - axisL.ViewObject.LabelOffset.Base = FreeCAD.Vector( - -axisL.Length.Value + Draft.getParam("textheight", 0.20) * 0.43, - 0, - Draft.getParam("textheight", 0.20) * 0.43, - ) - self.form.hide() + if self.project: + from nativeifc import ifc_tools + ifc_tools.aggregate(levGroup, lev) + else: + lev.addObject(levGroup) FreeCAD.ActiveDocument.recompute() + # fit zoom if outline: FreeCADGui.Selection.clearSelection() FreeCADGui.Selection.addSelection(outline) FreeCADGui.SendMsgToActiveView("ViewSelection") FreeCADGui.Selection.clearSelection() + # aggregate layout group + if self.building and grp: + if hasattr(self.building, "IfcClass"): + from nativeifc import ifc_tools + ifc_tools.aggregate(grp, self.building) + self.form.hide() + FreeCAD.ActiveDocument.recompute() if hasattr(FreeCADGui, "Snapper"): FreeCADGui.Snapper.show() + if self.form.radioNative3.isChecked(): + from nativeifc import ifc_status + ifc_status.set_button(True,True) return True def addGroup(self): @@ -403,6 +494,7 @@ class BIM_ProjectManager: import Arch from PySide import QtGui + preset = self.form.presets.itemText(preset) pfile = os.path.join(FreeCAD.getUserAppDataDir(), "BIM", preset + ".txt") if os.path.exists(pfile): f = open(pfile, "r") diff --git a/src/Mod/BIM/importers/exportIFC.py b/src/Mod/BIM/importers/exportIFC.py index fa65264b90..12e2a10933 100644 --- a/src/Mod/BIM/importers/exportIFC.py +++ b/src/Mod/BIM/importers/exportIFC.py @@ -2423,7 +2423,8 @@ def getUID(obj,preferences): obj.IfcData = d if hasattr(obj, "GlobalId"): obj.GlobalId = uid - uids.append(uid) + if "uids" in globals(): + uids.append(uid) return uid @@ -2499,6 +2500,11 @@ def create_annotation(anno, ifcfile, context, history, preferences): """Creates an annotation object""" global curvestyles, ifcbin + reps = [] + repid = "Annotation" + reptype = "Annotation2D" + description = getattr(anno, "Description", None) + # uses global ifcbin, curvestyles objectType = None ovc = None zvc = None @@ -2506,7 +2512,6 @@ def create_annotation(anno, ifcfile, context, history, preferences): reps = [] repid = "Annotation" reptype = "Annotation2D" - description = getattr(anno, "Description", None) if anno.isDerivedFrom("Part::Feature"): if Draft.getType(anno) == "Hatch": objectType = "HATCH" @@ -2525,15 +2530,8 @@ def create_annotation(anno, ifcfile, context, history, preferences): axes.append(axis) if axes: if len(axes) > 1: - xvc = ifcbin.createIfcDirection((1.0,0.0,0.0)) - zvc = ifcbin.createIfcDirection((0.0,0.0,1.0)) - ovc = ifcbin.createIfcCartesianPoint((0.0,0.0,0.0)) - gpl = ifcbin.createIfcAxis2Placement3D(ovc,zvc,xvc) - plac = ifcbin.createIfcLocalPlacement(gpl) - grid = ifcfile.createIfcGrid(uid,history,name,description,None,plac,None,axes,None,None) - return grid - else: - return axes[0] + print("DEBUG: exportIFC.create_annotation: Cannot create more thna one axis",anno.Label) + return axes[0] 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 ba902a11d1..ba81c6d5fd 100644 --- a/src/Mod/BIM/nativeifc/ifc_export.py +++ b/src/Mod/BIM/nativeifc/ifc_export.py @@ -13,10 +13,10 @@ # * 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 * +#* 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 * # * * # *************************************************************************** @@ -196,6 +196,8 @@ def get_object_type(ifcentity, objecttype=None): objecttype = "axis" elif ifcentity.is_a("IfcControl"): objecttype = "schedule" + elif ifcentity.is_a() in ["IfcBuilding", "IfcBuildingStorey"]: + objecttype = "buildingpart" return objecttype diff --git a/src/Mod/BIM/nativeifc/ifc_objects.py b/src/Mod/BIM/nativeifc/ifc_objects.py index f49ec19080..be202c7012 100644 --- a/src/Mod/BIM/nativeifc/ifc_objects.py +++ b/src/Mod/BIM/nativeifc/ifc_objects.py @@ -28,7 +28,7 @@ translate = FreeCAD.Qt.translate # the property groups below should not be treated as psets NON_PSETS = ["Base", "IFC", "", "Geometry", "Dimension", "Linear/radial dimension", - "SectionPlane", "Axis", "PhysicalProperties"] + "SectionPlane", "Axis", "PhysicalProperties", "BuildingPart", "IFC Attributes"] class ifc_object: """Base class for all IFC-based objects""" @@ -325,8 +325,6 @@ class ifc_object: ifc_types.edit_type(obj) - - def edit_quantity(self, obj, prop): """Edits the given quantity""" pass # TODO implement diff --git a/src/Mod/BIM/nativeifc/ifc_observer.py b/src/Mod/BIM/nativeifc/ifc_observer.py index aba3de0252..2aa933d905 100644 --- a/src/Mod/BIM/nativeifc/ifc_observer.py +++ b/src/Mod/BIM/nativeifc/ifc_observer.py @@ -81,10 +81,14 @@ class ifc_observer: def slotChangedDocument(self, doc, prop): """Watch document IFC properties""" + # only look at locked IFC documents + if not "IfcFilePath" in doc.PropertiesList: + return + from nativeifc import ifc_tools # lazy import from nativeifc import ifc_status - if prop == "Schema" and "IfcFilePath" in doc.PropertiesList: + if prop == "Schema": schema = doc.Schema ifcfile = ifc_tools.get_ifcfile(doc) if ifcfile: @@ -101,6 +105,10 @@ class ifc_observer: ] if len(child) == 1: child[0].StepId = new_id + elif prop == "Label": + ifcfile = ifc_tools.get_ifcfile(doc) + project = ifc_tools.get_ifc_element(doc) + ifc_tools.set_attribute(ifcfile, project, "Name", doc.Label) def slotCreatedObject(self, obj): """If this is an IFC document, turn the object into IFC""" diff --git a/src/Mod/BIM/nativeifc/ifc_tools.py b/src/Mod/BIM/nativeifc/ifc_tools.py index 2cffb9002e..27ac90e750 100644 --- a/src/Mod/BIM/nativeifc/ifc_tools.py +++ b/src/Mod/BIM/nativeifc/ifc_tools.py @@ -61,6 +61,7 @@ from nativeifc import ifc_export from nativeifc import ifc_psets from draftviewproviders import view_layer +import ArchBuildingPart from PySide import QtCore SCALE = 1000.0 # IfcOpenShell works in meters, FreeCAD works in mm @@ -396,7 +397,7 @@ def assign_groups(children, ifcfile=None): def get_children( - obj, ifcfile=None, only_structure=False, assemblies=True, expand=False, iftype=None + obj, ifcfile=None, only_structure=False, assemblies=True, expand=False, ifctype=None ): """Returns the direct descendants of an object""" @@ -417,7 +418,7 @@ def get_children( result = filter_elements( children, ifcfile, expand=expand, spaces=True, assemblies=assemblies ) - if iftype: + if ifctype: result = [r for r in result if r.is_a(ifctype)] return result @@ -524,6 +525,7 @@ def add_object(document, otype=None, oname="IfcObject"): 'sectionplane', 'axis', 'schedule' + 'buildingpart' or anything else for a standard IFC object""" if not document: @@ -572,6 +574,16 @@ def add_object(document, otype=None, oname="IfcObject"): proxy = ifc_objects.ifc_object(otype) vproxy = ifc_viewproviders.ifc_vp_document() obj = document.addObject("Part::FeaturePython", oname, proxy, vproxy, False) + elif otype == "buildingpart": + obj = Arch.makeBuildingPart() + if obj.ViewObject: + obj.ViewObject.ShowLevel = False + obj.ViewObject.ShowLabel = False + obj.ViewObject.Proxy = ifc_viewproviders.ifc_vp_buildingpart(obj.ViewObject) + for p in obj.PropertiesList: + if obj.getGroupOfProperty(p) in ["BuildingPart","IFC Attributes","Children"]: + obj.removeProperty(p) + obj.Proxy = ifc_objects.ifc_object(otype) else: # default case, standard IFC object proxy = ifc_objects.ifc_object(otype) vproxy = ifc_viewproviders.ifc_vp_object() @@ -701,6 +713,12 @@ def add_properties( setattr(obj, attr, [value]) setattr(obj, attr, value) setattr(obj, attr, items) + elif attr in ["RefLongitude", "RefLatitude"]: + obj.addProperty("App::PropertyFloat", attr, "IFC") + if value is not None: + # convert from list of 4 ints + value = value[0] + value[1]/60. + value[2]/3600. + value[3]/3600.e6 + setattr(obj, attr, value) else: if attr not in obj.PropertiesList: obj.addProperty("App::PropertyString", attr, "IFC") @@ -717,6 +735,10 @@ def add_properties( obj.setExpression("CustomText", "AxisTag") if "Length" not in obj.PropertiesList: obj.addProperty("App::PropertyLength","Length","Axis") + 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) obj.Length = axisdata[1] elif ifcentity.is_a("IfcAnnotation"): sectionplane = ifc_export.get_sectionplane(ifcentity) @@ -929,6 +951,12 @@ def set_attribute(ifcfile, element, attribute, value): product = api_run(cmd, ifcfile, product=element, ifc_class=value) # TODO fix attributes return product + if attribute in ["RefLongitude", "RefLatitude"]: + c = [int(value)] + c.append(int((value - c[0]) * 60)) + c.append(int(((value - c[0]) * 60 - c[1]) * 60)) + c.append(int((((value - c[0]) * 60 - c[1]) * 60 - c[2]) * 1.e6)) + value = c cmd = "attribute.edit_attributes" attribs = {attribute: value} if hasattr(element, attribute): @@ -1226,6 +1254,9 @@ def aggregate(obj, parent, mode=None): elif hasattr(child,"Hosts") and obj in child.Hosts: #op = create_product(child, newobj, ifcfile, ifcclass="IfcOpeningElement") aggregate(child, newobj) + for child in getattr(obj, "Group", []): + if newobj.IfcClass == "IfcGroup" and child in obj.Group: + aggregate(child, newobj) delete = not (PARAMS.GetBool("KeepAggregated", False)) if new and delete and base: obj.Document.removeObject(base.Name) @@ -1352,10 +1383,10 @@ def create_relationship(old_obj, obj, parent, element, ifcfile, mode=None): if hasattr(old_par, "Group") and old_obj in old_par.Group: old_par.Group = [o for o in old_par.Group if o != old_obj] try: - api_run("spatial.unassign_container", ifcfile, products=[element]) + uprel = api_run("spatial.unassign_container", ifcfile, products=[element]) except: # older version of IfcOpenShell - api_run("spatial.unassign_container", ifcfile, product=element) + uprel = api_run("spatial.unassign_container", ifcfile, product=element) if element.is_a("IfcOpeningElement"): uprel = api_run( "void.add_opening", diff --git a/src/Mod/BIM/nativeifc/ifc_viewproviders.py b/src/Mod/BIM/nativeifc/ifc_viewproviders.py index 77f99c20a9..4d3e301b77 100644 --- a/src/Mod/BIM/nativeifc/ifc_viewproviders.py +++ b/src/Mod/BIM/nativeifc/ifc_viewproviders.py @@ -22,6 +22,7 @@ import FreeCADGui +import ArchBuildingPart class ifc_vp_object: @@ -70,22 +71,7 @@ class ifc_vp_object: obj.ViewObject.DiffuseColor = colors def getIcon(self): - from PySide import QtCore, QtGui # lazy import - - rclass = self.Object.IfcClass.replace("StandardCase","") - rclass = self.Object.IfcClass.replace("Type","") - ifcicon = ":/icons/IFC/" + rclass + ".svg" - if QtCore.QFile.exists(ifcicon): - if getattr(self, "ifcclass", "") != rclass: - self.ifcclass = rclass - self.ifcicon = overlay(ifcicon, ":/icons/IFC.svg") - return getattr(self, "ifcicon", overlay(ifcicon, ":/icons/IFC.svg")) - elif self.Object.IfcClass == "IfcGroup": - return QtGui.QIcon.fromTheme("folder", QtGui.QIcon(":/icons/folder.svg")) - elif self.Object.ShapeMode == "Shape": - return ":/icons/IFC_object.svg" - else: - return ":/icons/IFC_mesh.svg" + return get_icon(self) def claimChildren(self): if hasattr(self.Object, "Group"): @@ -632,6 +618,13 @@ class ifc_vp_material: self.Object.Document.recompute() +class ifc_vp_buildingpart(ifc_vp_object, ArchBuildingPart.ViewProviderBuildingPart): + """A vp that inherits the Arch BuildingPart vp, but keeps aggregating properties of ifc vp""" + + def __init__(self, vobj): + ArchBuildingPart.ViewProviderBuildingPart.__init__(self,vobj) + + def overlay(icon1, icon2): """Overlays icon2 onto icon1""" @@ -678,3 +671,27 @@ def get_filepath(project): project.IfcFilePath = sf return sf return None + + +def get_icon(vp): + """Returns an icon for a view provider""" + + from PySide import QtCore, QtGui # lazy import + + if hasattr(vp, "Object"): + if hasattr(vp.Object, "IfcClass"): + rclass = vp.Object.IfcClass.replace("StandardCase","") + rclass = vp.Object.IfcClass.replace("Type","") + ifcicon = ":/icons/IFC/" + rclass + ".svg" + if QtCore.QFile.exists(ifcicon): + if getattr(self, "ifcclass", "") != rclass: + vp.ifcclass = rclass + vp.ifcicon = overlay(ifcicon, ":/icons/IFC.svg") + return getattr(vp, "ifcicon", overlay(ifcicon, ":/icons/IFC.svg")) + elif vp.Object.IfcClass == "IfcGroup": + return QtGui.QIcon.fromTheme("folder", QtGui.QIcon(":/icons/folder.svg")) + elif vp.Object.ShapeMode == "Shape": + return ":/icons/IFC_object.svg" + else: + return ":/icons/IFC_mesh.svg" + return ":/icons/IFC_object.svg"