Finally, a way to actually add an IFC element directly

Previously, the way to add an IFC element would be to 1) create a blender element then assign or 2) activate bim tool, launch type manager, click create type, fill out field, or 3) using various menus for parametric stuff like stairs. This is now the new proposed way to do things and everything will consolidate to here - and it allows you to directly create non-mesh geometry immediately too!
This commit is contained in:
Dion Moult
2024-09-23 18:21:44 +10:00
parent 194acb078c
commit 23db38d0e9
11 changed files with 384 additions and 39 deletions
@@ -90,6 +90,8 @@ class ViewportData:
modes.append(item_mode)
elif tool.Geometry.is_representation_item(obj):
modes.append(edit_mode)
else: # A regular Blender object
modes.append(edit_mode)
return modes
@@ -115,17 +115,13 @@ class ItemDecorator:
continue
edges = selected_edges
verts = selected_verts
offset = len(selected_verts)
selected_verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices])
selected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
tris = selected_tris
else:
offset = len(unselected_verts)
unselected_verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices])
unselected_tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
edges = unselected_edges
verts = unselected_verts
i = len(verts)
tris = unselected_tris
i = len(verts)
matrix_world = obj.matrix_world
bbox_verts = [matrix_world @ Vector(co) for co in obj.bound_box]
bbox_edges = [
@@ -145,6 +141,16 @@ class ItemDecorator:
edges.extend(bbox_edges)
verts.extend(bbox_verts)
if (total_tris := len(obj.data.loop_triangles)) == 0:
continue
if total_tris > 1000: # For performance
continue
offset = len(verts)
verts.extend([tuple(obj.matrix_world @ v.co) for v in obj.data.vertices])
tris.extend([tuple([i + offset for i in t.vertices]) for t in obj.data.loop_triangles])
if unselected_verts:
self.draw_batch("LINES", unselected_verts, transparent_color(unselected_elements_color), unselected_edges)
self.draw_batch("TRIS", unselected_verts, transparent_color(special_elements_color), unselected_tris)
@@ -1631,6 +1631,8 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.bim.import_representation_items()
elif tool.Geometry.is_representation_item(obj):
self.enable_editing_representation_item(context, obj)
else: # A regular Blender object
self.enable_edit_mode(context)
def handle_multiple_selected_objects(self, context):
obj = context.active_object
@@ -2300,9 +2302,7 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
context.scene.BIMGeometryProperties.mode = "ITEM"
context.scene.BIMGeometryProperties.is_changing_mode = False
tool.Loader.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get())
tool.Loader.settings.context_settings = tool.Loader.create_settings()
tool.Loader.settings.gross_context_settings = tool.Loader.create_settings(is_gross=True)
tool.Loader.load_settings()
for item_id in set(obj.data["ios_item_ids"]):
item = tool.Ifc.get().by_id(item_id)
@@ -139,7 +139,7 @@ classes = (
ui.BIM_PT_roof,
ui.BIM_MT_type_manager_menu,
ui.LaunchTypeManager,
ui.BIM_MT_model,
ui.BIM_MT_elements,
grid.BIM_OT_add_object,
stair.BIM_OT_add_stair,
stair.AddStair,
@@ -216,8 +216,7 @@ def register():
bpy.types.Object.BIMRailingProperties = bpy.props.PointerProperty(type=prop.BIMRailingProperties)
bpy.types.Object.BIMRoofProperties = bpy.props.PointerProperty(type=prop.BIMRoofProperties)
bpy.types.VIEW3D_MT_mesh_add.append(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.append(ui.add_menu)
bpy.types.VIEW3D_MT_add.prepend(ui.add_menu)
bpy.app.handlers.load_post.append(handler.load_post)
workspace.load_custom_icons()
@@ -248,7 +247,6 @@ def unregister():
del bpy.types.Object.BIMRoofProperties
bpy.app.handlers.load_post.remove(handler.load_post)
bpy.types.VIEW3D_MT_mesh_add.remove(ui.add_mesh_object_menu)
bpy.types.VIEW3D_MT_add.remove(ui.add_menu)
workspace.unload_custom_icons()
+10 -19
View File
@@ -116,7 +116,8 @@ class LaunchTypeManager(bpy.types.Operator):
row.operator("bim.disable_add_type", icon="CANCEL", text="")
else:
row = self.layout.row()
row.operator("bim.enable_add_type", icon="ADD", text="Create New Type")
# row.operator("bim.enable_add_type", icon="ADD", text="Create New Type")
row.operator("bim.launch_add_element", icon="ADD", text="Create New Type")
flow = self.layout.grid_flow(row_major=True, columns=3, even_columns=True, even_rows=True, align=True)
@@ -656,26 +657,16 @@ class BIM_PT_roof(bpy.types.Panel):
row.operator("bim.add_roof", icon="ADD", text="")
class BIM_MT_model(Menu):
bl_idname = "BIM_MT_model"
bl_label = "Objects"
class BIM_MT_elements(Menu):
bl_idname = "BIM_MT_elements"
bl_label = "IFC Elements"
def draw(self, context):
layout = self.layout
layout.operator("bim.add_empty_type", text="Empty Type", icon="EMPTY_AXIS")
layout.operator("bim.add_potential_half_space_solid", text="Half Space Proxy", icon="ORIENTATION_NORMAL")
layout.operator("bim.add_potential_opening", text="Opening Proxy", icon="CUBE")
# TODO consolidate in Item mode UI then remove
self.layout.operator("bim.add_potential_half_space_solid", text="Half Space", icon="ORIENTATION_NORMAL")
self.layout.operator("bim.add_potential_opening", text="Opening", icon="CUBE")
def add_menu(self, context):
self.layout.menu("BIM_MT_model", icon="FILE_3D")
def add_mesh_object_menu(self, context):
if context.mode == "OBJECT":
self.layout.separator()
self.layout.operator("mesh.add_stair", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Stair (Untyped)")
self.layout.operator("mesh.add_window", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Window (Untyped)")
self.layout.operator("mesh.add_door", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Door (Untyped)")
self.layout.operator("mesh.add_railing", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Railing (Untyped)")
self.layout.operator("mesh.add_roof", icon_value=bonsai.bim.icons["IFC"].icon_id, text="Roof (Untyped)")
self.layout.operator("bim.launch_add_element", icon_value=bonsai.bim.icons["IFC"].icon_id, text="IFC Element")
self.layout.separator()
@@ -131,6 +131,7 @@ class CreateProject(bpy.types.Operator):
)
bonsai.bim.schema.reload(tool.Ifc.get().schema_identifier)
tool.Blender.register_toolbar()
tool.Root.reload_item_decorator()
def rollback(self, data):
IfcStore.file = None
@@ -20,10 +20,12 @@ import bpy
from . import ui, prop, operator
classes = (
operator.AddElement,
operator.AssignClass,
operator.CopyClass,
operator.DisableReassignClass,
operator.EnableReassignClass,
operator.LaunchAddElement,
operator.ReassignClass,
operator.UnlinkObject,
prop.BIMRootProperties,
+90 -3
View File
@@ -42,6 +42,7 @@ class IfcClassData:
cls.data["ifc_products"] = cls.ifc_products()
cls.data["ifc_classes"] = cls.ifc_classes()
cls.data["ifc_classes_suggestions"] = cls.ifc_classes_suggestions() # Call AFTER cls.ifc_classes()
cls.data["representation_template"] = cls.representation_template()
cls.data["contexts"] = cls.contexts()
cls.data["has_entity"] = cls.has_entity()
@@ -58,9 +59,7 @@ class IfcClassData:
"IfcElement",
"IfcSpatialElement",
"IfcSpatialElementType",
"IfcGroup",
"IfcStructuralItem",
"IfcContext",
"IfcAnnotation",
"IfcRelSpaceBoundary",
]
@@ -70,7 +69,6 @@ class IfcClassData:
"IfcElementType",
"IfcElement",
"IfcSpatialStructureElement",
"IfcGroup",
"IfcStructuralItem",
"IfcAnnotation",
"IfcRelSpaceBoundary",
@@ -130,6 +128,95 @@ class IfcClassData:
suggestions[ifc_class].append(suggestion_dict)
return suggestions
@classmethod
def representation_template(cls):
ifc_class = bpy.context.scene.BIMRootProperties.ifc_class
templates = [
("EMPTY", "No Geometry", "Start with an empty object"),
None,
]
if (
hasattr(bpy.context, "selected_objects")
and len(bpy.context.selected_objects) > 0
and (obj := bpy.context.active_object)
and obj.type == "MESH"
):
templates.append(
(
"OBJ",
"Tessellation From Active Selection",
"Use the actively selected object as a template to create a new tessellation",
)
)
templates.extend(
[
(
"MESH",
"Custom Tessellation",
"Create a basic tessellated or faceted cube",
),
(
"EXTRUSION",
"Custom Extruded Solid",
"An extrusion from an arbitrary profile",
),
]
)
if ifc_class.endswith("Type") or ifc_class.endswith("Style"):
templates.extend(
[
None,
(
"LAYERSET_AXIS2",
"Vertical Layers",
"For objects similar to walls, will automatically add IfcMaterialLayerSet",
),
(
"LAYERSET_AXIS3",
"Horizontal Layers",
"For objects similar to slabs, will automatically add IfcMaterialLayerSet",
),
(
"PROFILESET",
"Extruded Profile",
"Create profile type object, automatically defines IfcMaterialProfileSet with the first profile from library",
),
]
)
if ifc_class in ("IfcWindowType", "IfcWindowStyle", "IfcWindow"):
templates.extend([None, ("WINDOW", "Window", "Parametric window")])
elif ifc_class in ("IfcDoorType", "IfcDoorStyle", "IfcDoor"):
templates.extend([None, ("DOOR", "Door", "Parametric door")])
elif ifc_class in ("IfcStairType", "IfcStairFlightType", "IfcStair", "IfcStairFlight"):
templates.extend([None, ("STAIR", "Stair", "Parametric stair")])
elif ifc_class in ("IfcRailingType", "IfcRailing"):
templates.extend([None, ("RAILING", "Railing", "Parametric railing")])
elif ifc_class in ("IfcRoofType", "IfcRoof"):
templates.extend([None, ("ROOF", "Roof", "Parametric roof with a constant pitch")])
elif ifc_class and "Segment" in ifc_class:
templates.extend(
(
None,
(
"FLOW_SEGMENT_RECTANGULAR",
"Rectangular Distribution Segment",
"Works similarly to Profile, has distribution ports",
),
(
"FLOW_SEGMENT_CIRCULAR",
"Circular Distribution Segment",
"Works similarly to Profile, has distribution ports",
),
(
"FLOW_SEGMENT_CIRCULAR_HOLLOW",
"Circular Hollow Distribution Segment",
"Works similarly to Profile, has distribution ports",
),
)
)
return templates
@classmethod
def contexts(cls):
results = []
+243 -1
View File
@@ -17,6 +17,7 @@
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import bpy
import bmesh
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.util.schema
@@ -24,9 +25,12 @@ import ifcopenshell.util.element
import ifcopenshell.util.type
import bonsai.bim.handler
import bonsai.core.root as core
import bonsai.core.geometry
import bonsai.tool as tool
import bonsai.bim.module.root.prop as root_prop
from bonsai.bim.ifc import IfcStore
from bonsai.bim.helper import get_enum_items
from bonsai.bim.helper import get_enum_items, prop_with_search
from mathutils import Vector
class EnableReassignClass(bpy.types.Operator):
@@ -302,3 +306,241 @@ class CopyClass(bpy.types.Operator, tool.Ifc.Operator):
for obj in objects:
core.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=obj)
bonsai.bim.handler.refresh_ui_data()
class AddElement(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.add_element"
bl_label = "Add Element"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add an IFC physical product, construction type, and more"
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
def _invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def _execute(self, context):
props = context.scene.BIMRootProperties
predefined_type = (
props.userdefined_type if props.ifc_predefined_type == "USERDEFINED" else props.ifc_predefined_type
)
representation_template = props.representation_template
ifc_context = None
if get_enum_items(props, "contexts", context):
ifc_context = int(props.contexts or "0") or None
if ifc_context:
ifc_context = tool.Ifc.get().by_id(ifc_context)
if representation_template in (
"EMPTY",
"LAYERSET_AXIS2",
"LAYERSET_AXIS3",
"PROFILESET",
) or representation_template.startswith("FLOW_SEGMENT_"):
mesh = None
else:
mesh = bpy.data.meshes.new("Mesh")
obj = bpy.data.objects.new(props.ifc_class[3:], mesh)
obj.location = bpy.context.scene.cursor.location
element = core.assign_class(
tool.Ifc,
tool.Collector,
tool.Root,
obj=obj,
ifc_class=props.ifc_class,
predefined_type=predefined_type,
should_add_representation=False,
)
if representation_template == "EMTPY" or not ifc_context:
pass
elif (
representation_template == "OBJ"
and (template_obj := context.active_object)
and template_obj.type == "MESH"
and len(template_obj.data.vertices)
):
obj.matrix_world = template_obj.matrix_world
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=0.5)
verts = [v.co / unit_scale for v in template_obj.data.vertices]
faces = [p.vertices[:] for p in template_obj.data.polygons]
item = builder.mesh(verts, faces)
bm.free()
representation = builder.get_representation(ifc_context, [item])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
if not tool.Ifc.get_entity(template_obj):
bpy.data.objects.remove(template_obj)
elif representation_template == "MESH":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
bm = bmesh.new()
bmesh.ops.create_cube(bm, size=0.5)
verts = [v.co / unit_scale for v in bm.verts]
faces = [[v.index for v in p.verts] for p in bm.faces]
item = builder.mesh(verts, faces)
bm.free()
representation = builder.get_representation(ifc_context, [item])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template == "EXTRUSION":
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
curve = builder.rectangle(size=Vector((0.5, 0.5)) / unit_scale)
item = builder.extrude(curve, magnitude=0.5 / unit_scale)
representation = builder.get_representation(ifc_context, [item])
ifcopenshell.api.geometry.assign_representation(tool.Ifc.get(), element, representation)
bonsai.core.geometry.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
)
elif representation_template in ("LAYERSET_AXIS2", "LAYERSET_AXIS3"):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
materials = tool.Ifc.get().by_type("IfcMaterial")
if materials:
material = materials[0] # Arbitrarily pick a material
else:
material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown")
rel = ifcopenshell.api.run(
"material.assign_material", tool.Ifc.get(), products=[element], type="IfcMaterialLayerSet"
)
layer_set = rel.RelatingMaterial
layer = ifcopenshell.api.run("material.add_layer", tool.Ifc.get(), layer_set=layer_set, material=material)
thickness = 0.1 # Arbitrary metric thickness for now
layer.LayerThickness = thickness / unit_scale
pset = ifcopenshell.api.run("pset.add_pset", tool.Ifc.get(), product=element, name="EPset_Parametric")
if representation_template == "LAYERSET_AXIS2":
axis = "AXIS2"
elif representation_template == "LAYERSET_AXIS3":
axis = "AXIS3"
ifcopenshell.api.run("pset.edit_pset", tool.Ifc.get(), pset=pset, properties={"LayerSetDirection": axis})
elif representation_template == "PROFILESET" or representation_template.startswith("FLOW_SEGMENT_"):
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
materials = tool.Ifc.get().by_type("IfcMaterial")
if materials:
material = materials[0] # Arbitrarily pick a material
else:
material = ifcopenshell.api.run("material.add_material", tool.Ifc.get(), name="Unknown")
if representation_template == "PROFILESET":
named_profiles = [p for p in tool.Ifc.get().by_type("IfcProfileDef") if p.ProfileName]
if named_profiles:
profile = named_profiles[0]
else:
size = 0.5 / unit_scale
profile = tool.Ifc.get().create_entity(
"IfcRectangleProfileDef", ProfileName="New Profile", ProfileType="AREA", XDim=size, YDim=size
)
else:
# NOTE: defaults dims are in meters / mm
# for now default names are hardcoded to mm
if representation_template == "FLOW_SEGMENT_RECTANGULAR":
default_x_dim = 0.4
default_y_dim = 0.2
profile_name = f"{props.ifc_class}-{default_x_dim*1000}x{default_y_dim*1000}"
profile = tool.Ifc.get().create_entity(
"IfcRectangleProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
XDim=default_x_dim / unit_scale,
YDim=default_y_dim / unit_scale,
)
elif representation_template == "FLOW_SEGMENT_CIRCULAR":
default_diameter = 0.1
profile_name = f"{props.ifc_class}-{default_diameter*1000}"
profile = tool.Ifc.get().create_entity(
"IfcCircleProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
Radius=(default_diameter / 2) / unit_scale,
)
elif representation_template == "FLOW_SEGMENT_CIRCULAR_HOLLOW":
default_diameter = 0.15
default_thickness = 0.005
profile_name = f"{props.ifc_class}-{default_diameter*1000}x{default_thickness*1000}"
profile = tool.Ifc.get().create_entity(
"IfcCircleHollowProfileDef",
ProfileName=profile_name,
ProfileType="AREA",
Radius=(default_diameter / 2) / unit_scale,
WallThickness=default_thickness,
)
rel = ifcopenshell.api.run(
"material.assign_material", tool.Ifc.get(), products=[element], type="IfcMaterialProfileSet"
)
profile_set = rel.RelatingMaterial
material_profile = ifcopenshell.api.run(
"material.add_profile", tool.Ifc.get(), profile_set=profile_set, material=material
)
ifcopenshell.api.run(
"material.assign_profile", tool.Ifc.get(), material_profile=material_profile, profile=profile
)
elif representation_template == "WINDOW":
with context.temp_override(active_object=obj):
bpy.ops.bim.add_window()
elif representation_template == "DOOR":
with context.temp_override(active_object=obj):
bpy.ops.bim.add_door()
elif representation_template == "STAIR":
with context.temp_override(active_object=obj):
bpy.ops.bim.add_stair()
elif representation_template == "RAILING":
with context.temp_override(active_object=obj):
bpy.ops.bim.add_railing()
elif representation_template == "ROOF":
with context.temp_override(active_object=obj):
bpy.ops.bim.add_roof()
def draw(self, context):
props = context.scene.BIMRootProperties
self.layout.use_property_split = True
self.layout.use_property_decorate = False
prop_with_search(self.layout, props, "ifc_product", text="Definition")
prop_with_search(self.layout, props, "ifc_class", should_click_ok_to_validate=True)
ifc_predefined_types = root_prop.get_ifc_predefined_types(context.scene.BIMRootProperties, context)
if ifc_predefined_types:
prop_with_search(self.layout, props, "ifc_predefined_type")
if props.ifc_predefined_type == "USERDEFINED":
row = self.layout.row()
row.prop(props, "ifc_userdefined_type")
prop_with_search(self.layout, props, "representation_template", text="Representation")
if props.representation_template != "EMPTY":
prop_with_search(self.layout, props, "contexts")
class LaunchAddElement(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.launch_add_element"
bl_label = "Add Element"
bl_options = {"REGISTER", "UNDO"}
bl_description = "Add an IFC physical product, construction type, and more"
def execute(self, context):
bpy.ops.bim.add_element("INVOKE_DEFAULT")
return {"FINISHED"}
+8 -1
View File
@@ -41,6 +41,12 @@ def get_ifc_predefined_types(self, context):
return IfcClassData.data["ifc_predefined_types"]
def get_representation_template(self, context):
if not IfcClassData.is_loaded:
IfcClassData.load()
return IfcClassData.data["representation_template"]
def refresh_classes(self, context):
enum = get_ifc_classes(self, context)
context.scene.BIMRootProperties.ifc_class = enum[0][0]
@@ -110,11 +116,12 @@ def is_object_class_applicable(self, obj):
class BIMRootProperties(PropertyGroup):
contexts: EnumProperty(items=get_contexts, name="Contexts")
contexts: EnumProperty(items=get_contexts, name="Contexts", options=set())
ifc_product: EnumProperty(items=get_ifc_products, name="Products", update=refresh_classes)
ifc_class: EnumProperty(items=get_ifc_classes, name="Class", update=refresh_predefined_types)
ifc_predefined_type: EnumProperty(items=get_ifc_predefined_types, name="Predefined Type", default=None)
ifc_userdefined_type: StringProperty(name="Userdefined Type")
representation_template: bpy.props.EnumProperty(items=get_representation_template, name="Representation Template", default=0)
relating_class_object: PointerProperty(
type=bpy.types.Object,
name="Copy Class",
+10 -1
View File
@@ -19,9 +19,10 @@
from __future__ import annotations
import os
import re
import math
import bpy
import math
import bmesh
import logging
import ifcopenshell.geom
import ifcopenshell.util.element
import ifcopenshell.util.geolocation
@@ -56,6 +57,14 @@ class Loader(bonsai.core.tool.Loader):
def set_unit_scale(cls, unit_scale: float) -> None:
cls.unit_scale = unit_scale
@classmethod
def load_settings(cls) -> None:
logger = logging.getLogger("ImportIFC")
cls.settings = bonsai.bim.import_ifc.IfcImportSettings.factory(bpy.context, None, logger)
cls.settings.contexts = ifcopenshell.util.representation.get_prioritised_contexts(tool.Ifc.get())
cls.settings.context_settings = cls.create_settings()
cls.settings.gross_context_settings = cls.create_settings(is_gross=True)
@classmethod
def set_settings(cls, settings: bonsai.bim.import_ifc.IfcImportSettings) -> None:
cls.settings = settings