mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-09 13:52:23 +00:00
Extend tool.Blender for parametric framework + decorators
Adds:
* ViewportDecorator base class — install/uninstall/draw lifecycle for
3D viewport gpu overlays, with handler-rollback-on-failure so a
partial install can't leave dangling draw handlers.
* sync_all classmethod — drive each listed ViewportDecorator subclass
to its desired install state in one call.
* is_view_top_down + top_down_factor — viewport-camera orientation
predicates used by gizmo billboarding and decorator layout.
* get_screen_up_world — screen-up vector in world space for gizmo
text orientation.
* are_viewport_gizmos_enabled — central gate for the global
draw_gizmos_in_3d_viewport pref, replacing duplicated prefs reads.
* DecoratorColors NamedTuple + get_decorator_colors — single source
for the colour palette every viewport decorator binds.
Preserves Ryan Schultz's add_layout_hotkey_operator polish (719309571,
2026-05-25): the row-position move + separator(factor=1) between the
modifier and key icons stay intact in this extraction.
Generated with the assistance of an AI coding tool.
This commit is contained in:
committed by
Thomas Krijnen
parent
4a70250c68
commit
6c4414aa4e
+245
-116
@@ -22,7 +22,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import importlib
|
import importlib
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -30,7 +29,7 @@ import sys
|
|||||||
import tempfile
|
import tempfile
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
from collections.abc import Callable, Generator, Iterable, Sequence, Sized
|
from collections.abc import Callable, Generator, Iterable, Mapping, Sequence, Sized
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import cache, lru_cache
|
from functools import cache, lru_cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -47,7 +46,6 @@ from typing import (
|
|||||||
|
|
||||||
import bmesh
|
import bmesh
|
||||||
import bpy
|
import bpy
|
||||||
import ifcopenshell.api
|
|
||||||
import ifcopenshell.util.element
|
import ifcopenshell.util.element
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import numpy.typing as npt
|
import numpy.typing as npt
|
||||||
@@ -99,6 +97,19 @@ VIEWPORT_ATTRIBUTES = [
|
|||||||
|
|
||||||
OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera]
|
OBJECT_DATA_TYPE = Union[bpy.types.Mesh, bpy.types.Curve, bpy.types.Camera]
|
||||||
|
|
||||||
|
_RAILING_MODIFIER_IFC_CLASSES = ("IfcRailing", "IfcRailingType")
|
||||||
|
_STAIR_MODIFIER_IFC_CLASSES = (
|
||||||
|
"IfcStairFlight",
|
||||||
|
"IfcStairFlightType",
|
||||||
|
"IfcMember",
|
||||||
|
"IfcMemberType",
|
||||||
|
"IfcStair",
|
||||||
|
"IfcStairType",
|
||||||
|
)
|
||||||
|
_WINDOW_MODIFIER_IFC_CLASSES = ("IfcWindow", "IfcWindowType", "IfcWindowStyle")
|
||||||
|
_DOOR_MODIFIER_IFC_CLASSES = ("IfcDoor", "IfcDoorType", "IfcDoorStyle")
|
||||||
|
_ROOF_MODIFIER_IFC_CLASSES = ("IfcRoof", "IfcRoofType")
|
||||||
|
|
||||||
|
|
||||||
class Blender(bonsai.core.tool.Blender):
|
class Blender(bonsai.core.tool.Blender):
|
||||||
OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE = ("MESH", "CURVE", "SURFACE", "META", "FONT", "LATTICE", "ARMATURE")
|
OBJECT_TYPES_THAT_SUPPORT_EDIT_MODE = ("MESH", "CURVE", "SURFACE", "META", "FONT", "LATTICE", "ARMATURE")
|
||||||
@@ -417,6 +428,189 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
with bpy.context.temp_override(**cls.get_viewport_context()):
|
with bpy.context.temp_override(**cls.get_viewport_context()):
|
||||||
bpy.ops.wm.tool_set_by_id(name=tool_name)
|
bpy.ops.wm.tool_set_by_id(name=tool_name)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def are_viewport_gizmos_enabled(cls) -> bool:
|
||||||
|
"""Central gate every Bonsai gizmo poll / decorator draw checks before
|
||||||
|
rendering. Centralises the read of
|
||||||
|
``gizmos.draw_gizmos_in_3d_viewport`` from addon preferences."""
|
||||||
|
return cls.get_addon_preferences().gizmos.draw_gizmos_in_3d_viewport
|
||||||
|
|
||||||
|
class DecoratorColors(NamedTuple):
|
||||||
|
selected: tuple
|
||||||
|
unselected: tuple
|
||||||
|
special: tuple
|
||||||
|
error: tuple
|
||||||
|
background: tuple
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_decorator_colors(cls) -> Blender.DecoratorColors:
|
||||||
|
"""The five ``decorator_color_*`` fields read together so each viewport
|
||||||
|
decorator's draw callback resolves them in one call instead of five."""
|
||||||
|
prefs = cls.get_addon_preferences()
|
||||||
|
return cls.DecoratorColors(
|
||||||
|
selected=prefs.decorator_color_selected,
|
||||||
|
unselected=prefs.decorator_color_unselected,
|
||||||
|
special=prefs.decorator_color_special,
|
||||||
|
error=prefs.decorator_color_error,
|
||||||
|
background=prefs.decorator_color_background,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ViewportDecorator:
|
||||||
|
"""Shared ``SpaceView3D.draw_handler_add`` lifecycle for feature decorators.
|
||||||
|
|
||||||
|
Single-handler subclasses set ``draw_method`` (default ``"draw"``); the
|
||||||
|
handler binds at ``POST_VIEW``. Multi-handler subclasses set
|
||||||
|
``draw_methods`` to a tuple of ``(method_name, phase)`` pairs; when it
|
||||||
|
is non-``None`` it supersedes ``draw_method``.
|
||||||
|
|
||||||
|
Decorators whose ``install`` must accept extra arguments (e.g. a callback
|
||||||
|
or a precomputed bmesh) override ``install`` themselves."""
|
||||||
|
|
||||||
|
draw_method: str = "draw"
|
||||||
|
draw_methods: tuple[tuple[str, str], ...] | None = None
|
||||||
|
|
||||||
|
def __init_subclass__(cls, **kwargs):
|
||||||
|
super().__init_subclass__(**kwargs)
|
||||||
|
cls.handlers = []
|
||||||
|
cls.is_installed = False
|
||||||
|
# Fail loudly at class-definition time if draw_method / draw_methods
|
||||||
|
# names an attribute the class doesn't expose. Without this, a typo
|
||||||
|
# only surfaces on the first redraw — as a silent missing-attribute
|
||||||
|
# handler — which may be far from the offending declaration.
|
||||||
|
method_names = (
|
||||||
|
tuple(name for name, _phase in cls.draw_methods) if cls.draw_methods is not None else (cls.draw_method,)
|
||||||
|
)
|
||||||
|
for name in method_names:
|
||||||
|
if getattr(cls, name, None) is None:
|
||||||
|
raise TypeError(f"{cls.__name__}: draw method {name!r} is declared but not defined on the class")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def install(cls, context: bpy.types.Context) -> None:
|
||||||
|
if cls.is_installed:
|
||||||
|
cls.uninstall()
|
||||||
|
handler = cls()
|
||||||
|
bindings = cls.draw_methods if cls.draw_methods is not None else ((cls.draw_method, "POST_VIEW"),)
|
||||||
|
# Rollback partial registrations on any draw_handler_add failure, so
|
||||||
|
# cls.handlers never ends up holding a half-installed set.
|
||||||
|
added: list = []
|
||||||
|
try:
|
||||||
|
for method_name, phase in bindings:
|
||||||
|
added.append(
|
||||||
|
bpy.types.SpaceView3D.draw_handler_add(
|
||||||
|
getattr(handler, method_name), (context,), "WINDOW", phase
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
for h in added:
|
||||||
|
try:
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(h, "WINDOW")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
cls.handlers = added
|
||||||
|
cls.is_installed = True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def uninstall(cls) -> None:
|
||||||
|
for h in cls.handlers:
|
||||||
|
try:
|
||||||
|
bpy.types.SpaceView3D.draw_handler_remove(h, "WINDOW")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
cls.handlers.clear()
|
||||||
|
cls.is_installed = False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _lookup_active_instance(gizmo_cls: type, context: bpy.types.Context) -> Optional[Any]:
|
||||||
|
"""Return the live ``GizmoGroup`` instance registered under
|
||||||
|
``context.region``, or ``None`` if there isn't one. The per-region
|
||||||
|
weakref dict on the gizmo class is populated by ``setup()``; multi-
|
||||||
|
viewport setups put one entry per region in it so each region's
|
||||||
|
decorator sees only its own region's hover state."""
|
||||||
|
instances = getattr(gizmo_cls, "_active_instances", None)
|
||||||
|
if not instances:
|
||||||
|
return None
|
||||||
|
region = getattr(context, "region", None)
|
||||||
|
if region is None:
|
||||||
|
return None
|
||||||
|
ref = instances.get(region.as_pointer())
|
||||||
|
if ref is None:
|
||||||
|
return None
|
||||||
|
return ref()
|
||||||
|
|
||||||
|
def _cursor_icon_hovered(self, gizmo_cls: type, attr_name: str, context: bpy.types.Context) -> bool:
|
||||||
|
"""True iff the gizmo group instance in the current region exposes a gizmo
|
||||||
|
under ``attr_name`` that reports as highlighted. Any access exception is
|
||||||
|
swallowed so a transient bpy-state hiccup never breaks the draw loop."""
|
||||||
|
inst = self._lookup_active_instance(gizmo_cls, context)
|
||||||
|
if inst is None:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
return bool(getattr(inst, attr_name).is_highlight)
|
||||||
|
except (AttributeError, ReferenceError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def sync_all(
|
||||||
|
cls,
|
||||||
|
context: bpy.types.Context,
|
||||||
|
enabled: Mapping[type[ViewportDecorator], bool],
|
||||||
|
) -> None:
|
||||||
|
"""Drive each listed decorator to its desired install state in one call.
|
||||||
|
|
||||||
|
Each entry whose value is ``True`` ends up installed; each entry whose
|
||||||
|
value is ``False`` ends up uninstalled. Pass ``True`` for always-on
|
||||||
|
overlays so they survive subsequent file loads."""
|
||||||
|
for decorator_cls, should_install in enabled.items():
|
||||||
|
if should_install:
|
||||||
|
decorator_cls.install(context)
|
||||||
|
else:
|
||||||
|
decorator_cls.uninstall()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_view_top_down(cls, context: bpy.types.Context, threshold: float = 0.9659) -> bool:
|
||||||
|
"""True when the viewport camera is looking ~straight down (or up) the world Z axis.
|
||||||
|
|
||||||
|
Default threshold of 0.9659 = cos(15°) — a 15° tilt cone around ±world Z.
|
||||||
|
Above the threshold the world-Z axis projects to a small fraction of its
|
||||||
|
true length on screen, so callers that lay icons or markers out along
|
||||||
|
world Z should switch to a screen-space offset and any gizmo whose intent
|
||||||
|
is specifically "vertical" loses its visual cue. The cone is kept narrow
|
||||||
|
so vertical-intent gizmos stay visible across the typical orbit range of
|
||||||
|
3D viewport work and drop out only near genuine plan view."""
|
||||||
|
rv3d = context.region_data
|
||||||
|
if rv3d is None:
|
||||||
|
return False
|
||||||
|
view_forward = Vector(rv3d.view_matrix.inverted().col[2][:3]).normalized()
|
||||||
|
return abs(view_forward.z) > threshold
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def top_down_factor(cls, context: bpy.types.Context, threshold: float = 0.9659) -> float:
|
||||||
|
"""Continuous 0–1 ramp matching ``is_view_top_down``'s cone: 0 outside the
|
||||||
|
cone, ramping linearly to 1 at strict alignment with world Z. Callers that
|
||||||
|
want a proportional effect (an icon-stack lift growing as the view
|
||||||
|
approaches plan) use this in place of the boolean to avoid a one-frame
|
||||||
|
visual jump as the camera crosses the threshold."""
|
||||||
|
rv3d = context.region_data
|
||||||
|
if rv3d is None:
|
||||||
|
return 0.0
|
||||||
|
view_forward = Vector(rv3d.view_matrix.inverted().col[2][:3]).normalized()
|
||||||
|
alignment = abs(view_forward.z)
|
||||||
|
if alignment <= threshold:
|
||||||
|
return 0.0
|
||||||
|
return (alignment - threshold) / (1.0 - threshold)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_screen_up_world(cls, context: bpy.types.Context) -> Vector:
|
||||||
|
"""World-space direction corresponding to the camera's up axis (screen-vertical).
|
||||||
|
|
||||||
|
Returns ``+Y`` when region data is unavailable so callers can compute an
|
||||||
|
offset without a guard branch."""
|
||||||
|
rv3d = context.region_data
|
||||||
|
if rv3d is None:
|
||||||
|
return Vector((0.0, 1.0, 0.0))
|
||||||
|
return Vector(rv3d.view_matrix.inverted().col[1][:3]).normalized()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_shader_editor_context(cls) -> Union[dict[str, Any], None]:
|
def get_shader_editor_context(cls) -> Union[dict[str, Any], None]:
|
||||||
for screen in bpy.data.screens:
|
for screen in bpy.data.screens:
|
||||||
@@ -1143,13 +1337,13 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
"""
|
"""
|
||||||
# roof and railing both finalize then drop into path-edit mode — handle
|
# roof and railing both finalize then drop into path-edit mode — handle
|
||||||
# them before the generic finish dispatch so the path transition runs.
|
# them before the generic finish dispatch so the path transition runs.
|
||||||
if cls.is_roof(element):
|
if tool.Parametric.is_roof(element):
|
||||||
if (feature := tool.Parametric.find_by_name("roof")) and feature.is_editing(obj):
|
if tool.Parametric.ROOF.is_editing(obj):
|
||||||
tool.Parametric.run_bim_op(feature.finish_op)
|
tool.Parametric.run_bim_op(tool.Parametric.ROOF.finish_op)
|
||||||
bpy.ops.bim.enable_editing_roof_path()
|
bpy.ops.bim.enable_editing_roof_path()
|
||||||
elif cls.is_railing(element):
|
elif tool.Parametric.is_railing(element):
|
||||||
if (feature := tool.Parametric.find_by_name("railing")) and feature.is_editing(obj):
|
if tool.Parametric.RAILING.is_editing(obj):
|
||||||
tool.Parametric.run_bim_op(feature.finish_op)
|
tool.Parametric.run_bim_op(tool.Parametric.RAILING.finish_op)
|
||||||
bpy.ops.bim.enable_editing_railing_path()
|
bpy.ops.bim.enable_editing_railing_path()
|
||||||
elif feature := tool.Parametric.is_object_editing(obj):
|
elif feature := tool.Parametric.is_object_editing(obj):
|
||||||
tool.Parametric.run_bim_op(feature.finish_op)
|
tool.Parametric.run_bim_op(feature.finish_op)
|
||||||
@@ -1176,59 +1370,67 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_eligible_for_railing_modifier(cls, obj: bpy.types.Object) -> bool:
|
def is_eligible_for_railing_modifier(cls, obj: bpy.types.Object) -> bool:
|
||||||
return tool.Blender.is_object_an_ifc_class(obj, ("IfcRailing", "IfcRailingType"))
|
return tool.Blender.is_object_an_ifc_class(obj, _RAILING_MODIFIER_IFC_CLASSES)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_eligible_for_stair_modifier(cls, obj: bpy.types.Object) -> bool:
|
def is_eligible_for_stair_modifier(cls, obj: bpy.types.Object) -> bool:
|
||||||
return tool.Blender.is_object_an_ifc_class(
|
return tool.Blender.is_object_an_ifc_class(obj, _STAIR_MODIFIER_IFC_CLASSES)
|
||||||
obj, ("IfcStairFlight", "IfcStairFlightType", "IfcMember", "IfcMemberType", "IfcStair", "IfcStairType")
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_eligible_for_window_modifier(cls, obj: bpy.types.Object) -> bool:
|
def is_eligible_for_window_modifier(cls, obj: bpy.types.Object) -> bool:
|
||||||
return tool.Blender.is_object_an_ifc_class(obj, ("IfcWindow", "IfcWindowType", "IfcWindowStyle"))
|
return tool.Blender.is_object_an_ifc_class(obj, _WINDOW_MODIFIER_IFC_CLASSES)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_eligible_for_door_modifier(cls, obj: bpy.types.Object) -> bool:
|
def is_eligible_for_door_modifier(cls, obj: bpy.types.Object) -> bool:
|
||||||
return tool.Blender.is_object_an_ifc_class(obj, ("IfcDoor", "IfcDoorType", "IfcDoorStyle"))
|
return tool.Blender.is_object_an_ifc_class(obj, _DOOR_MODIFIER_IFC_CLASSES)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_eligible_for_roof_modifier(cls, obj: bpy.types.Object) -> bool:
|
def is_eligible_for_roof_modifier(cls, obj: bpy.types.Object) -> bool:
|
||||||
return tool.Blender.is_object_an_ifc_class(obj, ("IfcRoof", "IfcRoofType"))
|
return tool.Blender.is_object_an_ifc_class(obj, _ROOF_MODIFIER_IFC_CLASSES)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_railing(cls, element: entity_instance) -> bool:
|
def is_array_child(cls, element: entity_instance) -> bool:
|
||||||
return tool.Pset.get_element_pset(element, "BBIM_Railing")
|
"""True if element is a CHILD of a Bonsai parametric array.
|
||||||
|
|
||||||
@classmethod
|
Children are managed replicas regenerated from the parent's pset —
|
||||||
def is_roof(cls, element: entity_instance) -> bool:
|
their parametric attributes (door dimensions, wall lengths, …) are
|
||||||
return tool.Pset.get_element_pset(element, "BBIM_Roof")
|
overwritten on the next ``regenerate_array``. Parametric gizmo
|
||||||
|
groups skip children via this predicate in ``poll``.
|
||||||
|
|
||||||
@classmethod
|
This sits on a different axis from ``tool.Parametric.is_array``:
|
||||||
def is_window(cls, element: entity_instance) -> bool:
|
cardinality (parent vs child) is orthogonal to feature kind, and
|
||||||
return tool.Pset.get_element_pset(element, "BBIM_Window")
|
an arrayed wall fires both ``is_wall`` and ``is_array`` on the
|
||||||
|
same element."""
|
||||||
@classmethod
|
if element is None:
|
||||||
def is_door(cls, element: entity_instance) -> bool:
|
|
||||||
return tool.Pset.get_element_pset(element, "BBIM_Door")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_stair(cls, element: entity_instance) -> bool:
|
|
||||||
return tool.Pset.get_element_pset(element, "BBIM_Stair")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def is_wall(cls, element: entity_instance) -> bool:
|
|
||||||
"""A wall is editable by the parametric gizmo if it is an IfcWall with LAYER2 usage.
|
|
||||||
|
|
||||||
Unlike doors/windows/stairs, walls do not carry a proprietary BBIM_Wall pset —
|
|
||||||
their parametric state lives in standard IFC (axis polyline, IfcMaterialLayerSetUsage,
|
|
||||||
IfcExtrudedAreaSolid). Any LAYER2 wall qualifies."""
|
|
||||||
if not element.is_a("IfcWall"):
|
|
||||||
return False
|
return False
|
||||||
return tool.Model.get_usage_type(element) == "LAYER2"
|
pset = ifcopenshell.util.element.get_pset(element, "BBIM_Array")
|
||||||
|
if not pset:
|
||||||
|
return False
|
||||||
|
parent_guid = pset.get("Parent")
|
||||||
|
return parent_guid is not None and parent_guid != element.GlobalId
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_editing_railing_path(cls, obj: bpy.types.Object):
|
def is_slab(cls, element: entity_instance) -> bool:
|
||||||
|
"""A slab is host-eligible for the parametric add-opening gizmo if
|
||||||
|
it is an IfcSlab with LAYER3 usage.
|
||||||
|
|
||||||
|
Slabs carry no proprietary BBIM_Slab pset — their parametric state
|
||||||
|
lives in standard IFC (extrusion depth, IfcMaterialLayerSetUsage
|
||||||
|
with LayerSetDirection AXIS3). Any LAYER3 slab qualifies."""
|
||||||
|
if element is None or not element.is_a("IfcSlab"):
|
||||||
|
return False
|
||||||
|
return tool.Model.get_usage_type(element) == "LAYER3"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_pipe_segment(cls, element: entity_instance) -> bool:
|
||||||
|
return element is not None and element.is_a("IfcPipeSegment")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_duct_segment(cls, element: entity_instance) -> bool:
|
||||||
|
return element is not None and element.is_a("IfcDuctSegment")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_editing_railing_path(cls, obj: bpy.types.Object) -> bool:
|
||||||
props = tool.Model.get_railing_props(obj)
|
props = tool.Model.get_railing_props(obj)
|
||||||
return props.is_editing_path
|
return props.is_editing_path
|
||||||
|
|
||||||
@@ -1242,79 +1444,6 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
feature = tool.Parametric.find_for_element(element)
|
feature = tool.Parametric.find_for_element(element)
|
||||||
return bool(feature and feature.has_non_editable_path)
|
return bool(feature and feature.has_non_editable_path)
|
||||||
|
|
||||||
class Array:
|
|
||||||
@classmethod
|
|
||||||
def bake_children_transform(cls, parent_element: entity_instance, item: int) -> None:
|
|
||||||
modifier_data = list(cls.get_modifiers_data(parent_element))[item]
|
|
||||||
children = cls.get_children_objects(modifier_data)
|
|
||||||
for child in children:
|
|
||||||
constraint = next((c for c in child.constraints if c.type == "CHILD_OF"), None)
|
|
||||||
if constraint:
|
|
||||||
with bpy.context.temp_override(object=child):
|
|
||||||
bpy.ops.constraint.apply(constraint=constraint.name, owner="OBJECT")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def constrain_children_to_parent(cls, parent_element: ifcopenshell.entity_instance) -> None:
|
|
||||||
if not (parent_obj := tool.Ifc.get_object(parent_element)):
|
|
||||||
return # Filtered out, arrayed void, etc
|
|
||||||
assert isinstance(parent_obj, bpy.types.Object)
|
|
||||||
children = cls.get_all_children_objects(parent_element)
|
|
||||||
for child in children:
|
|
||||||
constraint = next((c for c in child.constraints if c.type == "CHILD_OF"), None)
|
|
||||||
if constraint:
|
|
||||||
child.constraints.remove(constraint)
|
|
||||||
constraint = child.constraints.new("CHILD_OF")
|
|
||||||
constraint.name = "BBIM_Array_CHILD_OF"
|
|
||||||
assert isinstance(constraint, bpy.types.ChildOfConstraint)
|
|
||||||
constraint.target = parent_obj
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def set_children_lock_state(
|
|
||||||
cls, parent_element: ifcopenshell.entity_instance, item: int, lock_state: bool = True
|
|
||||||
) -> None:
|
|
||||||
modifier_data = list(cls.get_modifiers_data(parent_element))[item]
|
|
||||||
children = cls.get_children_objects(modifier_data)
|
|
||||||
for child_obj in children:
|
|
||||||
Blender.lock_transform(child_obj, lock_state)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def remove_constraints(cls, parent_element: ifcopenshell.entity_instance) -> None:
|
|
||||||
children = cls.get_all_children_objects(parent_element)
|
|
||||||
for child in children:
|
|
||||||
constraint = next((c for c in child.constraints if c.type == "CHILD_OF"), None)
|
|
||||||
if constraint:
|
|
||||||
child.constraints.remove(constraint)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_objects(cls, parent_element: ifcopenshell.entity_instance) -> list[bpy.types.Object]:
|
|
||||||
parent_obj = tool.Ifc.get_object(parent_element)
|
|
||||||
assert isinstance(parent_obj, bpy.types.Object)
|
|
||||||
children_objects = list(cls.get_all_children_objects(parent_element))
|
|
||||||
array_objects = [parent_obj] + children_objects # We ensure the parent is at index 0
|
|
||||||
return array_objects
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_all_children_objects(
|
|
||||||
cls, parent_element: ifcopenshell.entity_instance
|
|
||||||
) -> Generator[bpy.types.Object, None, None]:
|
|
||||||
for array_modifier in cls.get_modifiers_data(parent_element):
|
|
||||||
yield from cls.get_children_objects(array_modifier)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_modifiers_data(
|
|
||||||
cls, parent_element: ifcopenshell.entity_instance
|
|
||||||
) -> Generator[dict[str, Any], None, None]:
|
|
||||||
array_pset = ifcopenshell.util.element.get_pset(parent_element, "BBIM_Array")
|
|
||||||
yield from json.loads(array_pset["Data"])
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_children_objects(cls, modifier_data: dict[str, Any]) -> Generator[bpy.types.Object, None, None]:
|
|
||||||
child_guid: str
|
|
||||||
for child_guid in modifier_data["children"]:
|
|
||||||
child_obj = tool.Blender.get_object_from_guid(child_guid)
|
|
||||||
if child_obj:
|
|
||||||
yield child_obj
|
|
||||||
|
|
||||||
class Attribute:
|
class Attribute:
|
||||||
@classmethod
|
@classmethod
|
||||||
def fill_attribute(cls, data: bpy.types.ID, attribute_name: str, domain: str, data_type: str, values):
|
def fill_attribute(cls, data: bpy.types.ID, attribute_name: str, domain: str, data_type: str, values):
|
||||||
|
|||||||
Reference in New Issue
Block a user