From bd916d46cd6a48d57d94886cee3123291e53b7b7 Mon Sep 17 00:00:00 2001 From: Licini Date: Mon, 3 Mar 2025 11:08:40 +0100 Subject: [PATCH 1/3] drafts --- src/compas_ifc/entities/buildingelements.py | 84 +++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 src/compas_ifc/entities/buildingelements.py diff --git a/src/compas_ifc/entities/buildingelements.py b/src/compas_ifc/entities/buildingelements.py new file mode 100644 index 0000000..93d34e9 --- /dev/null +++ b/src/compas_ifc/entities/buildingelements.py @@ -0,0 +1,84 @@ +from compas_model.models import Model +from compas_model.elements import Element + + +# Use pydantic class for auto-generated IFC classes?? + + + +class BuildingModel(Model): + # This class will maintain and update a IFC file underneath. + + def __init__(self, **kwargs): + + # The model will start an empty IFC file if not provided. + super().__init__(**kwargs) + self._building_elements = [] + self._ifc_project = None # The underlying IFC project (we don't treat it as element here) + + @classmethod + def from_ifc(self, ifc_file): + pass + + @classmethod + def from_template(self, building_count=1, storey_count=1): + # Do we support multiple sites? + pass + + + +class BuildingElement(Element): + def __init__(self, **kwargs): + super().__init__(**kwargs) + + self._ifc_entity = None # Read-only, underlying IFC entity. + self._ifc_class = None # Read-only, IFC class name, maybe redundant. + + + self.ifc_attributes = {} # View object, mimics dict but will update the underlying IFC entity when changed, it also validates againts the IFC schema. + self.ifc_property_sets = {} # View object, the parametric definition will be added here too. + + # Common attributes of all building elements. + self._name = None + self._description = None + + self.geometry = None + # Option1: "Baked geometry", Brep (Tessellated or Full). + # Option2a: "Parametric geometry", this class inherrits from another like `Beam`, this might branching out to too many classes. + # Option2b: "Parametric geometry", this refers to a class like `Beam` as attribute, this makes the class structure convoluted. + + # The question becomes: + # 1.should the concept of "geometry" be detached from "Element"? which is the case of IFC. + # 2.Data flow-wise, the data should always flow from Model to IFC, we will not likely modify geometry if IFC elements, but what happens when we read it back? + # 3.How do we convert baked geometry back to a parametric definition? (we save the parametric definition in PSet) + # 4.If we want to make update, do we convert the whole model back to a parametric (Block) model, or should we update the BuildingModel in-place? + + + self.material = None # There needs to be some mapping mechanism between the material class and Qtos. + + + @classmethod + def from_ifc(cls, ifc_entity): + # From a raw IFC entity fill-in all the attributes and setup the links etc. + pass + + def add(self, **kwargs): + super().add(**kwargs) + # also update the IFC file + pass + + def remove(self, **kwargs): + super().remove(**kwargs) + # also update the IFC file + pass + + @property + def transformation(self): + # Get the transformation matrix of the element + pass + + @transformation.setter + def transformation(self, transformation): + # Set the transformation matrix of the element, will also update the placement of the underlying IFC entity. + pass + From d25ee5a20c1217be6a820a8cb97bc9cbb1b2be41 Mon Sep 17 00:00:00 2001 From: Li Chen Date: Tue, 11 Mar 2025 12:41:21 +0100 Subject: [PATCH 2/3] more notes --- src/compas_ifc/entities/buildingelements.py | 53 +++++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/src/compas_ifc/entities/buildingelements.py b/src/compas_ifc/entities/buildingelements.py index 93d34e9..49c776b 100644 --- a/src/compas_ifc/entities/buildingelements.py +++ b/src/compas_ifc/entities/buildingelements.py @@ -2,9 +2,6 @@ from compas_model.elements import Element -# Use pydantic class for auto-generated IFC classes?? - - class BuildingModel(Model): # This class will maintain and update a IFC file underneath. @@ -13,8 +10,8 @@ def __init__(self, **kwargs): # The model will start an empty IFC file if not provided. super().__init__(**kwargs) - self._building_elements = [] self._ifc_project = None # The underlying IFC project (we don't treat it as element here) + # How about site? Maybe also integrate it here. @classmethod def from_ifc(self, ifc_file): @@ -25,6 +22,24 @@ def from_template(self, building_count=1, storey_count=1): # Do we support multiple sites? pass + @classmethod + def from_blockmodel(self, blockmodel, mapping=None): + # Create a building model from a blockmodel. + # mapping is a dictionary that maps the blockmodel elements to IFC elements. + # TODO: think about the role of different models. + pass + + # To ... + + def add(self, **kwargs): + super().add(**kwargs) + # also update the IFC file + pass + + def remove(self, **kwargs): + super().remove(**kwargs) + # also update the IFC file + pass class BuildingElement(Element): @@ -33,10 +48,18 @@ def __init__(self, **kwargs): self._ifc_entity = None # Read-only, underlying IFC entity. self._ifc_class = None # Read-only, IFC class name, maybe redundant. + # There are 30 IfcBuiling(/t)Elements, but they only add one or two more attributes and all of them OPTIONAL. + # The value of adding these classes are questionable. + # The definition of these classes can be sometimes conflited between fields, for example Beams in architecrual and structrual context. + # At least in IFC cases, these classification are perhaps better expressed as "Labels" instead of branched-out classes. + self.ifc_attributes = {} # View object, mimics dict but will update the underlying IFC entity when changed, it also validates againts the IFC schema. self.ifc_property_sets = {} # View object, the parametric definition will be added here too. + # Should we abstract away these two, and have them managed together by a single JSON schema? + # The artificial division between attributes and property sets is also a reason of "rogue customizetion". It also confuse with Python's interperation of these two words. + # The combined name being? # Common attributes of all building elements. self._name = None @@ -53,8 +76,17 @@ def __init__(self, **kwargs): # 3.How do we convert baked geometry back to a parametric definition? (we save the parametric definition in PSet) # 4.If we want to make update, do we convert the whole model back to a parametric (Block) model, or should we update the BuildingModel in-place? + # Prefered: single class with consistant APIs that handles mapping to/from IFC classes. + + self.material = None # There needs to be some mapping mechanism between the material class and Qtos in IFC. + + # How about storeys? Maybe as a calculated property of the building elements? + - self.material = None # There needs to be some mapping mechanism between the material class and Qtos. + # links to parents + self.storey = None + self.building = None + self.model = None @classmethod @@ -62,6 +94,12 @@ def from_ifc(cls, ifc_entity): # From a raw IFC entity fill-in all the attributes and setup the links etc. pass + @classmethod + def from_blockelement(cls, blockelement, mapping=None): + # Create a building element from a blockelement. + # mapping is a dictionary that maps the blockelement elements to IFC elements. + pass + def add(self, **kwargs): super().add(**kwargs) # also update the IFC file @@ -82,3 +120,8 @@ def transformation(self, transformation): # Set the transformation matrix of the element, will also update the placement of the underlying IFC entity. pass + + +# Use pydantic class for auto-generated IFC classes?? How does it deal with pointers in seriliasition? + +# TODO: Transparent way to work with grid and storeys etc. \ No newline at end of file From 22e71c9d8c39e6adc6768ff684fd940c6983f186 Mon Sep 17 00:00:00 2001 From: Licini Date: Mon, 28 Apr 2025 17:46:51 +0200 Subject: [PATCH 3/3] WIP on update/compas_model --- scripts/model.py | 7 + src/compas_ifc/brep/__init__.py | 5 +- src/compas_ifc/brep/buildingelementobject.py | 34 +++++ src/compas_ifc/entities/buildingelements.py | 139 +++++++------------ src/compas_ifc/model.py | 2 +- 5 files changed, 95 insertions(+), 92 deletions(-) create mode 100644 scripts/model.py create mode 100644 src/compas_ifc/brep/buildingelementobject.py diff --git a/scripts/model.py b/scripts/model.py new file mode 100644 index 0000000..200a342 --- /dev/null +++ b/scripts/model.py @@ -0,0 +1,7 @@ +from compas_ifc.entities.buildingelements import BuildingModel + +model = BuildingModel(filepath="data/Duplex_A_20110907.ifc") +# print(model.tree.get_hierarchy_string(max_depth=10)) + +model.show() + diff --git a/src/compas_ifc/brep/__init__.py b/src/compas_ifc/brep/__init__.py index 6d131ea..53956c2 100644 --- a/src/compas_ifc/brep/__init__.py +++ b/src/compas_ifc/brep/__init__.py @@ -7,11 +7,14 @@ from compas.plugins import plugin from compas.scene import register - +from .buildingelementobject import BuildingElementObject +from compas_ifc.entities.buildingelements import BuildingElement @plugin(category="factories", requires=["compas_viewer"], trylast=True) def register_scene_objects(): register(TessellatedBrep, TessellatedBrepObject, context="Viewer") + register(BuildingElement, BuildingElementObject, context="Viewer") + try: from compas_occ.brep import OCCBrep diff --git a/src/compas_ifc/brep/buildingelementobject.py b/src/compas_ifc/brep/buildingelementobject.py new file mode 100644 index 0000000..e140509 --- /dev/null +++ b/src/compas_ifc/brep/buildingelementobject.py @@ -0,0 +1,34 @@ +from .tessellatedbrep import TessellatedBrep +from compas_model.viewer import ElementObject +from compas_model.elements import Group +from .tessellatedbrepobject import TessellatedBrepObject + + +class BuildingElementObject(ElementObject): + + def __init__(self, **kwargs): + super().__init__(**kwargs) + if self.element.ifc_type == "IfcSpace": + self.opacity = 0 + + def create_visualization_object(self, **kwargs): + if isinstance(self.element, Group): + self.visualization_object = None + elif isinstance(self.element.geometry, TessellatedBrep): + brep_kwargs = kwargs.copy() + brep_kwargs["item"] = self.element.geometry + self.visualization_object = TessellatedBrepObject(**brep_kwargs, **self.element.ifc_entity.style) + else: + self.visualization_object = None + + + global_transformation = self.element.transformation + + if self.element.parent: + parent_global_transformation = self.element.parent.transformation + local_transformation = global_transformation * parent_global_transformation.inverse() + else: + local_transformation = global_transformation + + self.transformation = local_transformation + diff --git a/src/compas_ifc/entities/buildingelements.py b/src/compas_ifc/entities/buildingelements.py index 49c776b..49d1d1d 100644 --- a/src/compas_ifc/entities/buildingelements.py +++ b/src/compas_ifc/entities/buildingelements.py @@ -2,126 +2,85 @@ from compas_model.elements import Element - class BuildingModel(Model): # This class will maintain and update a IFC file underneath. - def __init__(self, **kwargs): + def __init__(self, filepath=None, **kwargs): + + from compas_ifc.file import IFCFile # The model will start an empty IFC file if not provided. super().__init__(**kwargs) - self._ifc_project = None # The underlying IFC project (we don't treat it as element here) - # How about site? Maybe also integrate it here. - - @classmethod - def from_ifc(self, ifc_file): - pass + self.file = IFCFile(None, filepath=filepath) + self.load_hierarchy() - @classmethod - def from_template(self, building_count=1, storey_count=1): - # Do we support multiple sites? - pass + def load_hierarchy(self): - @classmethod - def from_blockmodel(self, blockmodel, mapping=None): - # Create a building model from a blockmodel. - # mapping is a dictionary that maps the blockmodel elements to IFC elements. - # TODO: think about the role of different models. - pass + self.project = self.file.get_entities_by_type("IfcProject")[0] - # To ... + def load_recursively(entity, parent=None): + for child in entity.children: + name = f"{child.Name}[{child.is_a()}]" + element = BuildingElement(ifc_entity=child, name=name, model=self) + self.add_element(element, parent=parent) + load_recursively(child, element) - def add(self, **kwargs): - super().add(**kwargs) - # also update the IFC file - pass + load_recursively(self.project) - def remove(self, **kwargs): - super().remove(**kwargs) - # also update the IFC file - pass + def show(self): + from compas_viewer import Viewer + from compas_viewer.components import Treeform + viewer = Viewer() + viewer.ui.sidebar.show_objectsetting = False -class BuildingElement(Element): - def __init__(self, **kwargs): - super().__init__(**kwargs) + viewer.scene.add(self) - self._ifc_entity = None # Read-only, underlying IFC entity. - self._ifc_class = None # Read-only, IFC class name, maybe redundant. - # There are 30 IfcBuiling(/t)Elements, but they only add one or two more attributes and all of them OPTIONAL. - # The value of adding these classes are questionable. - # The definition of these classes can be sometimes conflited between fields, for example Beams in architecrual and structrual context. - # At least in IFC cases, these classification are perhaps better expressed as "Labels" instead of branched-out classes. + treeform = Treeform() + viewer.ui.sidebar.widget.addWidget(treeform) + def update_treeform(form, obj): + treeform.update_from_dict({"Attributes": obj.element.ifc_attributes, "PSets": obj.element.ifc_psets}) + viewer.ui.sidebar.sceneform.callback = update_treeform - self.ifc_attributes = {} # View object, mimics dict but will update the underlying IFC entity when changed, it also validates againts the IFC schema. - self.ifc_property_sets = {} # View object, the parametric definition will be added here too. - # Should we abstract away these two, and have them managed together by a single JSON schema? - # The artificial division between attributes and property sets is also a reason of "rogue customizetion". It also confuse with Python's interperation of these two words. - # The combined name being? + viewer.show() - # Common attributes of all building elements. - self._name = None - self._description = None - self.geometry = None - # Option1: "Baked geometry", Brep (Tessellated or Full). - # Option2a: "Parametric geometry", this class inherrits from another like `Beam`, this might branching out to too many classes. - # Option2b: "Parametric geometry", this refers to a class like `Beam` as attribute, this makes the class structure convoluted. - - # The question becomes: - # 1.should the concept of "geometry" be detached from "Element"? which is the case of IFC. - # 2.Data flow-wise, the data should always flow from Model to IFC, we will not likely modify geometry if IFC elements, but what happens when we read it back? - # 3.How do we convert baked geometry back to a parametric definition? (we save the parametric definition in PSet) - # 4.If we want to make update, do we convert the whole model back to a parametric (Block) model, or should we update the BuildingModel in-place? - - # Prefered: single class with consistant APIs that handles mapping to/from IFC classes. - - self.material = None # There needs to be some mapping mechanism between the material class and Qtos in IFC. +class BuildingElement(Element): + def __init__(self, ifc_entity=None, model=None, **kwargs): + super().__init__(**kwargs) - # How about storeys? Maybe as a calculated property of the building elements? + self.model = model + self.ifc_entity = ifc_entity # Read-only, underlying IFC entity. + # self.material = None # There needs to be some mapping mechanism between the material class and Qtos in IFC. # links to parents - self.storey = None - self.building = None - self.model = None - + # self.storey = None + # self.building = None - @classmethod - def from_ifc(cls, ifc_entity): - # From a raw IFC entity fill-in all the attributes and setup the links etc. - pass + @property + def ifc_attributes(self): + return self.ifc_entity.attributes - @classmethod - def from_blockelement(cls, blockelement, mapping=None): - # Create a building element from a blockelement. - # mapping is a dictionary that maps the blockelement elements to IFC elements. - pass + @property + def ifc_psets(self): + return self.ifc_entity.property_sets - def add(self, **kwargs): - super().add(**kwargs) - # also update the IFC file - pass + @property + def ifc_type(self): + return self.ifc_entity.is_a() - def remove(self, **kwargs): - super().remove(**kwargs) - # also update the IFC file - pass + @property + def geometry(self): + return self.ifc_entity.geometry @property def transformation(self): - # Get the transformation matrix of the element - pass - - @transformation.setter - def transformation(self, transformation): - # Set the transformation matrix of the element, will also update the placement of the underlying IFC entity. - pass - + # TODO: this is not 100% correct + return self.ifc_entity.frame.to_transformation() -# Use pydantic class for auto-generated IFC classes?? How does it deal with pointers in seriliasition? -# TODO: Transparent way to work with grid and storeys etc. \ No newline at end of file +# TODO: Transparent way to work with grid and storeys etc. diff --git a/src/compas_ifc/model.py b/src/compas_ifc/model.py index 42c0120..817accb 100644 --- a/src/compas_ifc/model.py +++ b/src/compas_ifc/model.py @@ -287,7 +287,7 @@ def parse_entity(entity, parent=None): obj = viewer.scene.add(entity.geometry, name=name, parent=parent, hide_coplanaredges=True, **entity.style) obj.transformation = transformation else: - obj = viewer.scene.add([], name=name, parent=parent) + obj = viewer.scene.add_group(name=name, parent=parent) obj.attributes["entity"] = entity