mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 02:02:22 +00:00
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:
@@ -66,6 +66,7 @@ classes = (
|
||||
operator.RewindLibrary,
|
||||
operator.SaveLibraryFile,
|
||||
operator.SelectLibraryFile,
|
||||
operator.SelectLinkedModelElement,
|
||||
operator.SelectLinkHandle,
|
||||
operator.ToggleFilterCategories,
|
||||
operator.ToggleLinkSelectability,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user