diff --git a/src/bonsai/bonsai/bim/module/project/operator.py b/src/bonsai/bonsai/bim/module/project/operator.py index bf2fc16cfd..1cc5518eb4 100644 --- a/src/bonsai/bonsai/bim/module/project/operator.py +++ b/src/bonsai/bonsai/bim/module/project/operator.py @@ -2373,19 +2373,23 @@ class HideQueriedLinkedElement(bpy.types.Operator): 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.\n\n" - "Know limitation: doesn't work with UNDO." + "SHIFT+Click (or SHIFT+H in Explore Tool) to hide everything " + "in the currently selected model, but the queried element.\n" + "ALT+Click (or ALT+H in Explore Tool) to unhide all geometry for currently selected linked model.\n\n" + "Known limitation: doesn't work with UNDO." ) bl_options = {"REGISTER", "UNDO"} unhide_all: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] + hide_all_except: bpy.props.BoolProperty(options={"SKIP_SAVE"}) # pyright: ignore[reportRedeclaration] if TYPE_CHECKING: unhide_all: bool + hide_all_except: bool def invoke(self, context, event): self.unhide_all = event.alt + self.hide_all_except = event.shift return self.execute(context) def execute(self, context) -> set["rna_enums.OperatorReturnItems"]: @@ -2394,6 +2398,9 @@ class HideQueriedLinkedElement(bpy.types.Operator): if self.unhide_all: return self.run_unhide_all() + if self.hide_all_except: + return self.run_hide_all_except() + obj = props.queried_obj if not obj: self.report({"INFO"}, "No object is queried to hide.") @@ -2415,6 +2422,21 @@ class HideQueriedLinkedElement(bpy.types.Operator): self.report({"INFO"}, "All linked model geometry is unhidden.") return {"FINISHED"} + def run_hide_all_except(self) -> set["rna_enums.OperatorReturnItems"]: + props = tool.Project.get_project_props() + obj = props.queried_obj + if not obj: + self.report({"INFO"}, "No object is queried.") + return {"FINISHED"} + link = props.active_link + if not link: + self.report({"INFO"}, "No linked model is currently selected.") + return {"FINISHED"} + guid = props.queried_guid + tool.Project.Link.hide_all_elements_except(link, obj, guid) + self.report({"INFO"}, "All other linked model geometry is now hidden.") + return {"FINISHED"} + class AppendInspectedLinkedElement(AppendLibraryElement): bl_idname = "bim.append_inspected_linked_element" diff --git a/src/bonsai/bonsai/bim/module/project/workspace.py b/src/bonsai/bonsai/bim/module/project/workspace.py index 5437d1270b..a01a144b31 100644 --- a/src/bonsai/bonsai/bim/module/project/workspace.py +++ b/src/bonsai/bonsai/bim/module/project/workspace.py @@ -41,6 +41,7 @@ class ExploreTool(bpy.types.WorkSpaceTool): ("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", "shift": True}, {"properties": [("hotkey", "S_H")]}), ("bim.explore_hotkey", {"type": "H", "value": "PRESS", "alt": True}, {"properties": [("hotkey", "A_H")]}), ) @@ -147,5 +148,8 @@ class ExploreHotkey(bpy.types.Operator): def hotkey_H(self) -> None: bpy.ops.bim.hide_queried_linked_element() + def hotkey_S_H(self) -> None: + bpy.ops.bim.hide_queried_linked_element(hide_all_except=True) + def hotkey_A_H(self) -> None: bpy.ops.bim.hide_queried_linked_element(unhide_all=True) diff --git a/src/bonsai/bonsai/tool/project.py b/src/bonsai/bonsai/tool/project.py index fe2d6873cb..259bff86d0 100644 --- a/src/bonsai/bonsai/tool/project.py +++ b/src/bonsai/bonsai/tool/project.py @@ -24,7 +24,15 @@ import shutil from collections import defaultdict from math import radians from pathlib import Path -from typing import TYPE_CHECKING, Any, NamedTuple, Optional +from typing import ( + TYPE_CHECKING, + Any, + Literal, + NamedTuple, + NotRequired, + Optional, + TypedDict, +) import bpy import ifcopenshell @@ -600,6 +608,49 @@ class Project(bonsai.core.tool.Project): class Link: """Tools for working with linked models.""" + class LinkedObjectChunk(TypedDict): + """There's actually no dictionary with those keys, + just using this class to document what keys we do assign to the objects + that represent chunks of the linked models. + """ + + guids: list[str] + """List of guids present in the object.""" + + guid_ids: list[int] + """Number of faces that belong to each guid. + + E.g. if chunk consists of two 12 tris cubes: + ``` + guids = ["aaa", "bbb"] + # Meaning object has 24 polygons + # [0;11] is part of "aaa", [12:23] is part of "bbb". + guid_ids = [12, 24] + ``` + """ + + db: str + """Absolute filepath to .ifc.cache.sqlite.""" + + ifc_filepath: str + """Absolute filepath to .ifc.""" + + # Only added when object is queried. + selected_vertices: NotRequired[list[tuple[int, int, int]]] + selected_edges: NotRequired[list[tuple[int, int]]] + selected_tris: NotRequired[list[tuple[int, int, int]]] + + hidden_indices: NotRequired[list[int]] + """List of hidden indices in "guids". + Note that entire object also can be hidden by ``hide_viewport`` and then "hidden_indices" won't be set. + + ``` + guids = ["aaa", "bbb", "ccc"] + # guid "bbb" is hidden. + hidden_indices = [1] + ``` + """ + @classmethod def is_linked_element(cls, obj: bpy.types.Object) -> bool: return "guids" in obj @@ -670,9 +721,11 @@ class Project(bonsai.core.tool.Project): 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) - + def setup_hide_modifier( + cls, + obj: bpy.types.Object, + hide_type: Literal["hide_selected", "hide_unselected"], + ) -> bpy.types.VertexGroup: # `MeshPolygon.hide` works only in EDIT mode, # so we use vertex groups + Mask modifier. # But since for hiding we're modifying object in a linked model, @@ -692,8 +745,15 @@ class Project(bonsai.core.tool.Project): modifier = modifiers.new(MODIFIER_VG_NAME, "MASK") assert isinstance(modifier, bpy.types.MaskModifier) modifier.vertex_group = MODIFIER_VG_NAME - modifier.invert_vertex_group = True + # Mask modifier by default shows only geometry from the provided vertex group. + modifier.invert_vertex_group = hide_type == "hide_selected" + return vertex_group + @classmethod + def hide_linked_element(cls, obj: bpy.types.Object, guid: str) -> None: + verts = tool.Project.Link.get_linked_element_verts(obj, guid) + + vertex_group = cls.setup_hide_modifier(obj, "hide_selected") vertex_group.add(verts, 1.0, "REPLACE") hidden_indices: list[int] = list(obj.get("hidden_indices") or []) @@ -710,12 +770,45 @@ class Project(bonsai.core.tool.Project): assert col for obj_ in col.objects: + obj_.hide_viewport = False + if "hidden_indices" not in obj_: continue obj_.vertex_groups.clear() obj_.modifiers.clear() del obj_["hidden_indices"] + @classmethod + def hide_all_elements_except(cls, link: Link, queried_obj: bpy.types.Object, queried_guid: str) -> None: + handle = tool.Project.get_link_empty_handle(link) + assert handle + col = handle.instance_collection + assert col + + for obj_ in col.objects: + if "guids" not in obj_: + continue + if obj_ != queried_obj: + # Just hide the entire chunk, if queried guid is not part of it. + obj_.hide_viewport = True + continue + + guids: list[str] = obj_["guids"] + queried_guid_index = guids.index(queried_guid) + + # Get vertices for the queried element before modifying hidden_indices. + queried_verts = cls.get_linked_element_verts(obj_, queried_guid) + vertex_group = cls.setup_hide_modifier(obj_, "hide_unselected") + + assert isinstance(mesh := obj_.data, bpy.types.Mesh) + # Clean up possible previously hidden elements. + vertex_group.remove(range(len(mesh.vertices))) + + vertex_group.add(queried_verts, 1.0, "REPLACE") + + # Mark all guids as hidden except the queried one. + obj_["hidden_indices"] = [i for i in range(len(guids)) if i != queried_guid_index] + @classmethod def select_linked_element_geom(cls, obj: bpy.types.Object, guid: str) -> None: slice_ = cls.get_linked_element_geom_slice(obj, guid)