mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
WARNING: Remove collection syncing. The outliner no longer is used to change spatial hierarchy.
This is a BREAKING change (on purpose). If you want to change containment / aggregation, use the UI panels, not drag and drop on the outliner.
This commit is contained in:
@@ -99,7 +99,6 @@ class IfcExporter:
|
|||||||
continue
|
continue
|
||||||
if obj.library:
|
if obj.library:
|
||||||
continue
|
continue
|
||||||
tool.Collector.sync(obj, skip_unlinking)
|
|
||||||
result = self.sync_object_placement(obj)
|
result = self.sync_object_placement(obj)
|
||||||
if result:
|
if result:
|
||||||
results.append(result)
|
results.append(result)
|
||||||
|
|||||||
@@ -302,7 +302,9 @@ class IfcImporter:
|
|||||||
self.setup_viewport_camera()
|
self.setup_viewport_camera()
|
||||||
self.setup_arrays()
|
self.setup_arrays()
|
||||||
self.profile_code("Setup arrays")
|
self.profile_code("Setup arrays")
|
||||||
blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
|
tool.Spatial.run_spatial_import_spatial_decomposition()
|
||||||
|
if default_container := tool.Spatial.guess_default_container():
|
||||||
|
tool.Spatial.set_default_container(default_container)
|
||||||
self.update_progress(100)
|
self.update_progress(100)
|
||||||
bpy.context.window_manager.progress_end()
|
bpy.context.window_manager.progress_end()
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,6 @@ class BIM_OT_add_aggregate(bpy.types.Operator, tool.Ifc.Operator):
|
|||||||
if not element:
|
if not element:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tool.Collector.sync(obj)
|
|
||||||
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
current_aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||||
current_container = ifcopenshell.util.element.get_container(element)
|
current_container = ifcopenshell.util.element.get_container(element)
|
||||||
if current_aggregate:
|
if current_aggregate:
|
||||||
|
|||||||
@@ -122,8 +122,7 @@ class CreateProject(bpy.types.Operator):
|
|||||||
bpy.data.meshes.remove(mesh)
|
bpy.data.meshes.remove(mesh)
|
||||||
for mat in bpy.data.materials:
|
for mat in bpy.data.materials:
|
||||||
bpy.data.materials.remove(mat)
|
bpy.data.materials.remove(mat)
|
||||||
core.create_project(tool.Ifc, tool.Project, schema=props.export_schema, template=template)
|
core.create_project(tool.Ifc, tool.Project, tool.Spatial, schema=props.export_schema, template=template)
|
||||||
blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
|
|
||||||
tool.Blender.register_toolbar()
|
tool.Blender.register_toolbar()
|
||||||
|
|
||||||
def rollback(self, data):
|
def rollback(self, data):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ if TYPE_CHECKING:
|
|||||||
import blenderbim.tool as tool
|
import blenderbim.tool as tool
|
||||||
|
|
||||||
|
|
||||||
def create_project(ifc: tool.Ifc, project: tool.Project, schema: str, template: Optional[str] = None) -> None:
|
def create_project(ifc: tool.Ifc, project: tool.Project, spatial: tool.Spatial, schema: str, template: Optional[str] = None) -> None:
|
||||||
if ifc.get():
|
if ifc.get():
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -90,7 +90,10 @@ def create_project(ifc: tool.Ifc, project: tool.Project, schema: str, template:
|
|||||||
project.run_aggregate_assign_object(relating_obj=building, related_obj=storey)
|
project.run_aggregate_assign_object(relating_obj=building, related_obj=storey)
|
||||||
|
|
||||||
project.set_context(body)
|
project.set_context(body)
|
||||||
project.set_active_spatial_element(storey)
|
spatial.run_spatial_import_spatial_decomposition()
|
||||||
|
if default_container := spatial.guess_default_container():
|
||||||
|
spatial.set_default_container(default_container)
|
||||||
|
|
||||||
project.create_project_collections()
|
project.create_project_collections()
|
||||||
|
|
||||||
if template:
|
if template:
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ def assign_class(
|
|||||||
predefined_type: Optional[str] = None,
|
predefined_type: Optional[str] = None,
|
||||||
should_add_representation: bool = True,
|
should_add_representation: bool = True,
|
||||||
ifc_representation_class: Optional[str] = None,
|
ifc_representation_class: Optional[str] = None,
|
||||||
) -> ifcopenshell.entity_instance:
|
) -> Optional[ifcopenshell.entity_instance]:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
context: is not optional if `should_add_representation` is True
|
context: is not optional if `should_add_representation` is True
|
||||||
@@ -86,6 +86,10 @@ def assign_class(
|
|||||||
obj=obj, context=context, ifc_representation_class=ifc_representation_class, profile_set_usage=None
|
obj=obj, context=context, ifc_representation_class=ifc_representation_class, profile_set_usage=None
|
||||||
)
|
)
|
||||||
|
|
||||||
collector.sync(obj)
|
if default_container := root.get_default_container():
|
||||||
|
if root.is_spatial_element(element):
|
||||||
|
ifc.run("aggregate.assign_object", products=[element], relating_object=default_container)
|
||||||
|
elif root.is_containable(element):
|
||||||
|
ifc.run("spatial.assign_container", products=[element], relating_structure=default_container)
|
||||||
collector.assign(obj)
|
collector.assign(obj)
|
||||||
return element
|
return element
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ def copy_to_container(ifc, collector, spatial, obj=None, containers=None):
|
|||||||
element = ifc.get_entity(obj)
|
element = ifc.get_entity(obj)
|
||||||
if not element:
|
if not element:
|
||||||
return
|
return
|
||||||
collector.sync(obj)
|
|
||||||
from_container = spatial.get_container(element)
|
from_container = spatial.get_container(element)
|
||||||
if from_container:
|
if from_container:
|
||||||
matrix = spatial.get_relative_object_matrix(obj, ifc.get_object(from_container))
|
matrix = spatial.get_relative_object_matrix(obj, ifc.get_object(from_container))
|
||||||
|
|||||||
@@ -689,12 +689,15 @@ class Root:
|
|||||||
def copy_representation(cls, source, dest): pass
|
def copy_representation(cls, source, dest): pass
|
||||||
def does_type_have_representations(cls, element): pass
|
def does_type_have_representations(cls, element): pass
|
||||||
def get_decomposition_relationships(cls, objs): pass
|
def get_decomposition_relationships(cls, objs): pass
|
||||||
|
def get_default_container(cls): pass
|
||||||
def get_element_representation(cls, element, context): pass
|
def get_element_representation(cls, element, context): pass
|
||||||
def get_element_type(cls, element): pass
|
def get_element_type(cls, element): pass
|
||||||
def get_object_name(cls, obj): pass
|
def get_object_name(cls, obj): pass
|
||||||
def get_object_representation(cls, obj): pass
|
def get_object_representation(cls, obj): pass
|
||||||
def get_representation_context(cls, representation): pass
|
def get_representation_context(cls, representation): pass
|
||||||
|
def is_containable(cls, element): pass
|
||||||
def is_element_a(cls, element, ifc_class): pass
|
def is_element_a(cls, element, ifc_class): pass
|
||||||
|
def is_spatial_element(cls, element): pass
|
||||||
def link_object_data(cls, source_obj, destination_obj): pass
|
def link_object_data(cls, source_obj, destination_obj): pass
|
||||||
def recreate_decompositions(cls, relationships, old_to_new): pass
|
def recreate_decompositions(cls, relationships, old_to_new): pass
|
||||||
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
|
def run_geometry_add_representation(cls, obj=None, context=None, ifc_representation_class=None, profile_set_usage=None): pass
|
||||||
@@ -851,6 +854,7 @@ class Spatial:
|
|||||||
def import_spatial_decomposition(cls): pass
|
def import_spatial_decomposition(cls): pass
|
||||||
def run_root_copy_class(cls, obj=None): pass
|
def run_root_copy_class(cls, obj=None): pass
|
||||||
def run_spatial_assign_container(cls, structure_obj=None, element_obj=None): pass
|
def run_spatial_assign_container(cls, structure_obj=None, element_obj=None): pass
|
||||||
|
def run_spatial_import_spatial_decomposition(cls): pass
|
||||||
def select_object(cls, obj): pass
|
def select_object(cls, obj): pass
|
||||||
def select_products(cls, products, unhide=False): pass
|
def select_products(cls, products, unhide=False): pass
|
||||||
def set_active_object(cls, obj): pass
|
def set_active_object(cls, obj): pass
|
||||||
|
|||||||
@@ -27,81 +27,6 @@ from typing import Union
|
|||||||
|
|
||||||
|
|
||||||
class Collector(blenderbim.core.tool.Collector):
|
class Collector(blenderbim.core.tool.Collector):
|
||||||
@classmethod
|
|
||||||
def sync(cls, obj: bpy.types.Object, skip_unlinking=False) -> None:
|
|
||||||
"""Sync object IFC state (assigned containter / aggregate) with the collection it's currently in.
|
|
||||||
|
|
||||||
Then subsequently run `Collector.assign` (if state has changed)
|
|
||||||
to link them to collections / unlink from anything unrelated collections.
|
|
||||||
|
|
||||||
If `skip_unlinking` is `True` then method won't try to assign parent object
|
|
||||||
if it's already assigned in IFC saving some time.
|
|
||||||
But it has a downside not unlinking object from unrelated collections.
|
|
||||||
"""
|
|
||||||
# This is the reverse of assign. It reads the Blender collection and figures out its IFC hierarchy
|
|
||||||
element = tool.Ifc.get_entity(obj)
|
|
||||||
|
|
||||||
if (
|
|
||||||
not element
|
|
||||||
or element.is_a("IfcProject")
|
|
||||||
or element.is_a("IfcGridAxis")
|
|
||||||
or element.is_a("IfcOpeningElement")
|
|
||||||
):
|
|
||||||
return
|
|
||||||
|
|
||||||
if not obj.users_collection:
|
|
||||||
return
|
|
||||||
|
|
||||||
# create related collections
|
|
||||||
cls._get_own_collection(element, obj)
|
|
||||||
cls._get_collection(element, obj)
|
|
||||||
|
|
||||||
parent_collection = None
|
|
||||||
|
|
||||||
for collection in obj.users_collection:
|
|
||||||
if parent_collection:
|
|
||||||
break
|
|
||||||
# skip Types and non-BIM collections
|
|
||||||
if not collection.BIMCollectionProperties.obj:
|
|
||||||
continue
|
|
||||||
# for objects that own collections we search for the first parent collection
|
|
||||||
if collection.BIMCollectionProperties.obj == obj:
|
|
||||||
collection_name = collection.name
|
|
||||||
for bpy_collection in bpy.data.collections:
|
|
||||||
if bpy_collection.children.get(collection_name) and bpy_collection.BIMCollectionProperties.obj:
|
|
||||||
parent_collection = bpy_collection
|
|
||||||
parent_obj = bpy_collection.BIMCollectionProperties.obj
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
parent_collection = collection
|
|
||||||
parent_obj = collection.BIMCollectionProperties.obj
|
|
||||||
|
|
||||||
if not parent_collection:
|
|
||||||
return
|
|
||||||
|
|
||||||
parent = tool.Ifc.get_entity(parent_obj)
|
|
||||||
if skip_unlinking:
|
|
||||||
previous_parent = ifcopenshell.util.element.get_container(
|
|
||||||
element, should_get_direct=True
|
|
||||||
) or ifcopenshell.util.element.get_aggregate(element)
|
|
||||||
if parent == previous_parent:
|
|
||||||
return
|
|
||||||
|
|
||||||
if parent:
|
|
||||||
# This is lazy, but works. One of these will succeed, the other will fail silently.
|
|
||||||
blenderbim.core.spatial.assign_container(
|
|
||||||
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=parent_obj, element_obj=obj
|
|
||||||
)
|
|
||||||
# NOTE: won't allow assigning IfcElements to the IfcProject directly
|
|
||||||
# and some elements might get missing in other viewers if they're don't support displaying
|
|
||||||
# elements without hierarchy
|
|
||||||
try:
|
|
||||||
blenderbim.core.aggregate.assign_object(
|
|
||||||
tool.Ifc, tool.Aggregate, tool.Collector, relating_obj=parent_obj, related_obj=obj
|
|
||||||
)
|
|
||||||
except blenderbim.core.aggregate.IncompatibleAggregateError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def assign(cls, obj: bpy.types.Object) -> None:
|
def assign(cls, obj: bpy.types.Object) -> None:
|
||||||
"""link object and it's owned collection to the proper collection
|
"""link object and it's owned collection to the proper collection
|
||||||
|
|||||||
@@ -98,6 +98,16 @@ class Root(blenderbim.core.tool.Root):
|
|||||||
relationships[element] = {"type": "fill", "element": building}
|
relationships[element] = {"type": "fill", "element": building}
|
||||||
return relationships
|
return relationships
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_default_container(cls) -> Optional[ifcopenshell.entity_instance]:
|
||||||
|
props = bpy.context.scene.BIMSpatialDecompositionProperties
|
||||||
|
if container := props.default_container:
|
||||||
|
try:
|
||||||
|
return tool.Ifc.get().by_id(container)
|
||||||
|
except:
|
||||||
|
props.default_container = 0
|
||||||
|
return None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_connection_relationships(
|
def get_connection_relationships(
|
||||||
cls, objs: list[bpy.types.Object]
|
cls, objs: list[bpy.types.Object]
|
||||||
@@ -162,10 +172,20 @@ class Root(blenderbim.core.tool.Root):
|
|||||||
def get_representation_context(cls, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
def get_representation_context(cls, representation: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance:
|
||||||
return representation.ContextOfItems
|
return representation.ContextOfItems
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_containable(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||||
|
return element.is_a("IfcElement") or element.is_a("IfcGrid")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool:
|
def is_element_a(cls, element: ifcopenshell.entity_instance, ifc_class: str) -> bool:
|
||||||
return element.is_a(ifc_class)
|
return element.is_a(ifc_class)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_spatial_element(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||||
|
if tool.Ifc.get().schema == "IFC2X3":
|
||||||
|
return element.is_a("IfcSpatialStructureElement")
|
||||||
|
return element.is_a("IfcSpatialElement")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None:
|
def link_object_data(cls, source_obj: bpy.types.Object, destination_obj: bpy.types.Object) -> None:
|
||||||
destination_obj.data = source_obj.data
|
destination_obj.data = source_obj.data
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ import blenderbim.tool as tool
|
|||||||
import json
|
import json
|
||||||
from math import pi
|
from math import pi
|
||||||
from mathutils import Vector, Matrix
|
from mathutils import Vector, Matrix
|
||||||
from shapely import Polygon, MultiPolygon
|
from shapely import Polygon
|
||||||
from typing import Generator
|
from typing import Generator, Optional
|
||||||
|
|
||||||
|
|
||||||
class Spatial(blenderbim.core.tool.Spatial):
|
class Spatial(blenderbim.core.tool.Spatial):
|
||||||
@@ -134,6 +134,10 @@ class Spatial(blenderbim.core.tool.Spatial):
|
|||||||
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=structure_obj, element_obj=element_obj
|
tool.Ifc, tool.Collector, tool.Spatial, structure_obj=structure_obj, element_obj=element_obj
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def run_spatial_import_spatial_decomposition(cls):
|
||||||
|
return blenderbim.core.spatial.import_spatial_decomposition(tool.Spatial)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def select_object(cls, obj):
|
def select_object(cls, obj):
|
||||||
obj.select_set(True)
|
obj.select_set(True)
|
||||||
@@ -445,7 +449,6 @@ class Spatial(blenderbim.core.tool.Spatial):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_x_y_z_h_mat_from_active_obj(cls, active_obj):
|
def get_x_y_z_h_mat_from_active_obj(cls, active_obj):
|
||||||
element = tool.Ifc.get_entity(active_obj)
|
|
||||||
mat = active_obj.matrix_world
|
mat = active_obj.matrix_world
|
||||||
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector())
|
local_bbox_center = 0.125 * sum((Vector(b) for b in active_obj.bound_box), Vector())
|
||||||
global_bbox_center = mat @ local_bbox_center
|
global_bbox_center = mat @ local_bbox_center
|
||||||
@@ -518,14 +521,13 @@ class Spatial(blenderbim.core.tool.Spatial):
|
|||||||
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
|
project_unit = ifcopenshell.util.unit.get_project_unit(model, "LENGTHUNIT")
|
||||||
prefix = getattr(project_unit, "Prefix", None)
|
prefix = getattr(project_unit, "Prefix", None)
|
||||||
|
|
||||||
converted_tolerance = ifcopenshell.util.unit.convert(
|
return ifcopenshell.util.unit.convert(
|
||||||
value=tolerance,
|
value=tolerance,
|
||||||
from_prefix=None,
|
from_prefix=None,
|
||||||
from_unit="METRE",
|
from_unit="METRE",
|
||||||
to_prefix=prefix,
|
to_prefix=prefix,
|
||||||
to_unit=project_unit.Name,
|
to_unit=project_unit.Name,
|
||||||
)
|
)
|
||||||
return tolerance
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_purged_inner_holes_poly(cls, union_geom, min_area):
|
def get_purged_inner_holes_poly(cls, union_geom, min_area):
|
||||||
@@ -830,3 +832,19 @@ class Spatial(blenderbim.core.tool.Spatial):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def set_default_container(cls, container):
|
def set_default_container(cls, container):
|
||||||
bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id()
|
bpy.context.scene.BIMSpatialDecompositionProperties.default_container = container.id()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def guess_default_container(cls) -> Optional[ifcopenshell.entity_instance]:
|
||||||
|
project = tool.Ifc.get().by_type("IfcProject")[0]
|
||||||
|
subelement = None
|
||||||
|
# We try to priorise the first Site > Building > Storey as a convention for vertical projects
|
||||||
|
for subelement in ifcopenshell.util.element.get_parts(project):
|
||||||
|
if subelement.is_a("IfcSite"):
|
||||||
|
for subelement2 in ifcopenshell.util.element.get_parts(subelement):
|
||||||
|
if subelement2.is_a("IfcBuilding"):
|
||||||
|
for subelement3 in ifcopenshell.util.element.get_parts(subelement2):
|
||||||
|
if subelement3.is_a("IfcBuildingStorey"):
|
||||||
|
return subelement3
|
||||||
|
if subelement:
|
||||||
|
return subelement
|
||||||
|
return None
|
||||||
|
|||||||
@@ -80,7 +80,6 @@ class TestRemoveContainer:
|
|||||||
class TestCopyToContainer:
|
class TestCopyToContainer:
|
||||||
def test_run(self, ifc, collector, spatial):
|
def test_run(self, ifc, collector, spatial):
|
||||||
ifc.get_entity("obj").should_be_called().will_return("element")
|
ifc.get_entity("obj").should_be_called().will_return("element")
|
||||||
collector.sync("obj").should_be_called()
|
|
||||||
spatial.get_container("element").should_be_called().will_return("container")
|
spatial.get_container("element").should_be_called().will_return("container")
|
||||||
ifc.get_object("container").should_be_called().will_return("container_obj")
|
ifc.get_object("container").should_be_called().will_return("container_obj")
|
||||||
spatial.get_relative_object_matrix("obj", "container_obj").should_be_called().will_return("matrix")
|
spatial.get_relative_object_matrix("obj", "container_obj").should_be_called().will_return("matrix")
|
||||||
@@ -97,7 +96,6 @@ class TestCopyToContainer:
|
|||||||
|
|
||||||
def test_using_an_absolute_matrix_if_there_is_no_from_container(self, ifc, collector, spatial):
|
def test_using_an_absolute_matrix_if_there_is_no_from_container(self, ifc, collector, spatial):
|
||||||
ifc.get_entity("obj").should_be_called().will_return("element")
|
ifc.get_entity("obj").should_be_called().will_return("element")
|
||||||
collector.sync("obj").should_be_called()
|
|
||||||
spatial.get_container("element").should_be_called().will_return(None)
|
spatial.get_container("element").should_be_called().will_return(None)
|
||||||
spatial.get_object_matrix("obj").should_be_called().will_return("matrix")
|
spatial.get_object_matrix("obj").should_be_called().will_return("matrix")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user