diff --git a/ifc_import.py b/ifc_import.py index 05a96e5..e6553c3 100644 --- a/ifc_import.py +++ b/ifc_import.py @@ -45,7 +45,7 @@ def open(filename): doc = FreeCAD.newDocument() doc.Label = name FreeCAD.setActiveDocument(doc.Name) - insert(filename, doc.Name) + insert(filename, doc.Name, singledoc=True) del FreeCAD.IsOpeningIFC return doc diff --git a/ifc_observer.py b/ifc_observer.py index ed1819a..31ea792 100644 --- a/ifc_observer.py +++ b/ifc_observer.py @@ -69,9 +69,10 @@ def slotDeletedObject(self, obj): def slotChangedDocument(self, doc, prop): """Watch document IFC properties""" - if prop == "Schema" and "IfcFilePath" in doc.PropertiesList: - import ifc_tools # lazy import + import ifc_tools # lazy import + import ifc_status + if prop == "Schema" and "IfcFilePath" in doc.PropertiesList: schema = doc.Schema ifcfile = ifc_tools.get_ifcfile(doc) if ifcfile: @@ -88,6 +89,9 @@ def slotChangedDocument(self, doc, prop): ] if len(child) == 1: child[0].StepId = new_id + ifc_status.toggle_lock(True) + else: + ifc_status.toggle_lock(False) def slotCreatedObject(self, obj): """If this is an IFC document, turn the object into IFC""" @@ -103,26 +107,16 @@ def slotCreatedObject(self, obj): QtCore.QTimer.singleShot(100, self.convert) def slotActivateDocument(self, doc): - """Check if we need to display a ghost""" + """Check if we need to lock""" from PySide2 import QtCore # lazy loading + import ifc_status - if hasattr(doc, "Proxy"): - if hasattr(doc.Proxy, "ifcfile"): - # this runs when loading a file - if getattr(doc, "Objects", ""): - for obj in doc.Objects: - if getattr(obj, "ShapeMode", None) == "Coin": - obj.Proxy.cached = True - QtCore.QTimer.singleShot(100, obj.touch) - QtCore.QTimer.singleShot(100, doc.recompute) - QtCore.QTimer.singleShot(100, self.fit_all) - else: - if not hasattr(doc.Proxy, "ghost"): - import ifc_generator - - ifc_generator.create_ghost(doc) + if hasattr(doc, "IfcFilePath"): + ifc_status.toggle_lock(True) else: + ifc_status.toggle_lock(False) + if not hasattr(doc, "Proxy"): # this is a new file, wait a bit to make sure all components are populated QtCore.QTimer.singleShot(1000, self.propose_conversion) @@ -193,19 +187,20 @@ def convert(self): return del self.docname del self.objname - if obj.isDerivedFrom("Part::Feature"): - if "IfcType" in obj.PropertiesList: - print("Converting", obj.Label, "to IFC") - import ifc_tools # lazy loading - import ifc_geometry # lazy loading + if obj.isDerivedFrom("Part::Feature") or "IfcType" in obj.PropertiesList: + FreeCAD.Console.PrintLog("Converting" + obj.Label + "to IFC\n") + import ifc_tools # lazy loading + import ifc_geometry # lazy loading - newobj = ifc_tools.aggregate(obj, doc) - ifc_geometry.add_geom_properties(newobj) - doc.recompute() + newobj = ifc_tools.aggregate(obj, doc) + ifc_geometry.add_geom_properties(newobj) + doc.recompute() def propose_conversion(self): """Propose a conversion of the current document""" + import ifc_status # lazy loading + doc = FreeCAD.ActiveDocument if not getattr(FreeCAD, "IsOpeningIFC", False): if not hasattr(doc, "Proxy"): @@ -226,6 +221,7 @@ def propose_conversion(self): ) return else: + ifc_status.toggle_lock(False) return d = os.path.dirname(__file__) dlg = FreeCADGui.PySideUic.loadUi( @@ -250,6 +246,7 @@ def convert_document(self): """Converts the active document""" import ifc_tools # lazy loading + import ifc_status doc = FreeCAD.ActiveDocument ifc_tools.convert_document(doc, strategy=2, silent=True) @@ -259,4 +256,5 @@ def convert_document(self): site = ifc_tools.aggregate(Arch.makeSite(), doc) building = ifc_tools.aggregate(Arch.makeBuilding(), site) storey = ifc_tools.aggregate(Arch.makeFloor(), building) + ifc_status.toggle_lock(True) doc.recompute() diff --git a/ifc_status.py b/ifc_status.py new file mode 100644 index 0000000..9fd83f0 --- /dev/null +++ b/ifc_status.py @@ -0,0 +1,187 @@ +# -*- coding: utf8 -*- +# *************************************************************************** +# * * +# * Copyright (c) 2024 Yorik van Havre * +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License (GPL) * +# * as published by the Free Software Foundation; either version 3 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 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 * +# * * +# *************************************************************************** + +"""This contains nativeifc status widgets and functionality""" + + +import os +import FreeCAD +import FreeCADGui + + +translate = FreeCAD.Qt.translate +params = FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Mod/NativeIFC") +text_on = translate("BIM", "Strict IFC mode is ON (all objects are IFC)") +text_off = translate("BIM", "Strict IFC mode is OFF (IFC and non-IFC objects allowed)") + + +def set_status_widget(statuswidget): + """Adds the needed controls to the status bar""" + + from PySide import QtGui # lazy import + + # lock button + lock_button = QtGui.QAction() + path = os.path.dirname(os.path.dirname(__file__)) + icon = QtGui.QIcon(os.path.join(path, "icons", "IFC.svg")) + lock_button.setIcon(icon) + lock_button.setCheckable(True) + doc = FreeCAD.ActiveDocument + if doc and "IfcFilePath" in doc.PropertiesList: + checked = True + else: + checked = params.GetBool("SingleDoc", False) + lock_button.setChecked(checked) + if checked: + lock_button.setText("🔒") + lock_button.setToolTip(text_on) + else: + lock_button.setText(" ") + lock_button.setToolTip(text_off) + lock_button.triggered.connect(do_lock) + lock_button.triggered.connect(toggle_lock) + statuswidget.addAction(lock_button) + statuswidget.lock_button = lock_button + + +def toggle_lock(checked): + """Sets the lock button on/off""" + + from PySide import QtGui # lazy loading + + mw = FreeCADGui.getMainWindow() + statuswidget = mw.findChild(QtGui.QToolBar, "BIMStatusWidget") + if hasattr(statuswidget, "lock_button"): + if checked: + statuswidget.lock_button.setChecked(True) + statuswidget.lock_button.setText("🔒") + statuswidget.lock_button.setToolTip(text_on) + else: + statuswidget.lock_button.setChecked(False) + statuswidget.lock_button.setText(" ") + statuswidget.lock_button.setToolTip(text_off) + + +def do_lock(checked): + """Locks or unlocks the document""" + + if checked: + lock_document() + else: + unlock_document() + + +def unlock_document(): + """Unlocks the active document""" + + import ifc_tools # lazy loading + + doc = FreeCAD.ActiveDocument + if "IfcFilePath" in doc.PropertiesList: + # this is a locked document + doc.openTransaction("Unlock document") + children = [ifc_tools.get_object(o) for o in ifc_tools.get_children(doc)] + if children: + project = ifc_tools.create_document_object(doc, filename = doc.IfcFilePath, silent = True) + project.Group = children + props = ["IfcFilePath", "Modified", "Proxy", "Schema"] + props += [p for p in doc.PropertiesList if doc.getGroupOfProperty(p) == "IFC"] + for prop in props: + doc.removeProperty(prop) + doc.commitTransaction() + doc.recompute() + + +def lock_document(): + """Locks the active document""" + + import ifc_tools # lazy loading + import exportIFC + import ifc_geometry + + doc = FreeCAD.ActiveDocument + products = [] + spatial = [] + if "IfcFilePath" not in doc.PropertiesList: + # this is not a locked document + projects = [o for o in doc.Objects if getattr(o,"Class",None) == "IfcProject"] + if len(projects) == 1: + # 1 there is a project already + project = projects[0] + children = project.OutListRecursive + rest = [o for o in doc.Objects if o not in children and o != project] + doc.openTransaction("Lock document") + ifc_tools.convert_document(doc, filename=project.IfcFilePath, strategy=3, silent=True) + ifcfile = doc.Proxy.ifcfile + if rest: + # 1b some objects are outside + objs = find_toplevel(rest) + prefs, context = ifc_tools.get_export_preferences(ifcfile) + products = exportIFC.export(objs, ifcfile, preferences=prefs) + for product in products.values(): + if not getattr(product, "ContainedInStructure", None): + if not getattr(product, "FillsVoids", None): + if not getattr(product, "VoidsElements", None): + if not getattr(product, "Decomposes", None): + new = ifc_tools.create_object(product, doc, ifcfile) + children = ifc_tools.create_children(new, ifcfile, recursive=True) + for o in [new] + children: + ifc_geometry.add_geom_properties(o) + for n in [o.Name for o in rest]: + doc.removeObject(n) + else: + # 1a all objects are already inside a project + pass + doc.removeObject(project.Name) + doc.commitTransaction() + doc.recompute() + elif len(projects) > 1: + # 2 there is more than one project + FreeCAD.Console.PrintError("Unable to lock this document because it contains several IFC documents\n") + toggle_lock(False) + else: + # 3 there is no project + doc.openTransaction("Lock document") + ifc_tools.convert_document(doc, silent=True) + ifcfile = doc.Proxy.ifcfile + objs = find_toplevel(doc.Objects) + exportIFC.export(objs, ifcfile) + for n in [o.Name for o in doc.Objects]: + doc.removeObject(n) + ifc_tools.create_children(doc, ifcfile, recursive=True) + doc.commitTransaction() + doc.recompute() + + +def find_toplevel(objs): + """Finds the top-level objects from the list""" + + # filter out any object that depend on another from the list + nobjs = [] + for obj in objs: + for parent in obj.InListRecursive: + if parent in objs: + break + else: + nobjs.append(obj) + return nobjs diff --git a/ifc_tools.py b/ifc_tools.py index 4962ea5..f4cea8f 100644 --- a/ifc_tools.py +++ b/ifc_tools.py @@ -49,6 +49,7 @@ import ifc_import import ifc_layers import ifc_generator +import ifc_status SCALE = 1000.0 # IfcOpenShell works in meters, FreeCAD works in mm SHORT = False # If True, only Step ID attribute is created @@ -104,6 +105,7 @@ def create_document_object( site = aggregate(Arch.makeSite(), obj) building = aggregate(Arch.makeBuilding(), site) storey = aggregate(Arch.makeFloor(), building) + ifc_status.toggle_lock(False) return obj @@ -115,25 +117,30 @@ def convert_document(document, filename=None, shapemode=0, strategy=0, silent=Fa 1 = coin only 2 = no representation strategy: 0 = only root object - 1 = only bbuilding structure, + 1 = only bbuilding structure 2 = all children + 3 = no children """ - document.addProperty("App::PropertyPythonObject", "Proxy") + if not "Proxy" in document.PropertiesList: + document.addProperty("App::PropertyPythonObject", "Proxy") document.setPropertyStatus("Proxy", "Transient") document.Proxy = ifc_objects.document_object() ifcfile, project, full = setup_project(document, filename, shapemode, silent) if strategy == 0: - ifc_generator.create_ghost(document, ifcfile, project) + create_children(document, ifcfile, recursive=False) elif strategy == 1: create_children(document, ifcfile, recursive=True, only_structure=True) elif strategy == 2: create_children(document, ifcfile, recursive=True, assemblies=False) + elif strategy == 3: + pass # create default structure if full: site = aggregate(Arch.makeSite(), document) building = aggregate(Arch.makeBuilding(), site) storey = aggregate(Arch.makeFloor(), building) + ifc_status.toggle_lock(True) return document @@ -143,8 +150,10 @@ def setup_project(proj, filename, shapemode, silent): full = False d = "The path to the linked IFC file" - proj.addProperty("App::PropertyFile", "IfcFilePath", "Base", d) - proj.addProperty("App::PropertyBool", "Modified", "Base") + if not "IfcFilePath" in proj.PropertiesList: + proj.addProperty("App::PropertyFile", "IfcFilePath", "Base", d) + if not "Modified" in proj.PropertiesList: + proj.addProperty("App::PropertyBool", "Modified", "Base") proj.setPropertyStatus("Modified", "Hidden") if filename: # opening existing file @@ -160,7 +169,8 @@ def setup_project(proj, filename, shapemode, silent): # https://blenderbim.org/docs-python/autoapi/ifcopenshell/api/owner/create_owner_history/index.html proj.Proxy.ifcfile = ifcfile add_properties(proj, ifcfile, project, shapemode=shapemode) - proj.addProperty("App::PropertyEnumeration", "Schema", "Base") + if not "Schema" in proj.PropertiesList: + proj.addProperty("App::PropertyEnumeration", "Schema", "Base") # bug in FreeCAD - to avoid a crash, pre-populate the enum with one value proj.Schema = [ifcfile.wrapped_data.schema_name()] proj.Schema = ifcfile.wrapped_data.schema_name() @@ -268,11 +278,18 @@ def create_children( ): """Creates a hierarchy of objects under an object""" + def get_parent_objects(parent): + proj = get_project(parent) + if hasattr(proj, "OutListRecursive"): + return proj.OutListRecursive + elif hasattr(proj, "Objects"): + return proj.Objects + def create_child(parent, element): subresult = [] # do not create if a child with same stepid already exists if not element.id() in [ - getattr(c, "StepId", 0) for c in getattr(parent, "Group", []) + getattr(c, "StepId", 0) for c in get_parent_objects(parent) ]: doc = getattr(parent, "Document", parent) mode = getattr(parent, "ShapeMode", "Coin") @@ -441,8 +458,9 @@ def add_object(document, otype=None, oname="IfcObject"): obj = document.addObject(ftype, oname, proxy, vp, False) if obj.ViewObject and otype == "layer": from draftviewproviders import view_layer # lazy import - view_layer.ViewProviderLayer(obj.ViewObject) + obj.ViewObject.addProperty("App::PropertyBool","HideChildren","Layer") + obj.ViewObject.HideChildren = True return obj @@ -512,6 +530,10 @@ def add_properties( obj.addProperty("App::PropertyString", "IfcClass", "IFC") obj.setPropertyStatus("IfcClass", "Hidden") setattr(obj, "IfcClass", value) + elif attr_def and "IfcLengthMeasure" in str(attr_def.type_of_attribute()): + obj.addProperty("App::PropertyDistance", attr, "IFC") + if value: + setattr(obj, attr, value*get_scale(ifcfile)) elif isinstance(value, int): if attr not in obj.PropertiesList: obj.addProperty("App::PropertyInteger", attr, "IFC") @@ -817,6 +839,14 @@ def get_ios_matrix(m): return rmat +def get_scale(ifcfile): + """Returns the scale factor to convert any file length to mm""" + + scale = ifcopenshell.util.unit.calculate_unit_scale(ifcfile) + # the above lines yields meter -> file unit scale factor. We need mm + return 0.001 / scale + + def set_placement(obj): """Updates the internal IFC placement according to the object placement""" @@ -827,10 +857,7 @@ def set_placement(obj): print("DEBUG: No ifc file for object", obj.Label, "Aborting") element = get_ifc_element(obj) placement = FreeCAD.Placement(obj.Placement) - scale = ifcopenshell.util.unit.calculate_unit_scale(ifcfile) - # the above lines yields meter -> file unit scale factor. We need mm - scale = 0.001 / scale - placement.Base = FreeCAD.Vector(placement.Base).multiply(scale) + placement.Base = FreeCAD.Vector(placement.Base).multiply(get_scale(ifcfile)) new_matrix = get_ios_matrix(placement) old_matrix = ifcopenshell.util.placement.get_local_placement( element.ObjectPlacement diff --git a/ifc_tree.py b/ifc_tree.py index a5242b9..2d8d92f 100644 --- a/ifc_tree.py +++ b/ifc_tree.py @@ -50,6 +50,10 @@ def get_geometry_tree(element, prefix=""): result.extend(get_geometry_tree(element.SweptArea, prefix)) elif element.is_a("IfcArbitraryClosedProfileDef"): result.extend(get_geometry_tree(element.OuterCurve, prefix)) + elif element.is_a("IfcArbitraryProfileDefWithVoids"): + result.extend(get_geometry_tree(element.OuterCurve, prefix)) + for inn in element.InnerCurves: + result.extend(get_geometry_tree(inn, prefix)) elif element.is_a("IfcMappedItem"): result.extend(get_geometry_tree(element.MappingSource[1], prefix)) elif element.is_a("IfcBooleanClippingResult"):