mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-12 10:33:20 +00:00
Removing or changing spatial containers now supports updating more types of collection organisations
This commit is contained in:
@@ -188,9 +188,11 @@ class OwnerData:
|
||||
def get_users(cls):
|
||||
results = []
|
||||
for user in tool.Ifc.get().by_type("IfcPersonAndOrganization"):
|
||||
results.append({
|
||||
"id": user.id(),
|
||||
"label": "{} ({})".format(user.ThePerson[0] or "Unnamed", user.TheOrganization[0] or "Unnamed"),
|
||||
"is_active": bpy.context.scene.BIMOwnerProperties.active_user_id == user.id(),
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"id": user.id(),
|
||||
"label": "{} ({})".format(user.ThePerson[0] or "Unnamed", user.TheOrganization[0] or "Unnamed"),
|
||||
"is_active": bpy.context.scene.BIMOwnerProperties.active_user_id == user.id(),
|
||||
}
|
||||
)
|
||||
return results
|
||||
|
||||
@@ -27,8 +27,8 @@ import ifcopenshell.util.selector
|
||||
import ifcopenshell.util.representation
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.context as core_context
|
||||
import blenderbim.core.owner as core_owner
|
||||
import blenderbim.core.context
|
||||
import blenderbim.core.owner
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim import import_ifc
|
||||
|
||||
@@ -58,10 +58,10 @@ class CreateProject(bpy.types.Operator):
|
||||
)
|
||||
self.file = IfcStore.get_file()
|
||||
|
||||
person = core_owner.add_person(tool.Ifc)
|
||||
organisation = core_owner.add_organisation(tool.Ifc)
|
||||
user = core_owner.add_person_and_organisation(tool.Ifc, person=person, organisation=organisation)
|
||||
core_owner.set_user(tool.Owner, user=user)
|
||||
person = blenderbim.core.owner.add_person(tool.Ifc)
|
||||
organisation = blenderbim.core.owner.add_organisation(tool.Ifc)
|
||||
user = blenderbim.core.owner.add_person_and_organisation(tool.Ifc, person=person, organisation=organisation)
|
||||
blenderbim.core.owner.set_user(tool.Owner, user=user)
|
||||
|
||||
project = bpy.data.objects.new(self.get_name("IfcProject", "My Project"), None)
|
||||
site = bpy.data.objects.new(self.get_name("IfcSite", "My Site"), None)
|
||||
@@ -71,17 +71,19 @@ class CreateProject(bpy.types.Operator):
|
||||
bpy.ops.bim.assign_class(obj=project.name, ifc_class="IfcProject")
|
||||
bpy.ops.bim.assign_unit()
|
||||
|
||||
model = core_context.add_context(
|
||||
model = blenderbim.core.context.add_context(
|
||||
tool.Ifc, context_type="Model", context_identifier="", target_view="", parent=0
|
||||
)
|
||||
core_context.add_context(
|
||||
blenderbim.core.context.add_context(
|
||||
tool.Ifc, context_type="Model", context_identifier="Body", target_view="MODEL_VIEW", parent=model
|
||||
)
|
||||
core_context.add_context(
|
||||
blenderbim.core.context.add_context(
|
||||
tool.Ifc, context_type="Model", context_identifier="Box", target_view="MODEL_VIEW", parent=model
|
||||
)
|
||||
plan = core_context.add_context(tool.Ifc, context_type="Plan", context_identifier="", target_view="", parent=0)
|
||||
core_context.add_context(
|
||||
plan = blenderbim.core.context.add_context(
|
||||
tool.Ifc, context_type="Plan", context_identifier="", target_view="", parent=0
|
||||
)
|
||||
blenderbim.core.context.add_context(
|
||||
tool.Ifc, context_type="Plan", context_identifier="Annotation", target_view="PLAN_VIEW", parent=plan
|
||||
)
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import ifcopenshell.api
|
||||
import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.bim.handler
|
||||
import blenderbim.core.spatial
|
||||
import blenderbim.tool as tool
|
||||
from ifcopenshell.api.void.data import Data as VoidData
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
|
||||
@@ -236,8 +238,13 @@ class AssignClass(bpy.types.Operator):
|
||||
continue
|
||||
elif self.file.schema == "IFC2X3" and not element.is_a("IfcSpatialStructureElement"):
|
||||
continue
|
||||
bpy.ops.bim.assign_container(
|
||||
relating_structure=spatial_obj.BIMObjectProperties.ifc_definition_id, related_element=obj.name
|
||||
blenderbim.core.spatial.assign_container(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Container,
|
||||
tool.Surveyor,
|
||||
structure_obj=spatial_obj,
|
||||
element_obj=obj,
|
||||
)
|
||||
break
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ classes = (
|
||||
prop.BIMSpatialProperties,
|
||||
prop.BIMObjectSpatialProperties,
|
||||
ui.BIM_PT_spatial,
|
||||
ui.BIM_UL_spatial_elements,
|
||||
ui.BIM_UL_containers,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
import ifcopenshell.util.element
|
||||
|
||||
|
||||
class SpatialData:
|
||||
data = {}
|
||||
is_loaded = False
|
||||
|
||||
@classmethod
|
||||
def load(cls):
|
||||
cls.data = {
|
||||
"is_editing": bpy.context.active_object.BIMObjectSpatialProperties.is_editing,
|
||||
"parent_container_id": cls.get_parent_container_id(),
|
||||
"container_id": cls.get_container_id(),
|
||||
"container_list_settings": cls.get_container_list_settings(),
|
||||
"is_contained": cls.is_contained(),
|
||||
"label": cls.get_label(),
|
||||
}
|
||||
cls.is_loaded = True
|
||||
|
||||
@classmethod
|
||||
def get_parent_container_id(cls):
|
||||
container_id = bpy.context.scene.BIMSpatialProperties.active_container_id
|
||||
if not container_id:
|
||||
return
|
||||
container = tool.Ifc.get().by_id(container_id)
|
||||
if container.is_a("IfcProject"):
|
||||
return
|
||||
return container.Decomposes[0].RelatingObject.id()
|
||||
|
||||
@classmethod
|
||||
def get_container_id(cls):
|
||||
props = bpy.context.scene.BIMSpatialProperties
|
||||
try:
|
||||
return props.containers[props.active_container_index].ifc_definition_id
|
||||
except:
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def get_container_list_settings(cls):
|
||||
props = bpy.context.scene.BIMSpatialProperties
|
||||
return ["", props, "containers", props, "active_container_id"]
|
||||
|
||||
@classmethod
|
||||
def is_contained(cls):
|
||||
return ifcopenshell.util.element.get_container(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
|
||||
@classmethod
|
||||
def get_label(cls):
|
||||
container = ifcopenshell.util.element.get_container(tool.Ifc.get_entity(bpy.context.active_object))
|
||||
if container:
|
||||
return f"{container.is_a()}/{container.Name or ''}"
|
||||
@@ -19,150 +19,74 @@
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.util.element
|
||||
import blenderbim.tool as tool
|
||||
import blenderbim.core.spatial as core
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.spatial.data import Data
|
||||
from blenderbim.bim.module.spatial.prop import getSpatialContainers
|
||||
from blenderbim.bim.module.spatial.data import SpatialData
|
||||
|
||||
|
||||
class AssignContainer(bpy.types.Operator):
|
||||
class Operator:
|
||||
def execute(self, context):
|
||||
IfcStore.execute_ifc_operator(self, context)
|
||||
SpatialData.is_loaded = False
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AssignContainer(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.assign_container"
|
||||
bl_label = "Assign Container"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
relating_structure: bpy.props.IntProperty()
|
||||
related_element: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
structure: bpy.props.IntProperty()
|
||||
|
||||
def _execute(self, context):
|
||||
# The active object pointer is lost when the object is unlinked from all collections in the view layer
|
||||
# We need to store it first, then restore it at the end
|
||||
active_object = context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
related_elements = (
|
||||
[bpy.data.objects.get(self.related_element)] if self.related_element else context.selected_objects
|
||||
)
|
||||
sprops = context.scene.BIMSpatialProperties
|
||||
relating_structure = (
|
||||
self.relating_structure or sprops.spatial_elements[sprops.active_spatial_element_index].ifc_definition_id
|
||||
)
|
||||
for related_element in related_elements:
|
||||
oprops = related_element.BIMObjectProperties
|
||||
if not oprops.ifc_definition_id:
|
||||
continue
|
||||
props = related_element.BIMObjectSpatialProperties
|
||||
|
||||
ifcopenshell.api.run(
|
||||
"spatial.assign_container",
|
||||
self.file,
|
||||
**{
|
||||
"product": self.file.by_id(oprops.ifc_definition_id),
|
||||
"relating_structure": self.file.by_id(relating_structure),
|
||||
},
|
||||
structure_obj = tool.Ifc.get_object(tool.Ifc.get().by_id(self.structure))
|
||||
for element_obj in context.selected_objects:
|
||||
core.assign_container(
|
||||
tool.Ifc,
|
||||
tool.Collector,
|
||||
tool.Container,
|
||||
tool.Surveyor,
|
||||
structure_obj=structure_obj,
|
||||
element_obj=element_obj,
|
||||
)
|
||||
bpy.ops.bim.edit_object_placement(obj=related_element.name)
|
||||
Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
|
||||
bpy.ops.bim.disable_editing_container(obj=related_element.name)
|
||||
|
||||
aggregate_collection = bpy.data.collections.get(related_element.name)
|
||||
|
||||
relating_structure_obj = IfcStore.get_element(relating_structure)
|
||||
relating_collection = None
|
||||
if relating_structure_obj:
|
||||
relating_collection = bpy.data.collections.get(relating_structure_obj.name)
|
||||
|
||||
if aggregate_collection:
|
||||
self.remove_collection(context.scene.collection, aggregate_collection)
|
||||
for collection in bpy.data.collections:
|
||||
self.remove_collection(collection, aggregate_collection)
|
||||
relating_collection.children.link(aggregate_collection)
|
||||
elif relating_collection:
|
||||
for collection in related_element.users_collection:
|
||||
collection.objects.unlink(related_element)
|
||||
relating_collection.objects.link(related_element)
|
||||
# Restore the active object :
|
||||
context.view_layer.objects.active = active_object
|
||||
return {"FINISHED"}
|
||||
|
||||
def remove_collection(self, parent, child):
|
||||
try:
|
||||
parent.children.unlink(child)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class EnableEditingContainer(bpy.types.Operator):
|
||||
class EnableEditingContainer(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.enable_editing_container"
|
||||
bl_label = "Enable Editing Container"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
context.active_object.BIMObjectSpatialProperties.is_editing = True
|
||||
getSpatialContainers(self, context)
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.enable_editing_container(tool.Container, obj=context.active_object)
|
||||
|
||||
|
||||
class ChangeSpatialLevel(bpy.types.Operator):
|
||||
class ChangeSpatialLevel(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.change_spatial_level"
|
||||
bl_label = "Change Spatial Level"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
parent_id: bpy.props.IntProperty()
|
||||
parent: bpy.props.IntProperty()
|
||||
|
||||
def execute(self, context):
|
||||
getSpatialContainers(self, context, parent_id=self.parent_id)
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.change_spatial_level(tool.Container, parent=tool.Ifc.get().by_id(self.parent))
|
||||
|
||||
|
||||
class DisableEditingContainer(bpy.types.Operator):
|
||||
class DisableEditingContainer(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.disable_editing_container"
|
||||
bl_label = "Disable Editing Container"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
obj = bpy.data.objects.get(self.obj) if self.obj else context.active_object
|
||||
obj.BIMObjectSpatialProperties.is_editing = False
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context):
|
||||
core.disable_editing_container(tool.Container, obj=context.active_object)
|
||||
|
||||
|
||||
class RemoveContainer(bpy.types.Operator):
|
||||
class RemoveContainer(bpy.types.Operator, Operator):
|
||||
bl_idname = "bim.remove_container"
|
||||
bl_label = "Remove Container"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
obj: bpy.props.StringProperty()
|
||||
|
||||
def execute(self, context):
|
||||
return IfcStore.execute_ifc_operator(self, context)
|
||||
|
||||
def _execute(self, context):
|
||||
active_object = context.active_object
|
||||
self.file = IfcStore.get_file()
|
||||
objs = [bpy.data.objects.get(self.obj)] if self.obj else context.selected_objects
|
||||
for obj in objs:
|
||||
obj_id = obj.BIMObjectProperties.ifc_definition_id
|
||||
if not obj_id:
|
||||
continue
|
||||
ifcopenshell.api.run("spatial.remove_container", self.file, **{"product": self.file.by_id(obj_id)})
|
||||
Data.load(self.file, obj_id)
|
||||
|
||||
aggregate_collection = bpy.data.collections.get(obj.name)
|
||||
if aggregate_collection:
|
||||
self.remove_collection(context.scene.collection, aggregate_collection)
|
||||
for collection in bpy.data.collections:
|
||||
self.remove_collection(collection, spatial_collection)
|
||||
context.scene.collection.children.link(aggregate_collection)
|
||||
else:
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.unlink(obj)
|
||||
context.scene.collection.objects.link(obj)
|
||||
context.view_layer.objects.active = active_object
|
||||
return {"FINISHED"}
|
||||
|
||||
def remove_collection(self, parent, child):
|
||||
try:
|
||||
parent.children.unlink(child)
|
||||
except:
|
||||
pass
|
||||
for obj in context.selected_objects:
|
||||
core.remove_container(tool.Ifc, tool.Collector, obj=obj)
|
||||
|
||||
|
||||
class CopyToContainer(bpy.types.Operator):
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
|
||||
import bpy
|
||||
from blenderbim.bim.prop import StrProperty, Attribute
|
||||
from ifcopenshell.api.spatial.data import Data
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from blenderbim.bim.module.spatial.data import SpatialData
|
||||
from bpy.types import PropertyGroup
|
||||
from bpy.props import (
|
||||
PointerProperty,
|
||||
@@ -33,37 +32,8 @@ from bpy.props import (
|
||||
)
|
||||
|
||||
|
||||
def getSpatialContainers(self, context, parent_id=None):
|
||||
Data.load(IfcStore.get_file())
|
||||
self.file = IfcStore.get_file()
|
||||
props = context.scene.BIMSpatialProperties
|
||||
props.spatial_elements.clear()
|
||||
|
||||
if parent_id:
|
||||
props.active_decomposes_id = parent_id
|
||||
|
||||
try:
|
||||
self.file.by_id(props.active_decomposes_id)
|
||||
except:
|
||||
props.active_decomposes_id = 0
|
||||
|
||||
project_id = self.file.by_type("IfcProject")[0].id()
|
||||
|
||||
if not props.active_decomposes_id:
|
||||
props.active_decomposes_id = project_id
|
||||
|
||||
for ifc_definition_id, spatial_element in Data.spatial_elements.items():
|
||||
if spatial_element["Decomposes"] == props.active_decomposes_id:
|
||||
new = props.spatial_elements.add()
|
||||
new.name = spatial_element["Name"] or "Unnamed"
|
||||
new.long_name = spatial_element["LongName"] or ""
|
||||
new.has_decomposition = bool(spatial_element["IsDecomposedBy"])
|
||||
new.ifc_definition_id = ifc_definition_id
|
||||
|
||||
if props.active_decomposes_id == project_id:
|
||||
props.active_decomposes_parent_id = 0
|
||||
else:
|
||||
props.active_decomposes_parent_id = Data.spatial_elements[props.active_decomposes_id]["Decomposes"]
|
||||
def update_active_container_index(self, context):
|
||||
SpatialData.is_loaded = False
|
||||
|
||||
|
||||
class SpatialElement(PropertyGroup):
|
||||
@@ -75,10 +45,9 @@ class SpatialElement(PropertyGroup):
|
||||
|
||||
|
||||
class BIMSpatialProperties(PropertyGroup):
|
||||
spatial_elements: CollectionProperty(name="Spatial Elements", type=SpatialElement)
|
||||
active_spatial_element_index: IntProperty(name="Active Spatial Element Index")
|
||||
active_decomposes_id: IntProperty(name="Active Decomposes Id")
|
||||
active_decomposes_parent_id: IntProperty(name="Active Decomposes Parent Id")
|
||||
containers: CollectionProperty(name="Containers", type=SpatialElement)
|
||||
active_container_index: IntProperty(name="Active Container Index", update=update_active_container_index)
|
||||
active_container_id: IntProperty(name="Active Container Id")
|
||||
|
||||
|
||||
class BIMObjectSpatialProperties(PropertyGroup):
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
from bpy.types import Panel, UIList
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from ifcopenshell.api.spatial.data import Data
|
||||
from blenderbim.bim.module.spatial.data import SpatialData
|
||||
|
||||
|
||||
class BIM_PT_spatial(Panel):
|
||||
@@ -44,44 +45,36 @@ class BIM_PT_spatial(Panel):
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
oprops = context.active_object.BIMObjectProperties
|
||||
sprops = context.scene.BIMSpatialProperties
|
||||
props = context.active_object.BIMObjectSpatialProperties
|
||||
if not oprops.ifc_definition_id:
|
||||
return
|
||||
if oprops.ifc_definition_id not in Data.products:
|
||||
Data.load(IfcStore.get_file(), oprops.ifc_definition_id)
|
||||
if not SpatialData.is_loaded:
|
||||
SpatialData.load()
|
||||
|
||||
if props.is_editing:
|
||||
if SpatialData.data["is_editing"]:
|
||||
row = self.layout.row(align=True)
|
||||
if sprops.active_decomposes_parent_id:
|
||||
if SpatialData.data["parent_container_id"]:
|
||||
op = row.operator("bim.change_spatial_level", text="", icon="FRAME_PREV")
|
||||
op.parent_id = sprops.active_decomposes_parent_id
|
||||
row.operator("bim.assign_container", icon="CHECKMARK")
|
||||
op.parent = SpatialData.data["parent_container_id"]
|
||||
row.operator("bim.assign_container", icon="CHECKMARK").structure = SpatialData.data["container_id"]
|
||||
row.operator("bim.copy_to_container", icon="COPYDOWN", text="")
|
||||
row.operator("bim.disable_editing_container", icon="CANCEL", text="")
|
||||
|
||||
self.layout.template_list(
|
||||
"BIM_UL_spatial_elements", "", sprops, "spatial_elements", sprops, "active_spatial_element_index"
|
||||
)
|
||||
self.layout.template_list("BIM_UL_containers", *SpatialData.data["container_list_settings"])
|
||||
else:
|
||||
row = self.layout.row(align=True)
|
||||
name = "{}/{}".format(
|
||||
Data.products[oprops.ifc_definition_id]["type"], Data.products[oprops.ifc_definition_id]["Name"]
|
||||
)
|
||||
if name == "None/None":
|
||||
name = "This object is not spatially contained"
|
||||
row.label(text=name)
|
||||
row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_container", icon="X", text="")
|
||||
if SpatialData.data["is_contained"]:
|
||||
row.label(text=SpatialData.data["label"])
|
||||
row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="")
|
||||
row.operator("bim.remove_container", icon="X", text="")
|
||||
else:
|
||||
row.label(text="This object is not spatially contained")
|
||||
row.operator("bim.enable_editing_container", icon="GREASEPENCIL", text="")
|
||||
|
||||
|
||||
class BIM_UL_spatial_elements(UIList):
|
||||
class BIM_UL_containers(UIList):
|
||||
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
|
||||
if item:
|
||||
if item.has_decomposition:
|
||||
op = layout.operator("bim.change_spatial_level", text="", icon="DISCLOSURE_TRI_RIGHT", emboss=False)
|
||||
op.parent_id = item.ifc_definition_id
|
||||
op.parent = item.ifc_definition_id
|
||||
layout.label(text=item.name)
|
||||
layout.label(text=item.long_name)
|
||||
layout.prop(
|
||||
|
||||
@@ -1,3 +1,22 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
def edit_object_placement(ifc, surveyor, obj=None):
|
||||
element = ifc.get_entity(obj)
|
||||
if element:
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blenderbim.core
|
||||
|
||||
|
||||
def assign_container(ifc, collector, container, surveyor, structure_obj=None, element_obj=None):
|
||||
if not container.can_contain(structure_obj, element_obj):
|
||||
return
|
||||
rel = ifc.run(
|
||||
"spatial.assign_container",
|
||||
product=ifc.get_entity(element_obj),
|
||||
relating_structure=ifc.get_entity(structure_obj),
|
||||
)
|
||||
blenderbim.core.geometry.edit_object_placement(ifc, surveyor, obj=element_obj)
|
||||
container.disable_editing(element_obj)
|
||||
collector.assign(element_obj)
|
||||
return rel
|
||||
|
||||
|
||||
def enable_editing_container(container, obj=None):
|
||||
container.enable_editing(obj)
|
||||
container.import_containers()
|
||||
|
||||
|
||||
def disable_editing_container(container, obj=None):
|
||||
container.disable_editing(obj)
|
||||
|
||||
|
||||
def change_spatial_level(container, parent=None):
|
||||
container.import_containers(parent=parent)
|
||||
|
||||
|
||||
def remove_container(ifc, collector, obj=None):
|
||||
ifc.run("spatial.remove_container", product=ifc.get_entity(obj))
|
||||
collector.assign(obj)
|
||||
@@ -83,6 +83,11 @@ class Container(abc.ABC):
|
||||
def disable_editing(cls, obj):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
@abc.abstractmethod
|
||||
def import_containers(cls, parent=None):
|
||||
pass
|
||||
|
||||
|
||||
class ContextEditor(abc.ABC):
|
||||
@classmethod
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
|
||||
from blenderbim.tool.address_editor import AddressEditor
|
||||
from blenderbim.tool.blender import Blender
|
||||
from blenderbim.tool.collector import Collector
|
||||
from blenderbim.tool.container import Container
|
||||
from blenderbim.tool.context_editor import ContextEditor
|
||||
from blenderbim.tool.ifc import Ifc
|
||||
from blenderbim.tool.organisation_editor import OrganisationEditor
|
||||
@@ -25,4 +27,3 @@ from blenderbim.tool.owner import Owner
|
||||
from blenderbim.tool.person_editor import PersonEditor
|
||||
from blenderbim.tool.role_editor import RoleEditor
|
||||
from blenderbim.tool.surveyor import Surveyor
|
||||
from blenderbim.tool.container import Container
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.util.element
|
||||
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class Collector(blenderbim.core.tool.Collector):
|
||||
@classmethod
|
||||
def assign(cls, obj):
|
||||
for collection in obj.users_collection:
|
||||
collection.objects.unlink(obj)
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
aggregate = ifcopenshell.util.element.get_aggregate(element)
|
||||
if aggregate:
|
||||
aggregate_obj = tool.Ifc.get_object(aggregate)
|
||||
collection = bpy.data.collections.get(aggregate_obj.name)
|
||||
if collection:
|
||||
collection.objects.link(obj)
|
||||
return
|
||||
|
||||
container = ifcopenshell.util.element.get_container(element)
|
||||
if container:
|
||||
container_obj = tool.Ifc.get_object(container)
|
||||
collection = bpy.data.collections.get(container_obj.name)
|
||||
if collection:
|
||||
collection.objects.link(obj)
|
||||
return
|
||||
|
||||
project_obj = tool.Ifc.get_object(tool.Ifc.get().by_type("IfcProject")[0])
|
||||
if project_obj:
|
||||
collection = bpy.data.collections.get(project_obj.name)
|
||||
if collection:
|
||||
collection.objects.link(obj)
|
||||
return
|
||||
@@ -1,7 +1,26 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class Container(blenderbim.core.tool.Container):
|
||||
@classmethod
|
||||
def can_contain(cls, structure_obj, element_obj):
|
||||
|
||||
@@ -1,7 +1,26 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
|
||||
|
||||
class Owner(blenderbim.core.tool.Owner):
|
||||
@classmethod
|
||||
def set_user(cls, user):
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell.api
|
||||
import blenderbim.core.tool
|
||||
|
||||
@@ -4,3 +4,4 @@ markers =
|
||||
geometry
|
||||
owner
|
||||
void
|
||||
spatial
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@spatial
|
||||
Feature: Spatial
|
||||
Covers spatial containment management.
|
||||
|
||||
Scenario: Enable editing container
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
When I press "bim.enable_editing_container"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Disable editing container
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I press "bim.enable_editing_container"
|
||||
When I press "bim.disable_editing_container"
|
||||
Then nothing happens
|
||||
|
||||
Scenario: Assign container
|
||||
Given an empty IFC project
|
||||
And I add a cube
|
||||
And the object "Cube" is selected
|
||||
And I set "scene.BIMRootProperties.ifc_class" to "IfcWall"
|
||||
And I press "bim.assign_class"
|
||||
And the object "IfcWall/Cube" is selected
|
||||
And I press "bim.enable_editing_container"
|
||||
And the variable "site" is "tool.Ifc.get().by_type('IfcSite')[0].id()"
|
||||
When I press "bim.assign_container(structure={site})"
|
||||
Then the object "IfcWall/Cube" is in the collection "IfcSite/My Site"
|
||||
@@ -18,6 +18,7 @@
|
||||
|
||||
import os
|
||||
import bpy
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.bim.ifc import IfcStore
|
||||
from pytest_bdd import scenarios, given, when, then, parsers
|
||||
|
||||
@@ -103,8 +104,9 @@ def i_delete_the_selected_objects():
|
||||
bpy.ops.object.delete()
|
||||
|
||||
|
||||
@then(parsers.parse('the variable "{key}" is "{value}"'))
|
||||
@given(parsers.parse('the variable "{key}" is "{value}"'))
|
||||
@when(parsers.parse('the variable "{key}" is "{value}"'))
|
||||
@then(parsers.parse('the variable "{key}" is "{value}"'))
|
||||
def the_variable_key_is_value(key, value):
|
||||
variables[key] = eval(replace_variables(value))
|
||||
|
||||
@@ -122,7 +124,7 @@ def the_object_name_exists(name) -> bpy.types.Object:
|
||||
return obj
|
||||
|
||||
|
||||
@then('an IFC file exists')
|
||||
@then("an IFC file exists")
|
||||
def an_ifc_file_exists():
|
||||
ifc = IfcStore.get_file()
|
||||
if not ifc:
|
||||
@@ -255,3 +257,8 @@ def prop_is_value(prop, value):
|
||||
if not is_value:
|
||||
actual_value = eval(f"bpy.context.{prop}")
|
||||
assert False, f"Value is {actual_value}"
|
||||
|
||||
|
||||
@then(parsers.parse('the object "{name}" is in the collection "{collection}"'))
|
||||
def the_object_name_is_in_the_collection_collection(name, collection):
|
||||
assert collection in [c.name for c in the_object_name_exists(name).users_collection]
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blenderbim.core.geometry as subject
|
||||
from test.core.bootstrap import ifc, surveyor
|
||||
|
||||
|
||||
class TestEditObjectPlacement:
|
||||
def test_run(self, ifc, surveyor):
|
||||
ifc.get_entity("obj").should_be_called().will_return("element")
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import blenderbim.core.spatial as subject
|
||||
from test.core.bootstrap import ifc, collector, container, surveyor
|
||||
|
||||
|
||||
class TestAssignContainer:
|
||||
def test_run(self, ifc, collector, container, surveyor):
|
||||
container.can_contain("structure_obj", "element_obj").should_be_called().will_return(True)
|
||||
ifc.get_entity("structure_obj").should_be_called().will_return("structure")
|
||||
ifc.get_entity("element_obj").should_be_called().will_return("element")
|
||||
ifc.run(
|
||||
"spatial.assign_container", product="element", relating_structure="structure"
|
||||
).should_be_called().will_return("rel")
|
||||
surveyor.get_absolute_matrix("element_obj").should_be_called().will_return("matrix")
|
||||
ifc.run("geometry.edit_object_placement", product="element", matrix="matrix").should_be_called()
|
||||
container.disable_editing("element_obj").should_be_called()
|
||||
collector.assign("element_obj").should_be_called()
|
||||
assert (
|
||||
subject.assign_container(
|
||||
ifc, collector, container, surveyor, structure_obj="structure_obj", element_obj="element_obj"
|
||||
)
|
||||
== "rel"
|
||||
)
|
||||
|
||||
|
||||
class TestEnableEditingContainer:
|
||||
def test_run(self, container):
|
||||
container.enable_editing("obj").should_be_called()
|
||||
container.import_containers().should_be_called()
|
||||
subject.enable_editing_container(container, obj="obj")
|
||||
|
||||
|
||||
class TestDisableEditingContainer:
|
||||
def test_run(self, container):
|
||||
container.disable_editing("obj").should_be_called()
|
||||
subject.disable_editing_container(container, obj="obj")
|
||||
|
||||
|
||||
class TestChangeSpatialLevel:
|
||||
def test_run(self, container):
|
||||
container.import_containers(parent="parent").should_be_called()
|
||||
subject.change_spatial_level(container, parent="parent")
|
||||
|
||||
|
||||
class TestRemoveContainer:
|
||||
def test_run(self, ifc, collector):
|
||||
ifc.get_entity("obj").should_be_called().will_return("element")
|
||||
ifc.run("spatial.remove_container", product="element").should_be_called()
|
||||
collector.assign("obj").should_be_called()
|
||||
subject.remove_container(ifc, collector, obj="obj")
|
||||
@@ -0,0 +1,57 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import blenderbim.core.tool
|
||||
import blenderbim.tool as tool
|
||||
from blenderbim.tool.collector import Collector as subject
|
||||
from test.bim.bootstrap import NewFile
|
||||
|
||||
|
||||
class TestImplementsTool(NewFile):
|
||||
def test_run(self):
|
||||
assert isinstance(subject(), blenderbim.core.tool.Collector)
|
||||
|
||||
|
||||
class TestAssign(NewFile):
|
||||
def test_in_decomposition_mode_walls_are_placed_in_its_spatial_collection(self):
|
||||
bpy.ops.bim.create_project()
|
||||
wall_obj = bpy.data.objects.new("Object", None)
|
||||
wall_element = tool.Ifc.get().createIfcWall()
|
||||
tool.Ifc.link(wall_element, wall_obj)
|
||||
bpy.context.scene.collection.objects.link(wall_obj)
|
||||
ifcopenshell.api.run(
|
||||
"spatial.assign_container",
|
||||
tool.Ifc.get(),
|
||||
product=wall_element,
|
||||
relating_structure=tool.Ifc.get().by_type("IfcSite")[0],
|
||||
)
|
||||
subject.assign(wall_obj)
|
||||
assert len(wall_obj.users_collection) == 1
|
||||
assert "IfcSite" in wall_obj.users_collection[0].name
|
||||
|
||||
def test_in_decomposition_mode_walls_are_placed_in_the_project_if_not_decomposes(self):
|
||||
bpy.ops.bim.create_project()
|
||||
wall_obj = bpy.data.objects.new("Object", None)
|
||||
wall_element = tool.Ifc.get().createIfcWall()
|
||||
tool.Ifc.link(wall_element, wall_obj)
|
||||
bpy.context.scene.collection.objects.link(wall_obj)
|
||||
subject.assign(wall_obj)
|
||||
assert len(wall_obj.users_collection) == 1
|
||||
assert "IfcProject" in wall_obj.users_collection[0].name
|
||||
@@ -1,3 +1,21 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import blenderbim.core.tool
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import ifcopenshell
|
||||
import test.bim.bootstrap
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
# BlenderBIM Add-on - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# This file is part of BlenderBIM Add-on.
|
||||
#
|
||||
# BlenderBIM Add-on is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# BlenderBIM Add-on 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 General Public License
|
||||
# along with BlenderBIM Add-on. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import bpy
|
||||
import numpy as np
|
||||
import ifcopenshell
|
||||
|
||||
Reference in New Issue
Block a user