Linked IFC models - hotkey to hide selected geometry

Demo - https://files.catbox.moe/aok74w.mp4
This commit is contained in:
Andrej730
2026-03-05 16:59:15 +05:00
parent 9d0c172a53
commit 3a59425a64
8 changed files with 219 additions and 32 deletions
@@ -46,6 +46,7 @@ classes = (
operator.EnableEditingLink,
operator.ExportIFC,
operator.FlipClippingPlane,
operator.HideQueriedLinkedElement,
operator.IFCFileHandlerOperator,
operator.ImageScalingTool,
operator.LinkIfc,
@@ -97,26 +97,20 @@ class ProjectDecorator:
# general shader
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
selected_vertices: list[tuple[float, float, float]] = []
selected_edges: list[tuple[int, int]] = []
selected_tris: list[tuple[int, int, int]] = []
props = tool.Project.get_project_props()
try:
obj = props.queried_obj
selected_vertices = obj["selected_vertices"]
selected_edges = obj["selected_edges"]
selected_tris = obj["selected_tris"]
except:
obj = props.queried_obj
if obj is None:
return
geom = tool.Project.Link.get_selected_geometry(obj)
selected_vertices = geom.selected_vertices
root_obj = props.queried_obj_root
if root_obj and not (m := root_obj.matrix_world).is_identity:
selected_vertices = [m @ Vector(v) for v in selected_vertices]
if selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), selected_tris)
if geom.selected_edges:
self.draw_batch("LINES", selected_vertices, selected_elements_color, geom.selected_edges)
self.draw_batch("TRIS", selected_vertices, transparent_color(selected_elements_color), geom.selected_tris)
class ClippingPlaneDecorator:
@@ -53,7 +53,7 @@ from bpy_extras.view3d_utils import (
region_2d_to_origin_3d,
region_2d_to_vector_3d,
)
from mathutils import Matrix, Vector
from mathutils import Vector
import bonsai.bim.handler
import bonsai.bim.helper
@@ -2367,6 +2367,53 @@ class QueryLinkedElement(bpy.types.Operator):
return self.execute(context)
class HideQueriedLinkedElement(bpy.types.Operator):
bl_idname = "bim.hide_queried_linked_element"
bl_label = "Hide Queried Linked Element"
bl_description = (
"Hide geometry for currently queried linked element.\n\n"
"ALT+Click (or ALT+H in Explore Tool) to unhide all geometry for currently selected linked model.\n"
"(Not Yet Implemented) SHIFT+Click to hide everything but currently queried element."
)
bl_options = {"REGISTER", "UNDO"}
unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration]
if TYPE_CHECKING:
unhide_all: bool
def invoke(self, context, event):
self.unhide_all = event.alt
return self.execute(context)
def execute(self, context) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Project.get_project_props()
if self.unhide_all:
return self.run_unhide_all()
obj = props.queried_obj
if not obj:
self.report({"INFO"}, "No object is queried to hide.")
return {"FINISHED"}
guid = props.queried_guid
tool.Project.Link.hide_linked_element(obj, guid)
tool.Project.Link.deselect_queried_linked_element()
self.report({"INFO"}, "Queried object is now hidden.")
return {"FINISHED"}
def run_unhide_all(self) -> set["rna_enums.OperatorReturnItems"]:
props = tool.Project.get_project_props()
link = props.active_link
if not link:
self.report({"INFO"}, "No linked model is currently selected.")
return {"FINISHED"}
tool.Project.Link.unhide_all_elements(link)
self.report({"INFO"}, "All linked model geometry is unhidden.")
return {"FINISHED"}
class AppendInspectedLinkedElement(AppendLibraryElement):
bl_idname = "bim.append_inspected_linked_element"
bl_label = "Append Inspected Linked Element"
@@ -452,6 +452,7 @@ class BIMProjectProperties(PropertyGroup):
)
queried_obj: bpy.props.PointerProperty(type=bpy.types.Object)
queried_obj_root: bpy.props.PointerProperty(type=bpy.types.Object)
queried_guid: bpy.props.StringProperty()
clipping_planes: bpy.props.CollectionProperty(type=ObjProperty)
clipping_planes_active_index: bpy.props.IntProperty(min=0, default=0, max=5)
edited_objs: bpy.props.CollectionProperty(type=EditedObj)
@@ -550,6 +551,7 @@ class BIMProjectProperties(PropertyGroup):
should_save_metadata_for_this_file: bool
queried_obj: Union[bpy.types.Object, None]
queried_obj_root: Union[bpy.types.Object, None]
queried_guid: str
clipping_planes: bpy.types.bpy_prop_collection_idprop[ObjProperty]
clipping_planes_active_index: int
edited_objs: bpy.types.bpy_prop_collection_idprop[EditedObj]
@@ -40,9 +40,11 @@ class ExploreTool(bpy.types.WorkSpaceTool):
("bim.explore_hotkey", {"type": "C", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_C")]}),
("bim.explore_hotkey", {"type": "M", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_M")]}),
("bim.explore_hotkey", {"type": "S", "value": "PRESS", "shift": True}, {"properties": [("hotkey", "S_S")]}),
("bim.explore_hotkey", {"type": "H", "value": "PRESS"}, {"properties": [("hotkey", "H")]}),
("bim.explore_hotkey", {"type": "H", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_H")]}),
)
def draw_settings(context, layout, ws_tool):
def draw_settings(context: bpy.types.Context, layout: bpy.types.UILayout, ws_tool) -> None:
row = layout.row(align=True)
row.label(text="Query Object", icon="MOUSE_RMB")
row = layout.row(align=True)
@@ -61,6 +63,9 @@ class ExploreTool(bpy.types.WorkSpaceTool):
row.label(text="", icon="EVENT_ALT")
row.label(text="Disable Culling" if LinksData.enable_culling else "Enable Culling", icon="EVENT_C")
row = layout.row(align=True)
row.operator("bim.hide_queried_linked_element", text="Hide Queried Element", icon="EVENT_H")
prop = tool.Project.get_measure_tool_settings()
row = layout.row(align=True)
row.label(text="", icon="EVENT_SHIFT")
@@ -86,6 +91,7 @@ class ExploreHotkey(bpy.types.Operator):
bl_idname = "bim.explore_hotkey"
bl_label = ""
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
hotkey: bpy.props.StringProperty()
description: bpy.props.StringProperty()
@@ -137,3 +143,9 @@ class ExploreHotkey(bpy.types.Operator):
return
bpy.ops.bim.image_scaling_tool("INVOKE_DEFAULT")
def hotkey_H(self) -> None:
bpy.ops.bim.hide_queried_linked_element()
def hotkey_A_H(self) -> None:
bpy.ops.bim.hide_queried_linked_element(unhide_all=True)
+13 -1
View File
@@ -2192,12 +2192,24 @@ class Blender(bonsai.core.tool.Blender):
origin: Vector,
direction: Vector,
) -> tuple[bool, Vector, Vector, int, bpy.types.Object, Matrix]:
"""
The returned matrix is just ``obj.matrix_world``.
The returned object is not evaluated by the current depsgraph,
e.g. if object is modified by the depsgraph (e.g. by modifiers)
object has to be evaluated first (`obj.evaluated_get(depsgraph)`).
"""
depsgraph = context.evaluated_depsgraph_get()
assert context.scene
# `matrix` is just `obj.matrix_world`.
result = context.scene.ray_cast(
depsgraph,
origin,
direction,
)
return result
@classmethod
def depsgraph_evaluate(cls, obj: bpy.types.Object) -> bpy.types.Object:
depsgraph = bpy.context.evaluated_depsgraph_get()
evaluated_obj = obj.evaluated_get(depsgraph)
return evaluated_obj
+134 -15
View File
@@ -33,6 +33,7 @@ import ifcopenshell.util.element
import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder
import numpy as np
import numpy.typing as npt
from ifcopenshell.api.project.append_asset import APPENDABLE_ASSET_TYPES
from mathutils import Matrix
@@ -621,28 +622,103 @@ class Project(bonsai.core.tool.Project):
return guid_obj
@classmethod
def get_linked_element_guid_ids(cls, obj: bpy.types.Object, *, skip_hidden: bool) -> npt.NDArray[np.int64]:
obj_guid_ids: npt.NDArray[np.int64] = np.array(obj["guid_ids"])
if not skip_hidden:
return obj_guid_ids
# 'hidden_indices' is needed, because otherwise we can't make sense of 'guid_ids',
# since part of the geometry is hidden.
obj_hidden_indices: list[int] = list(obj.get("hidden_indices") or [])
if not obj_hidden_indices:
return obj_guid_ids
# Skip hidden geometry indices.
hidden_indices_mask = np.zeros(len(obj_guid_ids), dtype=bool)
hidden_indices_mask[obj_hidden_indices] = True
deltas = np.diff(obj_guid_ids, prepend=0)
deltas[~hidden_indices_mask] = 0
obj_guid_ids -= np.cumsum(deltas)
return obj_guid_ids
@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"]
guid_ids = cls.get_linked_element_guid_ids(obj, skip_hidden=True)
for guid, guid_end_index in zip(guids, guid_ids):
if face_index < guid_end_index:
return guid
@classmethod
def get_linked_element_geom_slice(cls, obj: bpy.types.Object, guid: str) -> slice[int, int]:
"""
Get slice for ``obj.data.polygons`` for the provided ``guid``.
"""
obj_guids: list[str] = obj["guids"]
# Just to be safe.
obj_hidden_indices: list[int] = obj.get("hidden_indices") or []
index = obj_guids.index(guid)
if index in obj_hidden_indices:
assert False, "Unexpected. Why would you need the geometry for the hidden element?"
obj_guid_ids = cls.get_linked_element_guid_ids(obj, skip_hidden=False)
guid_end_index = obj_guid_ids[index]
guid_start_index = index and obj_guid_ids[index - 1]
return slice(guid_start_index, guid_end_index)
@classmethod
def hide_linked_element(cls, obj: bpy.types.Object, guid: str) -> None:
verts = tool.Project.Link.get_linked_element_verts(obj, guid)
# `MeshPolygon.hide` works only in EDIT mode,
# so we use vertex groups + Mask modifier.
MODIFIER_VG_NAME = "BBIM_HIDE_LINKED_GEOMETRY"
vertex_groups = obj.vertex_groups
vertex_group = vertex_groups.get(MODIFIER_VG_NAME)
if vertex_group is None:
vertex_group = vertex_groups.new(name=MODIFIER_VG_NAME)
modifiers = obj.modifiers
modifier = modifiers.get(MODIFIER_VG_NAME)
if modifier is None:
modifier = modifiers.new(MODIFIER_VG_NAME, "MASK")
assert isinstance(modifier, bpy.types.MaskModifier)
modifier.vertex_group = MODIFIER_VG_NAME
modifier.invert_vertex_group = True
vertex_group.add(verts, 1.0, "REPLACE")
hidden_indices: list[int] = list(obj.get("hidden_indices") or [])
guid_ids: list[str] = obj["guids"]
index = guid_ids.index(guid)
hidden_indices.append(index)
obj["hidden_indices"] = hidden_indices
@classmethod
def unhide_all_elements(cls, link: Link) -> None:
obj = tool.Project.get_link_empty_handle(link)
assert obj
col = obj.instance_collection
assert col
for obj_ in col.objects:
if "hidden_indices" not in obj_:
continue
obj_.vertex_groups.clear()
obj_.modifiers.clear()
del obj_["hidden_indices"]
@classmethod
def select_linked_element_geom(cls, obj: bpy.types.Object, guid: str) -> None:
slice_ = cls.get_linked_element_geom_slice(obj, guid)
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]
guid_polygons = mesh.polygons[slice_]
selected_tris: list[tuple[int, ...]] = []
selected_edges: list[tuple[int, ...]] = []
@@ -662,6 +738,19 @@ class Project(bonsai.core.tool.Project):
obj["selected_edges"] = selected_edges
obj["selected_tris"] = selected_tris
@classmethod
def get_linked_element_verts(cls, obj: bpy.types.Object, guid: str) -> set[int]:
slice_ = cls.get_linked_element_geom_slice(obj, guid)
mesh = obj.data
assert isinstance(mesh, bpy.types.Mesh)
guid_polygons = mesh.polygons[slice_]
guid_vertices_set: set[int] = set()
for polygon in guid_polygons:
guid_vertices_set.update(polygon.vertices)
return guid_vertices_set
@classmethod
def select_linked_element(
cls,
@@ -686,10 +775,8 @@ class Project(bonsai.core.tool.Project):
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.deselect_queried_linked_element()
cls.set_queried_linked_element(obj, guid, instance_matrix)
cls.select_linked_element_geom(obj, guid)
db = sqlite3.connect(obj["db"])
c = db.cursor()
@@ -743,6 +830,25 @@ class Project(bonsai.core.tool.Project):
ProjectDecorator.install(context)
@classmethod
def set_queried_linked_element(cls, obj: bpy.types.Object, guid: str, instance_matrix: Matrix) -> None:
props = tool.Project.get_project_props()
props.queried_obj = obj
props.queried_obj_root = cls.find_obj_root(obj, instance_matrix)
props.queried_guid = guid
@classmethod
def deselect_queried_linked_element(cls) -> None:
props = tool.Project.get_project_props()
obj = props.queried_obj
props.property_unset("queried_obj")
props.property_unset("queried_obj_root")
props.property_unset("queried_guid")
if obj is not None:
for field in cls.SelectedGeometry._fields:
del obj[field]
@classmethod
def find_obj_root(cls, obj: bpy.types.Object, matrix: Matrix) -> bpy.types.Object | None:
collections = set(obj.users_collection)
@@ -755,3 +861,16 @@ class Project(bonsai.core.tool.Project):
):
continue
return o
class SelectedGeometry(NamedTuple):
selected_vertices: list[tuple[float, float, float]]
selected_edges: list[tuple[int, int]]
selected_tris: list[tuple[int, int, int]]
@classmethod
def get_selected_geometry(cls, obj: bpy.types.Object) -> SelectedGeometry:
return cls.SelectedGeometry(
obj["selected_vertices"],
obj["selected_edges"],
obj["selected_tris"],
)
@@ -40,7 +40,7 @@ APPENDABLE_ASSET = Literal[
"IfcProfileDef",
"IfcPresentationStyle",
]
APPENDABLE_ASSET_TYPES = get_args(APPENDABLE_ASSET)
APPENDABLE_ASSET_TYPES: tuple[APPENDABLE_ASSET, ...] = get_args(APPENDABLE_ASSET)
MATERIAL_SETS = ("IfcMaterialLayerSet", "IfcMaterialConstituentSet", "IfcMaterialProfileSet")