bim.select_linked_model_element

Refactored methods for accessing objects in linked models and added a simple operator to select object in linked model by providing guid.

A quick demo - https://files.catbox.moe/sjjw37.mp4
This commit is contained in:
Andrej730
2026-03-05 15:26:27 +05:00
parent ecc82a52f5
commit 9d0c172a53
4 changed files with 219 additions and 113 deletions
@@ -66,6 +66,7 @@ classes = (
operator.RewindLibrary,
operator.SaveLibraryFile,
operator.SelectLibraryFile,
operator.SelectLinkedModelElement,
operator.SelectLinkHandle,
operator.ToggleFilterCategories,
operator.ToggleLinkSelectability,
+47 -109
View File
@@ -27,7 +27,7 @@ import traceback
from collections import defaultdict
from math import radians
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal, Union, get_args
from typing import TYPE_CHECKING, Literal, Union, get_args
import bpy
import ifcopenshell
@@ -49,6 +49,10 @@ import ifcopenshell.util.unit
import numpy as np
from bpy.app.handlers import persistent
from bpy_extras.io_utils import ExportHelper, ImportHelper
from bpy_extras.view3d_utils import (
region_2d_to_origin_3d,
region_2d_to_vector_3d,
)
from mathutils import Matrix, Vector
import bonsai.bim.handler
@@ -1797,6 +1801,43 @@ class SelectLinkHandle(bpy.types.Operator):
return {"FINISHED"}
class SelectLinkedModelElement(bpy.types.Operator):
bl_idname = "bim.select_linked_model_element"
bl_label = "Select Linked Model Element"
bl_options = {"REGISTER"}
bl_description = "Select an element in the currently selected linked model by providing GlobalId."
guid: bpy.props.StringProperty(name="GlobalId") # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
guid: str
def invoke(self, context, event):
assert context.window_manager
return context.window_manager.invoke_props_dialog(self)
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
guid = self.guid.strip()
if not guid:
self.report({"ERROR"}, "GlobalId is not provided.")
return {"CANCELLED"}
props = tool.Project.get_project_props()
active_link = props.active_link
assert active_link is not None
assert active_link.is_loaded
guid_obj = tool.Project.Link.get_obj_by_guid(active_link, guid)
if not guid_obj:
filepath = active_link.filepath
self.report({"INFO"}, f"Element with GlobalId '{guid}' not found in the linked model at '{filepath}'.")
return {"CANCELLED"}
tool.Project.Link.select_linked_element(context, guid_obj, guid)
self.report({"INFO"}, f"Element with GlobalId '{guid}' is selected.")
return {"FINISHED"}
class ExportIFC(bpy.types.Operator, ExportHelper):
bl_idname = "bim.save_project"
bl_label = "Save IFC"
@@ -2278,21 +2319,10 @@ class QueryLinkedElement(bpy.types.Operator):
@classmethod
def poll(cls, context):
assert context.area
return context.area.type == "VIEW_3D"
def execute(self, context):
import sqlite3
from bpy_extras.view3d_utils import (
region_2d_to_origin_3d,
region_2d_to_vector_3d,
)
from ifcpatch.recipes.ExtractPropertiesToSQLite import (
ElementRow,
PropertyRow,
RelationshipRow,
)
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
LinksData.linked_data = {}
props = tool.Project.get_project_props()
props.queried_obj = None
@@ -2319,105 +2349,18 @@ class QueryLinkedElement(bpy.types.Operator):
self.report({"INFO"}, "No object found.")
return {"FINISHED"}
if "guids" not in obj:
if not tool.Project.Link.is_linked_element(obj):
self.report({"INFO"}, "Object is not a linked IFC element.")
return {"FINISHED"}
guid = None
guid_start_index = 0
guid_ids: list[int] = obj["guid_ids"]
for i, guid_end_index in enumerate(guid_ids):
if face_index < guid_end_index:
guid = obj["guids"][i]
props.queried_obj = obj
props.queried_obj_root = self.find_obj_root(obj, instance_matrix)
selected_tris: list[tuple[int, ...]] = []
selected_edges: list[tuple[int, ...]] = []
vert_indices_set: set[int] = set()
assert isinstance(obj.data, bpy.types.Mesh)
for polygon in obj.data.polygons[guid_start_index:guid_end_index]:
vert_indices_set.update(polygon.vertices)
vert_indices = list(vert_indices_set)
vert_map = {k: v for v, k in enumerate(vert_indices)}
selected_vertices = [tuple(obj.matrix_world @ obj.data.vertices[vi].co) for vi in vert_indices]
for polygon in obj.data.polygons[guid_start_index:guid_end_index]:
selected_tris.append(tuple(vert_map[v] for v in polygon.vertices))
selected_edges.extend(tuple([vert_map[vi] for vi in e] for e in polygon.edge_keys))
obj["selected_vertices"] = selected_vertices
obj["selected_edges"] = selected_edges
obj["selected_tris"] = selected_tris
break
guid_start_index = guid_end_index
guid = tool.Project.Link.get_guid_by_face_index(obj, face_index)
assert guid is not None
self.db = sqlite3.connect(obj["db"])
self.c = self.db.cursor()
self.c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1")
element = ElementRow(*self.c.fetchone())
attributes: dict[str, Any] = {}
for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]):
if element[i + 1] is not None:
attributes[attr] = element[i + 1]
self.c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],))
rows = [PropertyRow(*row) for row in self.c.fetchall()]
properties: defaultdict[str, dict[str, str]] = defaultdict(dict)
for row in rows:
properties[row.pset_name][row.name] = row.value
self.c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],))
relationships = [RelationshipRow(*row) for row in self.c.fetchall()]
relating_type_id = None
for relationship in relationships:
if relationship[1] == "IfcRelDefinesByType":
relating_type_id = relationship[2]
type_properties: defaultdict[str, dict[str, str]] = defaultdict(dict)
if relating_type_id is not None:
self.c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,))
rows = [PropertyRow(*row) for row in self.c.fetchall()]
for row in rows:
type_properties[row.pset_name][row.name] = row.value
LinksData.linked_data = {
"attributes": attributes,
"properties": [(k, properties[k]) for k in sorted(properties.keys())],
"type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())],
}
self.db.close()
for area in bpy.context.screen.areas:
if area.type == "PROPERTIES":
for region in area.regions:
if region.type == "WINDOW":
region.tag_redraw()
elif area.type == "VIEW_3D":
area.tag_redraw()
tool.Project.Link.select_linked_element(context, obj, guid)
self.report({"INFO"}, f"Loaded data for {guid}")
ProjectDecorator.install(bpy.context)
return {"FINISHED"}
def find_obj_root(self, obj: bpy.types.Object, matrix: Matrix) -> Union[bpy.types.Object, None]:
collections = set(obj.users_collection)
for o in bpy.data.objects:
if (
o.type != "EMPTY"
or o.instance_type != "COLLECTION"
or o.instance_collection not in collections
or not np.allclose(matrix, o.matrix_world, atol=1e-4)
):
continue
return o
def invoke(self, context, event):
self.mouse_x = event.mouse_region_x
self.mouse_y = event.mouse_region_y
@@ -2702,11 +2645,6 @@ class CreateClippingPlane(bpy.types.Operator):
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
from bpy_extras.view3d_utils import (
region_2d_to_origin_3d,
region_2d_to_vector_3d,
)
# Clean up deleted planes
props = tool.Project.get_project_props()
if len(props.clipping_planes) > 5:
@@ -490,6 +490,7 @@ class BIM_PT_links(Panel):
row.operator("bim.disable_editing_link", text="", icon="CANCEL")
else:
row.operator("bim.enable_editing_link", text="", icon="GREASEPENCIL")
row.operator("bim.select_linked_model_element", icon="VIEWZOOM", text="")
row.operator("bim.select_link_handle", text="", icon="OBJECT_DATA").link_index = index
row.operator("bim.unload_link", text="", icon="UNLINKED").link_index = index
row.operator("bim.reload_link", text="", icon="FILE_REFRESH").link_index = index
+170 -4
View File
@@ -24,7 +24,7 @@ import shutil
from collections import defaultdict
from math import radians
from pathlib import Path
from typing import TYPE_CHECKING, NamedTuple, Optional
from typing import TYPE_CHECKING, Any, NamedTuple, Optional
import bpy
import ifcopenshell
@@ -34,6 +34,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import numpy as np
from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES
from mathutils import Matrix
import bonsai.bim.schema
import bonsai.core.aggregate
@@ -46,7 +47,11 @@ import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
if TYPE_CHECKING:
from bonsai.bim.module.project.prop import BIMProjectProperties, MeasureToolSettings, Link
from bonsai.bim.module.project.prop import (
BIMProjectProperties,
Link,
MeasureToolSettings,
)
HiearchyDict = dict[ifcopenshell.entity_instance, "HiearchyDict"]
@@ -497,8 +502,8 @@ class Project(bonsai.core.tool.Project):
)
@classmethod
def get_clipping_planes_normals(cls):
normals = []
def get_clipping_planes_normals(cls) -> list[tuple[Vector, Vector]]:
normals: list[tuple[Vector, Vector]] = []
for clipping_plane in tool.Project.get_project_props().clipping_planes:
plane = clipping_plane.obj
if not plane or not plane.data:
@@ -507,6 +512,7 @@ class Project(bonsai.core.tool.Project):
if plane.mode == "EDIT":
continue # A profile decorator or something else is used here.
assert isinstance(plane.data, bpy.types.Mesh)
v1 = plane.matrix_world @ plane.data.vertices[0].co
v2 = plane.matrix_world @ plane.data.vertices[1].co
v3 = plane.matrix_world @ plane.data.vertices[2].co
@@ -589,3 +595,163 @@ class Project(bonsai.core.tool.Project):
ifc_file = tool.Ifc.get()
ifcopenshell.api.document.remove_information(ifc_file, information=doc)
class Link:
"""Tools for working with linked models."""
@classmethod
def is_linked_element(cls, obj: bpy.types.Object) -> bool:
return "guids" in obj
@classmethod
def get_obj_by_guid(cls, link: Link, guid: str) -> bpy.types.Object | None:
assert link.is_loaded
handle = tool.Project.get_link_empty_handle(link)
assert handle
col = handle.instance_collection
assert col
guid_obj = None
for obj in col.objects:
obj_guids: list[str] = obj["guids"]
if guid in obj_guids:
guid_obj = obj
break
return guid_obj
@classmethod
def get_guid_by_face_index(cls, obj: bpy.types.Object, face_index: int) -> str | None:
guids: list[str] = obj["guids"]
guid_ids: list[int] = obj["guid_ids"]
for guid, guid_end_index in zip(guids, guid_ids):
if face_index < guid_end_index:
return guid
@classmethod
def select_linked_element_geom(cls, obj: bpy.types.Object, guid: str) -> None:
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
obj_guids: list[str] = obj["guids"]
obj_guid_ids: list[int] = obj["guid_ids"]
index = obj_guids.index(guid)
guid_end_index = obj_guid_ids[index]
if index > 0:
guid_start_index = obj_guid_ids[index - 1]
else:
guid_start_index = 0
guid_polygons = mesh.polygons[guid_start_index:guid_end_index]
selected_tris: list[tuple[int, ...]] = []
selected_edges: list[tuple[int, ...]] = []
# Restart verts indices for our polygons.
guid_vertices_set: set[int] = set()
for polygon in guid_polygons:
guid_vertices_set.update(polygon.vertices)
vert_map = {k: v for v, k in enumerate(guid_vertices_set)}
selected_vertices = [obj.matrix_world @ mesh.vertices[vi].co for vi in vert_map]
for polygon in guid_polygons:
selected_tris.append(tuple(vert_map[vi] for vi in polygon.vertices))
selected_edges.extend(tuple([vert_map[vi] for vi in e]) for e in polygon.edge_keys)
obj["selected_vertices"] = selected_vertices
obj["selected_edges"] = selected_edges
obj["selected_tris"] = selected_tris
@classmethod
def select_linked_element(
cls,
context: bpy.types.Context,
obj: bpy.types.Object,
guid: str,
instance_matrix: Matrix | None = None,
) -> None:
import sqlite3
from ifcpatch.recipes.ExtractPropertiesToSQLite import (
ElementRow,
PropertyRow,
RelationshipRow,
)
from bonsai.bim.module.project.data import LinksData
from bonsai.bim.module.project.decorator import ProjectDecorator
# Not sure if there's a difference between `instance_matrix` coming from `ray_cast`
# and usual `matrix_world`, maybe we can just get it from object always.
if instance_matrix is None:
instance_matrix = obj.matrix_world
props = tool.Project.get_project_props()
props.queried_obj = obj
props.queried_obj_root = cls.find_obj_root(obj, instance_matrix)
cls.select_linked_element_geom(obj, guid)
db = sqlite3.connect(obj["db"])
c = db.cursor()
c.execute(f"SELECT * FROM elements WHERE global_id = '{guid}' LIMIT 1")
element = ElementRow(*c.fetchone())
attributes: dict[str, Any] = {}
for i, attr in enumerate(["GlobalId", "IFC Class", "Predefined Type", "Name", "Description"]):
if element[i + 1] is not None:
attributes[attr] = element[i + 1]
c.execute("SELECT * FROM properties WHERE element_id = ?", (element[0],))
rows = [PropertyRow(*row) for row in c.fetchall()]
properties: defaultdict[str, dict[str, str]] = defaultdict(dict)
for row in rows:
properties[row.pset_name][row.name] = row.value
c.execute("SELECT * FROM relationships WHERE from_id = ?", (element[0],))
relationships = [RelationshipRow(*row) for row in c.fetchall()]
relating_type_id = None
for relationship in relationships:
if relationship[1] == "IfcRelDefinesByType":
relating_type_id = relationship[2]
type_properties: defaultdict[str, dict[str, str]] = defaultdict(dict)
if relating_type_id is not None:
c.execute("SELECT * FROM properties WHERE element_id = ?", (relating_type_id,))
rows = [PropertyRow(*row) for row in c.fetchall()]
for row in rows:
type_properties[row.pset_name][row.name] = row.value
LinksData.linked_data = {
"attributes": attributes,
"properties": [(k, properties[k]) for k in sorted(properties.keys())],
"type_properties": [(k, type_properties[k]) for k in sorted(type_properties.keys())],
}
db.close()
assert context.screen
for area in context.screen.areas:
if area.type == "PROPERTIES":
for region in area.regions:
if region.type == "WINDOW":
region.tag_redraw()
elif area.type == "VIEW_3D":
area.tag_redraw()
ProjectDecorator.install(context)
@classmethod
def find_obj_root(cls, obj: bpy.types.Object, matrix: Matrix) -> bpy.types.Object | None:
collections = set(obj.users_collection)
for o in bpy.data.objects:
if (
o.type != "EMPTY"
or o.instance_type != "COLLECTION"
or o.instance_collection not in collections
or not np.allclose(matrix, o.matrix_world, atol=1e-4)
):
continue
return o