Basic support for deleting items, and editing meshlike items

This commit is contained in:
Dion Moult
2024-09-18 21:35:18 +10:00
parent ed530f737c
commit a1d99b544a
8 changed files with 218 additions and 142 deletions
+1 -40
View File
@@ -1370,46 +1370,7 @@ class IfcImporter:
verts = geometry.verts
mesh["has_cartesian_point_offset"] = False
if geometry.faces:
num_vertices = len(verts) // 3
total_faces = len(geometry.faces)
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(geometry.faces)
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
#
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh["ios_item_ids"] = ifcopenshell.util.shape.get_representation_item_ids(geometry).tolist()
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry.faces)
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
mesh.update()
rep_str: str = geometry.id
if "openings" not in rep_str:
rep_id = rep_str.split("-", 1)[0]
rep = self.file.by_id(int(rep_id))
tool.Loader.load_indexed_colour_map(rep, mesh)
else:
e = geometry.edges
v = verts
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
mesh.from_pydata(vertices, edges, [])
mesh["ios_materials"] = [m.instance_id() for m in geometry.materials]
mesh["ios_material_ids"] = geometry.material_ids
return mesh
return tool.Loader.convert_geometry_to_mesh(geometry, mesh, verts=verts)
except:
self.ifc_import_settings.logger.error("Could not create mesh for %s", element)
import traceback
@@ -47,18 +47,18 @@ class ViewportData:
@classmethod
def mode(cls):
obj = bpy.context.active_object
obj_mode = [("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0)]
mesh_modes = [
modes = [
("OBJECT", "IFC Object Mode", "", "OBJECT_DATAMODE", 0),
("EDIT", "IFC Edit Mode", "", "EDITMODE_HLT", 1),
("ITEM", "IFC Item Mode", "", "MESH_DATA", 1),
]
if (
not obj
or not tool.Blender.is_editable(obj)
or ((element := tool.Ifc.get_entity(obj)) and tool.Geometry.is_locked(element))
):
return obj_mode
return mesh_modes
return modes
modes.append(("EDIT", "IFC Edit Mode", "", "EDITMODE_HLT", 2))
return modes
class RepresentationsData:
@@ -18,7 +18,6 @@
import blf
import gpu
import json
import bmesh
import bonsai.tool as tool
from bpy.types import SpaceView3D
@@ -125,9 +124,25 @@ class ItemDecorator:
edges = unselected_edges
verts = unselected_verts
i = len(verts)
edges.extend([[ei + i for ei in e] for e in json.loads(item.edges)])
matrix_world = obj.matrix_world
verts.extend([matrix_world @ Vector(v) for v in json.loads(item.verts)])
bbox_verts = [matrix_world @ Vector(co) for co in obj.bound_box]
bbox_edges = [
(0 + i, 3 + i),
(3 + i, 7 + i),
(7 + i, 4 + i),
(4 + i, 0 + i),
(0 + i, 1 + i),
(3 + i, 2 + i),
(7 + i, 6 + i),
(4 + i, 5 + i),
(1 + i, 2 + i),
(2 + i, 6 + i),
(6 + i, 5 + i),
(5 + i, 1 + i),
]
edges.extend(bbox_edges)
verts.extend(bbox_verts)
if unselected_verts:
self.draw_batch("LINES", unselected_verts, transparent_color(unselected_elements_color), unselected_edges)
@@ -16,12 +16,11 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
import re
import bpy
import bmesh
import logging
import numpy as np
import json
import re
import ifcopenshell
import ifcopenshell.util.unit
import ifcopenshell.util.element
@@ -637,6 +636,8 @@ class OverrideDelete(bpy.types.Operator):
self.report({"INFO"}, "Elements that are part of an array cannot be deleted.")
return {"FINISHED"}
tool.Geometry.delete_ifc_object(obj)
elif tool.Geometry.is_representation_item(obj):
tool.Geometry.delete_ifc_item(obj)
else:
bpy.data.objects.remove(obj)
@@ -1591,6 +1592,12 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
return bpy.ops.bim.enable_editing_boundary_geometry()
for obj in selected_objs:
if obj == context.scene.BIMGeometryProperties.representation_obj:
self.report({"ERROR"}, f"Element '{obj.name}' is in item mode and cannot be edited directly.")
obj.select_set(False)
continue
if tool.Geometry.is_representation_item(obj):
return self.enable_editing_representation_item(context, obj)
if not tool.Blender.is_editable(obj):
self.report({"INFO"}, f"Object cannot be edited: {obj.name}")
obj.select_set(False)
@@ -1694,6 +1701,21 @@ class OverrideModeSetEdit(bpy.types.Operator, tool.Ifc.Operator):
context.view_layer.objects.active = active_obj
return {"FINISHED"}
def enable_editing_representation_item(self, context, obj):
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if tool.Geometry.is_meshlike_item(item):
tool.Blender.select_and_activate_single_object(context, obj)
obj.data.BIMMeshProperties.mesh_checksum = tool.Geometry.get_mesh_checksum(obj.data)
if tool.Blender.toggle_edit_mode(context) == {"CANCELLED"}:
return {"CANCELLED"}
context.scene.BIMGeometryProperties.is_changing_mode = True
if context.scene.BIMGeometryProperties.mode != "EDIT":
context.scene.BIMGeometryProperties.mode = "EDIT"
context.scene.BIMGeometryProperties.is_changing_mode = False
return {"FINISHED"}
else:
self.report({"INFO"}, f"Editing {item.is_a()} geometry is not supported")
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
@@ -1716,28 +1738,49 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
if self.should_save:
bpy.ops.bim.update_representation(obj=obj.name, ifc_representation_class="")
if getattr(tool.Ifc.get_entity(obj), "HasOpenings", False):
self.reload_representation(obj)
tool.Geometry.reload_representation(obj)
else:
self.reload_representation(obj)
tool.Geometry.reload_representation(obj)
tool.Ifc.finish_edit(obj)
for obj in self.unchanged_objs_with_openings:
self.reload_representation(obj)
tool.Geometry.reload_representation(obj)
tool.Ifc.finish_edit(obj)
return {"FINISHED"}
def reload_representation(self, obj):
representation = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
core.switch_representation(
tool.Ifc,
tool.Geometry,
obj=obj,
representation=representation,
should_reload=True,
is_global=True,
should_sync_changes_first=False,
apply_openings=True,
)
def edit_representation_item(self, obj):
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
if tool.Geometry.is_meshlike_item(item):
if tool.Geometry.has_geometric_data(obj) and obj.data.polygons:
if obj.data.BIMMeshProperties.mesh_checksum == tool.Geometry.get_mesh_checksum(obj.data):
return {"FINISHED"}
builder = ifcopenshell.util.shape_builder.ShapeBuilder(tool.Ifc.get())
unit_scale = ifcopenshell.util.unit.calculate_unit_scale(tool.Ifc.get())
verts = [v.co / unit_scale for v in obj.data.vertices]
faces = [p.vertices[:] for p in obj.data.polygons]
if item.is_a("IfcAdvancedBrep"):
new_item = builder.faceted_brep(verts, faces)
else:
new_item = builder.mesh(verts, faces)
for inverse in tool.Ifc.get().get_inverse(item):
ifcopenshell.util.element.replace_attribute(inverse, item, new_item)
ifcopenshell.util.element.remove_deep2(tool.Ifc.get(), item)
obj.data.BIMMeshProperties.ifc_definition_id = new_item.id()
tool.Geometry.reload_representation(bpy.context.scene.BIMGeometryProperties.representation_obj)
return {"FINISHED"}
else:
self.reload_item(obj)
return {"FINISHED"}
return {"FINISHED"}
def reload_item(self, obj):
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)
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
geometry = tool.Loader.create_generic_shape(item)
obj.data.clear_geometry()
tool.Loader.convert_geometry_to_mesh(geometry, obj.data)
def invoke(self, context, event):
return IfcStore.execute_ifc_operator(self, context, is_invoke=True)
@@ -1750,8 +1793,12 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
bpy.ops.object.mode_set(mode="EDIT", toggle=True)
context.scene.BIMGeometryProperties.is_changing_mode = True
if context.scene.BIMGeometryProperties.mode != "OBJECT":
context.scene.BIMGeometryProperties.mode = "OBJECT"
if context.scene.BIMGeometryProperties.representation_obj:
if context.scene.BIMGeometryProperties.mode != "ITEM":
context.scene.BIMGeometryProperties.mode = "ITEM"
else:
if context.scene.BIMGeometryProperties.mode != "OBJECT":
context.scene.BIMGeometryProperties.mode = "OBJECT"
context.scene.BIMGeometryProperties.is_changing_mode = False
if not tool.Ifc.get():
@@ -1761,6 +1808,8 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator):
element = tool.Ifc.get_entity(context.active_object)
if element and element.is_a("IfcRelSpaceBoundary"):
return bpy.ops.bim.edit_boundary_geometry()
elif tool.Geometry.is_representation_item(context.active_object):
return self.edit_representation_item(context.active_object)
objs = context.selected_objects or [context.active_object]
@@ -1909,7 +1958,6 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.remove_representation_item"
bl_label = "Remove Representation Item"
bl_options = {"REGISTER", "UNDO"}
representation_item_id: bpy.props.IntProperty()
@classmethod
@@ -1923,7 +1971,6 @@ class RemoveRepresentationItem(bpy.types.Operator, tool.Ifc.Operator):
def _execute(self, context):
obj = context.active_object
props = obj.BIMGeometryProperties
ifc_file = tool.Ifc.get()
representation_item = ifc_file.by_id(self.representation_item_id)
@@ -2217,7 +2264,7 @@ class RemoveRepresentationItemFromShapeAspect(bpy.types.Operator, tool.Ifc.Opera
class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
bl_idname = "bim.xxx_import_representation_items"
bl_idname = "bim.import_representation_items"
bl_label = "Import Representation Items"
bl_options = {"REGISTER", "UNDO"}
@@ -2225,85 +2272,38 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator):
obj = context.active_object
tool.Geometry.apply_item_ids_as_vertex_groups(obj)
tool.Geometry.dissolve_triangulated_edges(obj)
bm_dict = self.separate_faces_by_vertex_group(obj)
# bm_dict = self.separate_faces_by_id(obj)
props = context.scene.BIMGeometryProperties
props.item_objs.clear()
props.representation_obj = obj
tool.Geometry.lock_object(obj)
for item_id, bm in bm_dict.items():
item_mesh = bpy.data.meshes.new(f"mesh_id_{item_id}")
bm.to_mesh(item_mesh)
bm.free()
context.scene.BIMGeometryProperties.is_changing_mode = True
if context.scene.BIMGeometryProperties.mode != "ITEM":
context.scene.BIMGeometryProperties.mode = "ITEM"
context.scene.BIMGeometryProperties.is_changing_mode = False
item_ids = set()
for vgroup in obj.vertex_groups:
if vgroup.name.startswith("ios_item_id_"):
item_ids.add(int(vgroup.name.split("_")[-1]))
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)
for item_id in item_ids:
item = tool.Ifc.get().by_id(item_id)
item_mesh = bpy.data.meshes.new(f"Item/{item.is_a()}/{item_id}")
item_mesh.BIMMeshProperties.ifc_definition_id = item_id
geometry = tool.Loader.create_generic_shape(item)
tool.Loader.convert_geometry_to_mesh(geometry, item_mesh)
item_obj = bpy.data.objects.new(f"Item/{item.is_a()}/{item_id}", item_mesh)
item_obj.matrix_world = obj.matrix_world
item_obj.show_in_front = True
bpy.context.collection.objects.link(item_obj)
if tool.Geometry.is_meshlike_item(item):
tool.Geometry.lock_object(item_obj)
new = props.item_objs.add()
new.obj = item_obj
verts = [list(co) for co in item_obj.bound_box]
edges = [(0, 3), (3, 7), (7, 4), (4, 0), (0, 1), (3, 2), (7, 6), (4, 5), (1, 2), (2, 6), (6, 5), (5, 1)]
new.verts = json.dumps(verts)
new.edges = json.dumps(edges)
def separate_faces_by_id(self, obj):
mesh = obj.data
bm = bmesh.new()
bm.from_mesh(mesh)
results = {}
face_ids = mesh["ios_item_ids"]
unique_ids = set(face_ids)
for item_id in unique_ids:
item_bm = bmesh.new()
for face, face_id in zip(bm.faces, face_ids):
if face_id == item_id:
# Copy the face and its vertices into the new BMesh
new_face_verts = [item_bm.verts.new(v.co) for v in face.verts]
item_bm.faces.new(new_face_verts)
item_bm.verts.ensure_lookup_table()
item_bm.faces.ensure_lookup_table()
results[item_id] = item_bm
return results
def separate_faces_by_vertex_group(self, obj):
mesh = obj.data
bm = bmesh.new()
bm.from_mesh(mesh)
# Ensure the bmesh has up-to-date vertex weights (vertex groups)
bm.verts.layers.deform.verify()
results = {}
for vgroup in obj.vertex_groups:
group_bm = bmesh.new()
deform_layer = bm.verts.layers.deform.active
# Iterate over the faces and check if all vertices of the face belong to the vertex group
for face in bm.faces:
face_in_group = True
for vert in face.verts:
# Get the vertex groups the vertex belongs to
deform = vert[deform_layer]
# If this vertex does not belong to the current vertex group, mark the face as outside the group
if vgroup.index not in deform:
face_in_group = False
break
# If all vertices of the face belong to the current vertex group, add the face to the new BMesh
if face_in_group:
new_face_verts = [group_bm.verts.new(v.co) for v in face.verts]
group_bm.faces.new(new_face_verts)
# Ensure the mesh is valid and doesn't have duplicate elements
group_bm.verts.ensure_lookup_table()
group_bm.faces.ensure_lookup_table()
results[int(vgroup.name.split("_")[3])] = group_bm
return results
@@ -43,11 +43,23 @@ def update_mode(self, context):
if self.is_changing_mode:
return
if self.mode == "OBJECT":
if self.representation_obj:
tool.Geometry.unlock_object(self.representation_obj)
self.representation_obj = None
bpy.ops.bim.override_mode_set_object("INVOKE_DEFAULT")
elif self.mode == "ITEM":
bpy.ops.bim.import_representation_items()
elif self.mode == "EDIT":
bpy.ops.bim.override_mode_set_edit("INVOKE_DEFAULT")
def update_representation_obj(self, context):
for item_obj in self.item_objs:
if item_obj.obj:
bpy.data.objects.remove(item_obj.obj)
self.item_objs.clear()
def get_mode(self, context):
if not ViewportData.is_loaded:
ViewportData.load()
@@ -147,6 +159,7 @@ class BIMGeometryProperties(PropertyGroup):
should_force_triangulation: BoolProperty(name="Force Triangulation", default=False)
is_changing_mode: BoolProperty(name="Is Changing Mode", default=False)
mode: EnumProperty(items=get_mode, name="IFC Interaction Mode", update=update_mode)
representation_obj: PointerProperty(name="Representation Object", type=bpy.types.Object, update=update_representation_obj)
item_objs: CollectionProperty(name="Item Objects", type=RepresentationItemObject)
def is_object_valid_for_representation_copy(self, obj: bpy.types.Object) -> bool:
+5 -1
View File
@@ -456,7 +456,11 @@ class BIM_PT_tabs(Panel):
op.uri = "https://docs.bonsaibim.org/guides/troubleshooting.html#saving-and-loading-blend-files"
row.operator("bim.close_blend_warning", text="", icon="CANCEL")
if not context.mode.startswith(context.scene.BIMGeometryProperties.mode):
if context.mode == "OBJECT" and context.scene.BIMGeometryProperties.mode in ("OBJECT", "ITEM"):
pass
elif context.mode.startswith("EDIT") and context.scene.BIMGeometryProperties.mode == "EDIT":
pass
else:
box = self.layout.box()
box.alert = True
row = box.row(align=True)
+38
View File
@@ -22,6 +22,7 @@ import struct
import hashlib
import logging
import numpy as np
import multiprocessing
import ifcopenshell
import ifcopenshell.api
import ifcopenshell.api.grid
@@ -114,6 +115,20 @@ class Geometry(bonsai.core.tool.Geometry):
obj.lock_location = (False, False, False)
obj.lock_rotation = (False, False, False)
@classmethod
def delete_ifc_item(cls, obj: bpy.types.Object) -> None:
props = bpy.context.scene.BIMGeometryProperties
if len(props.item_objs) == 1:
return
for i, item_obj in enumerate(props.item_objs):
if item_obj.obj == obj:
props.item_objs.remove(i)
break
item = tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
cls.remove_representation_item(item)
cls.reload_representation(props.representation_obj)
bpy.data.objects.remove(obj)
@classmethod
def delete_ifc_object(cls, obj: bpy.types.Object) -> None:
element = tool.Ifc.get_entity(obj)
@@ -184,6 +199,8 @@ class Geometry(bonsai.core.tool.Geometry):
bonsai.core.spatial.import_spatial_decomposition(tool.Spatial)
try:
obj.name
if bpy.context.scene.BIMGeometryProperties.representation_obj == obj:
bpy.context.scene.BIMGeometryProperties.representation_obj = None
bpy.data.objects.remove(obj)
except:
pass
@@ -401,6 +418,11 @@ class Geometry(bonsai.core.tool.Geometry):
if obj.data and hasattr(obj.data, "BIMMeshProperties") and obj.data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(obj.data.BIMMeshProperties.ifc_definition_id)
@classmethod
def get_data_representation(cls, data: bpy.types.Mesh) -> ifcopenshell.entity_instance | None:
if hasattr(data, "BIMMeshProperties") and data.BIMMeshProperties.ifc_definition_id:
return tool.Ifc.get().by_id(data.BIMMeshProperties.ifc_definition_id)
@classmethod
def get_active_representation_context(cls, obj: bpy.types.Object) -> ifcopenshell.entity_instance:
active_representation = tool.Geometry.get_active_representation(obj)
@@ -806,6 +828,10 @@ class Geometry(bonsai.core.tool.Geometry):
return True
return False
@classmethod
def is_meshlike_item(cls, item: ifcopenshell.entity_instance) -> bool:
return item.is_a("IfcTessellatedItem") or item.is_a("IfcManifoldSolidBrep")
@classmethod
def is_profile_based(cls, data: bpy.types.Mesh) -> bool:
return data.BIMMeshProperties.subshape_type == "PROFILE"
@@ -816,6 +842,14 @@ class Geometry(bonsai.core.tool.Geometry):
"SweptSolid",
)
@classmethod
def is_representation_item(cls, obj: bpy.types.Object) -> bool:
return (
obj.data
and (ifc_id := obj.data.BIMMeshProperties.ifc_definition_id)
and tool.Ifc.get().by_id(ifc_id).is_a("IfcRepresentationItem")
)
@classmethod
def is_text_literal(cls, representation: ifcopenshell.entity_instance) -> bool:
items = ifcopenshell.util.representation.resolve_items(representation)
@@ -1365,3 +1399,7 @@ class Geometry(bonsai.core.tool.Geometry):
use_immediate_repr = apply_openings and bool(getattr(element, "HasOpenings", None))
use_immediate_repr = use_immediate_repr or cls.has_material_style_override(element)
return use_immediate_repr
@classmethod
def get_elements_by_representation(cls, representation):
return ifcopenshell.util.element.get_elements_by_representation(tool.Ifc.get(), representation)
+45
View File
@@ -879,3 +879,48 @@ class Loader(bonsai.core.tool.Loader):
float(props.blender_x_axis_ordinate),
)
return Matrix(matrix.tolist())
@classmethod
def convert_geometry_to_mesh(cls, geometry, mesh: bpy.types.Mesh, verts=None) -> bpy.types.Mesh:
if verts is None:
verts = geometry.verts
if geometry.faces:
num_vertices = len(verts) // 3
total_faces = len(geometry.faces)
loop_start = range(0, total_faces, 3)
num_loops = total_faces // 3
loop_total = [3] * num_loops
num_vertex_indices = len(geometry.faces)
# See bug 3546
# ios_edges holds true edges that aren't triangulated.
#
# we do `.tolist()` because Blender can't assign `np.int32` to it's custom attributes
mesh["ios_edges"] = list(set(tuple(e) for e in ifcopenshell.util.shape.get_edges(geometry).tolist()))
mesh["ios_item_ids"] = ifcopenshell.util.shape.get_representation_item_ids(geometry).tolist()
mesh.vertices.add(num_vertices)
mesh.vertices.foreach_set("co", verts)
mesh.loops.add(num_vertex_indices)
mesh.loops.foreach_set("vertex_index", geometry.faces)
mesh.polygons.add(num_loops)
mesh.polygons.foreach_set("loop_start", loop_start)
mesh.polygons.foreach_set("loop_total", loop_total)
mesh.polygons.foreach_set("use_smooth", [0] * total_faces)
mesh.update()
# TODO: geometry id is not always an int.
rep_id = geometry.id
if rep_id.isdigit():
if (rep := tool.Ifc.get().by_id(int(rep_id))) and rep.is_a("IfcShapeRepresentation"):
tool.Loader.load_indexed_colour_map(rep, mesh)
else:
e = geometry.edges
v = verts
vertices = [[v[i], v[i + 1], v[i + 2]] for i in range(0, len(v), 3)]
edges = [[e[i], e[i + 1]] for i in range(0, len(e), 2)]
mesh.from_pydata(vertices, edges, [])
mesh["ios_materials"] = [m.instance_id() for m in geometry.materials]
mesh["ios_material_ids"] = geometry.material_ids
return mesh