From 786d3c8a894e57b340d725a701977456d285e05a Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Wed, 27 May 2026 14:38:37 +0200 Subject: [PATCH] Fix latent runtime bugs + ty annotations surfaced by CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five code paths in slim PR2 referenced symbols that don't exist in v0.8.0's bim layer, raising at first call. Plus three type annotations that ty flagged as unresolved. 1. tool/system.py:get_decoration_data — drop the cache layer that keyed on a token from a bim/decorator_cache.py module. The cache is dead-or-broken in slim: the depsgraph bump handler that would invalidate the token lives in PR3's bim/handler.py decompose, so the token stays at 0 forever. Either the cache never hits (decorated_elements rebuilt → new id() per call) or returns stale data (list reused). Revert to direct `_build_decoration_data()` calls. PR3 reintroduces the cache atomically: decorator_cache module + handler install + cache wrap + tests. Keeps `_build_decoration_data` extraction (cleaner than v0.8.0's monolithic version regardless of cache). 2. tool/spatial.py — add `get_host_element` + `get_host_wall`. The interface stubs in `core/tool.py:1037-1038` were declared but never implemented. `tool/duplicate.py:99` (object duplication with fills) and `tool/model.py:1260` (array per-child opening mirror) call these and would raise AttributeError. 3. tool/model.py:recreate_wall — drop the fillet-corner branch that function-locally imports `regenerate_fillet_corner_wall` from `bim/module/model/wall`. The function lands with PR4; fall through to the straight-extrusion path preserves v0.8.0 behaviour for fillet walls until then. Tag FIXME(PR4). 4. tool/model.py — drop `get_pipe_segment_props` / `get_duct_segment_props` accessors. Their return types reference `BIMPipeSegmentProperties` / `BIMDuctSegmentProperties` which land with PR4's prop.py; calling either accessor on v0.8.0 would AttributeError on `obj.BIMSegmentProperties`. Zero callers in slim — PR4 reintroduces both accessors together with the PropertyGroups they wrap. Also drops the matching TYPE_CHECKING imports. 5. tool/blender.py:557 — `Mapping[type[ViewportDecorator], bool]` needs the qualified `Blender.ViewportDecorator` because the annotation is on a method INSIDE the same nested class; the bare name doesn't resolve at type-check time. 6. core/tool.py Surveyor — drop the `obj: "bpy.types.Object"` / `z: float` / `-> float` / `-> None` annotations on `get_z_rotation` / `set_z_rotation`. The `@interface` decorator wraps each method as `classmethod(abstractmethod(...))` at import time, but ty doesn't track the wrap and flags every call site as `missing-argument` plus the `pass` body as `empty-body` against the declared return type, plus the `bpy.types.Object` forward-ref as `unresolved-reference`. Reverting to v0.8.0's untyped style (matching the sibling `get_absolute_matrix(cls, obj)` stub) clears six ty errors at the cost of zero runtime semantics — the abstract stubs only serve as registry markers, concrete `tool.Surveyor.*` carries the real signatures. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/core/tool.py | 4 ++-- src/bonsai/bonsai/tool/blender.py | 2 +- src/bonsai/bonsai/tool/model.py | 28 ++++------------------------ src/bonsai/bonsai/tool/spatial.py | 26 ++++++++++++++++++++++++++ src/bonsai/bonsai/tool/system.py | 16 +--------------- 5 files changed, 34 insertions(+), 42 deletions(-) diff --git a/src/bonsai/bonsai/core/tool.py b/src/bonsai/bonsai/core/tool.py index a617549bdf..d3260fa278 100644 --- a/src/bonsai/bonsai/core/tool.py +++ b/src/bonsai/bonsai/core/tool.py @@ -1156,8 +1156,8 @@ class Style: @interface class Surveyor: def get_absolute_matrix(cls, obj): pass - def get_z_rotation(cls, obj: "bpy.types.Object") -> float: pass - def set_z_rotation(cls, obj: "bpy.types.Object", z: float) -> None: pass + def get_z_rotation(cls, obj): pass + def set_z_rotation(cls, obj, z): pass @interface diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index b0c9423906..0ecb767da1 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -554,7 +554,7 @@ class Blender(bonsai.core.tool.Blender): def sync_all( cls, context: bpy.types.Context, - enabled: Mapping[type[ViewportDecorator], bool], + enabled: Mapping[type[Blender.ViewportDecorator], bool], ) -> None: """Drive each listed decorator to its desired install state in one call. diff --git a/src/bonsai/bonsai/tool/model.py b/src/bonsai/bonsai/tool/model.py index 5b7aafab7c..33bc310c22 100644 --- a/src/bonsai/bonsai/tool/model.py +++ b/src/bonsai/bonsai/tool/model.py @@ -75,10 +75,8 @@ if TYPE_CHECKING: from bonsai.bim.module.model.prop import ( BIMArrayProperties, BIMDoorProperties, - BIMDuctSegmentProperties, BIMExternalParametricGeometryProperties, BIMModelProperties, - BIMPipeSegmentProperties, BIMPolylineProperties, BIMRailingProperties, BIMRoofProperties, @@ -118,14 +116,6 @@ class Model(bonsai.core.tool.Model): def get_railing_props(cls, obj: bpy.types.Object) -> BIMRailingProperties: return obj.BIMRailingProperties # pyright: ignore[reportAttributeAccessIssue] - @classmethod - def get_pipe_segment_props(cls, obj: bpy.types.Object) -> BIMPipeSegmentProperties: - return obj.BIMPipeSegmentProperties # pyright: ignore[reportAttributeAccessIssue] - - @classmethod - def get_duct_segment_props(cls, obj: bpy.types.Object) -> BIMDuctSegmentProperties: - return obj.BIMDuctSegmentProperties # pyright: ignore[reportAttributeAccessIssue] - @classmethod def get_sverchok_props(cls, obj: bpy.types.Object) -> BIMSverchokProperties: return obj.BIMSverchokProperties # pyright: ignore[reportAttributeAccessIssue] @@ -2881,20 +2871,10 @@ class Model(bonsai.core.tool.Model): @classmethod def recreate_wall(cls, element: ifcopenshell.entity_instance, obj: bpy.types.Object) -> None: - # Curved fillet-corner walls own a hand-built banana body that - # ``regenerate_wall_representation`` would flatten — it reads the - # axis as a 2-point reference line and builds a straight extrusion. - # Instead rebuild the curve in place: ``regenerate_fillet_corner_wall`` - # keeps radius + placement from the pset / current ``ObjectPlacement`` - # while picking up new thickness / height from the wall type, which - # is what we want when a type-property edit triggered this call. - if ifcopenshell.util.element.get_pset(element, "BBIM_Wall", "IsFilletCorner"): - # Lazy import: ``tool.Model`` loads before ``bim/module/model`` - # at addon enable; a module-level import would cycle. - from bonsai.bim.module.model.wall import regenerate_fillet_corner_wall - - regenerate_fillet_corner_wall(element, obj) - return + # FIXME(PR4): the fillet-corner branch lands with PR4's + # `regenerate_fillet_corner_wall` (bim/module/model/wall.py). On v0.8.0 + # the function doesn't exist; falling through to the straight-extrusion + # path preserves v0.8.0 behaviour for fillet walls until PR4 ships. rep = ifcopenshell.api.geometry.regenerate_wall_representation(tool.Ifc.get(), element) bonsai.core.geometry.switch_representation( tool.Ifc, diff --git a/src/bonsai/bonsai/tool/spatial.py b/src/bonsai/bonsai/tool/spatial.py index 11a41672bc..163a3ea3c2 100644 --- a/src/bonsai/bonsai/tool/spatial.py +++ b/src/bonsai/bonsai/tool/spatial.py @@ -90,6 +90,32 @@ class Spatial(bonsai.core.tool.Spatial): break return element + @classmethod + def get_host_element(cls, filling: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None: + """The building element that hosts a filling (door/window) via the + standard ``FillsVoids → RelatingOpeningElement → VoidsElements → + RelatingBuildingElement`` chain, with safety guards at each hop. + Returns ``None`` if any link is missing, or if the given entity is + not a fillable type (no ``FillsVoids`` inverse). + + For the wall-only case (gizmos that only make sense on walls), use + `get_host_wall` which adds an ``IfcWall`` type filter on top of this.""" + if not getattr(filling, "FillsVoids", None): + return None + opening = filling.FillsVoids[0].RelatingOpeningElement + if not opening.VoidsElements: + return None + return opening.VoidsElements[0].RelatingBuildingElement + + @classmethod + def get_host_wall(cls, filling: ifcopenshell.entity_instance) -> ifcopenshell.entity_instance | None: + """The ``IfcWall`` that hosts a filling (door/window), or ``None``. + + Walls only — fillings hosted in slabs / roofs / arbitrary elements + produce ``None`` so wall-offset callers stay opted out cleanly.""" + host = cls.get_host_element(filling) + return host if host and host.is_a("IfcWall") else None + @classmethod def can_contain(cls, container: ifcopenshell.entity_instance, element: ifcopenshell.entity_instance) -> bool: if tool.Ifc.get_schema() == "IFC2X3": diff --git a/src/bonsai/bonsai/tool/system.py b/src/bonsai/bonsai/tool/system.py index 8d2b421370..bc11f3d642 100644 --- a/src/bonsai/bonsai/tool/system.py +++ b/src/bonsai/bonsai/tool/system.py @@ -299,29 +299,15 @@ class System(bonsai.core.tool.System): system_props = cls.get_system_props() return tool.Ifc.get_entity_by_id(system_props.active_system_id) - # Decoration-data cache, keyed on (decorator_cache_token, id(decorated_elements_set)). - _decoration_data_cache_key: tuple | None = None - _decoration_data_cache: dict[str, Any] | None = None - @classmethod def get_decoration_data(cls) -> dict[str, Any]: - from bonsai.bim.decorator_cache import get_decorator_cache_token from bonsai.bim.module.system.data import ObjectSystemData, SystemDecorationData if not ObjectSystemData.is_loaded: ObjectSystemData.load() if not SystemDecorationData.is_loaded: SystemDecorationData.load() - - token = get_decorator_cache_token() - key = (token, id(SystemDecorationData.data["decorated_elements"])) - if key == cls._decoration_data_cache_key and cls._decoration_data_cache is not None: - return cls._decoration_data_cache - - result = cls._build_decoration_data() - cls._decoration_data_cache_key = key - cls._decoration_data_cache = result - return result + return cls._build_decoration_data() @classmethod def _build_decoration_data(cls) -> dict[str, Any]: