mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
Linked IFC models - hotkey to hide selected geometry
Demo - https://files.catbox.moe/aok74w.mp4
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user