diff --git a/src/bonsai/bonsai/bim/module/aggregate/decorator.py b/src/bonsai/bonsai/bim/module/aggregate/decorator.py
index eb389a58bd..f13f189641 100644
--- a/src/bonsai/bonsai/bim/module/aggregate/decorator.py
+++ b/src/bonsai/bonsai/bim/module/aggregate/decorator.py
@@ -20,7 +20,6 @@ import blf
import bpy
import gpu
import ifcopenshell.util.element
-from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
@@ -28,12 +27,6 @@ from mathutils import Vector
import bonsai.tool as tool
-def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
-
def create_bounding_box(objs):
# Initialize the bounding box coordinates
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
@@ -79,26 +72,8 @@ def create_bounding_box(objs):
return indices, edges
-class AggregateDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_aggregate, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
+class AggregateDecorator(tool.Blender.ViewportDecorator):
+ draw_method = "draw_aggregate"
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
@@ -153,14 +128,6 @@ class AggregateDecorator:
shader.uniform_float("u_Scale", 25)
batch.draw(shader)
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
-
def draw_aggregate(self, context):
props = tool.Aggregate.get_aggregate_props()
self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -225,39 +192,11 @@ class AggregateDecorator:
self.draw_custom_batch(line, decorator_color_unselected)
-class AggregateModeDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(
- SpaceView3D.draw_handler_add(handler.draw_aggregate_name, (context,), "WINDOW", "POST_PIXEL")
- )
- cls.handlers.append(
- SpaceView3D.draw_handler_add(handler.draw_aggregate_empty, (context,), "WINDOW", "POST_VIEW")
- )
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
+class AggregateModeDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_aggregate_name", "POST_PIXEL"),
+ ("draw_aggregate_empty", "POST_VIEW"),
+ )
def draw_aggregate_name(self, context):
if context.mode == "EDIT_MESH":
diff --git a/src/bonsai/bonsai/bim/module/boundary/decorator.py b/src/bonsai/bonsai/bim/module/boundary/decorator.py
index a2d134a135..c879dc72db 100644
--- a/src/bonsai/bonsai/bim/module/boundary/decorator.py
+++ b/src/bonsai/bonsai/bim/module/boundary/decorator.py
@@ -56,11 +56,6 @@ class BoundaryDecorator:
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
- def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
@@ -109,7 +104,11 @@ class BoundaryDecorator:
if unselected_edges:
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
- self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
+ self.draw_batch(
+ "TRIS", unselected_vertices, tool.Blender.transparent_color(special_elements_color), unselected_tris
+ )
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)
+ self.draw_batch(
+ "TRIS", selected_vertices, tool.Blender.transparent_color(selected_elements_color), selected_tris
+ )
diff --git a/src/bonsai/bonsai/bim/module/clash/decorator.py b/src/bonsai/bonsai/bim/module/clash/decorator.py
index ab95c92d9f..6e5baa9f45 100644
--- a/src/bonsai/bonsai/bim/module/clash/decorator.py
+++ b/src/bonsai/bonsai/bim/module/clash/decorator.py
@@ -18,43 +18,17 @@
import blf
import gpu
-from bpy.types import SpaceView3D
from bpy_extras.view3d_utils import location_3d_to_region_2d
-from gpu_extras.batch import batch_for_shader
from mathutils import Vector
import bonsai.tool as tool
-class ClashDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
+class ClashDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_text", "POST_PIXEL"),
+ ("draw_geometry", "POST_VIEW"),
+ )
def draw_text(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences()
diff --git a/src/bonsai/bonsai/bim/module/geometry/decorator.py b/src/bonsai/bonsai/bim/module/geometry/decorator.py
index 589b18ec84..2da7162fc7 100644
--- a/src/bonsai/bonsai/bim/module/geometry/decorator.py
+++ b/src/bonsai/bonsai/bim/module/geometry/decorator.py
@@ -16,8 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Bonsai. If not, see .
-from collections.abc import Sequence
-
import blf
import bpy
import gpu
@@ -25,15 +23,16 @@ import ifcopenshell
import numpy as np
from bpy.types import SpaceView3D
from bpy_extras.view3d_utils import location_3d_to_region_2d
-from gpu_extras.batch import batch_for_shader
from mathutils import Matrix, Vector
import bonsai.tool as tool
-class ItemDecorator:
- is_installed = False
- handlers = []
+class ItemDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_text", "POST_PIXEL"),
+ ("draw", "POST_VIEW"),
+ )
objs: dict[str, dict[str, list]]
obj_is_selected: dict[str, bool]
obj_is_boolean: dict[str, list[ifcopenshell.entity_instance]]
@@ -119,23 +118,6 @@ class ItemDecorator:
"special_edges": special_edges,
}
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
-
def draw_text(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
@@ -163,11 +145,6 @@ class ItemDecorator:
blf.disable(font_id, blf.SHADOW)
def draw(self, context: bpy.types.Context) -> None:
- def transparent_color(color: Sequence[float], alpha: float = 0.05) -> list[float]:
- color = [i for i in color]
- color[3] = alpha
- return color
-
self.addon_prefs = tool.Blender.get_addon_preferences()
selected_elements_color = self.addon_prefs.decorator_color_selected
unselected_elements_color = self.addon_prefs.decorator_color_unselected
@@ -197,15 +174,33 @@ class ItemDecorator:
if context.mode != "OBJECT":
continue
self.draw_batch("LINES", data["verts"], selected_elements_color, data["edges"])
- self.draw_batch("TRIS", data["verts"], transparent_color(selected_elements_color), data["tris"])
+ self.draw_batch(
+ "TRIS",
+ data["verts"],
+ tool.Blender.transparent_color(selected_elements_color, alpha=0.05),
+ data["tris"],
+ )
self.draw_batch("LINES", data["special_verts"], selected_elements_color, data["special_edges"])
elif self.obj_is_boolean[obj_name]:
self.draw_batch("LINES", data["verts"], special_elements_color, data["edges"])
- self.draw_batch("TRIS", data["verts"], transparent_color(special_elements_color), data["tris"])
+ self.draw_batch(
+ "TRIS",
+ data["verts"],
+ tool.Blender.transparent_color(special_elements_color, alpha=0.05),
+ data["tris"],
+ )
self.draw_batch("LINES", data["special_verts"], special_elements_color, data["special_edges"])
else:
self.draw_batch(
- "LINES", data["verts"], transparent_color(unselected_elements_color, alpha=0.2), data["edges"]
+ "LINES",
+ data["verts"],
+ tool.Blender.transparent_color(unselected_elements_color, alpha=0.2),
+ data["edges"],
+ )
+ self.draw_batch(
+ "TRIS",
+ data["verts"],
+ tool.Blender.transparent_color(special_elements_color, alpha=0.05),
+ data["tris"],
)
- self.draw_batch("TRIS", data["verts"], transparent_color(special_elements_color), data["tris"])
self.draw_batch("LINES", data["special_verts"], special_elements_color, data["special_edges"])
diff --git a/src/bonsai/bonsai/bim/module/georeference/decorator.py b/src/bonsai/bonsai/bim/module/georeference/decorator.py
index 05bce0e20b..c0b9ef0b27 100644
--- a/src/bonsai/bonsai/bim/module/georeference/decorator.py
+++ b/src/bonsai/bonsai/bim/module/georeference/decorator.py
@@ -21,7 +21,6 @@ from math import radians
import blf
import gpu
import ifcopenshell.util.geolocation
-from bpy.types import SpaceView3D
from bpy_extras.view3d_utils import location_3d_to_region_2d
from gpu_extras.batch import batch_for_shader
from mathutils import Matrix, Vector
@@ -30,27 +29,11 @@ import bonsai.tool as tool
from bonsai.bim.module.georeference.data import GeoreferenceData
-class GeoreferenceDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
+class GeoreferenceDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_text", "POST_PIXEL"),
+ ("draw_geometry", "POST_VIEW"),
+ )
def draw_batch(self, shader_type, content_pos, color, indices=None, should_scale=True):
if not tool.Blender.validate_shader_batch_data(content_pos, indices):
diff --git a/src/bonsai/bonsai/bim/module/light/decorator.py b/src/bonsai/bonsai/bim/module/light/decorator.py
index f72a941ab8..829657180f 100644
--- a/src/bonsai/bonsai/bim/module/light/decorator.py
+++ b/src/bonsai/bonsai/bim/module/light/decorator.py
@@ -20,44 +20,18 @@
import blf
import bpy
import gpu
-from bpy.types import SpaceView3D
from bpy_extras.view3d_utils import location_3d_to_region_2d
-from gpu_extras.batch import batch_for_shader
from mathutils import Matrix, Vector
import bonsai.tool as tool
from bonsai.bim.module.light.data import SolarData
-class SolarDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_geometry, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
+class SolarDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_text", "POST_PIXEL"),
+ ("draw_geometry", "POST_VIEW"),
+ )
def draw_text(self, context: bpy.types.Context) -> None:
self.addon_prefs = tool.Blender.get_addon_preferences()
diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py
index 8ec53af5a8..65691f6edf 100644
--- a/src/bonsai/bonsai/bim/module/model/decorator.py
+++ b/src/bonsai/bonsai/bim/module/model/decorator.py
@@ -53,12 +53,6 @@ from bonsai.bim.module.drawing.gizmos import (
from bonsai.bim.module.drawing.helper import format_distance
-def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
-
def highlight_color(color, alpha=0.1):
color = [i + (1 - i) * 0.5 for i in color]
return color
@@ -133,7 +127,7 @@ class ProfileDecorator:
def draw_faces(self, bm, vertices_coords):
"""Submit a non-mutating beauty-triangulated TRIS batch over ``bm``'s faces."""
- faces_color = transparent_color(self.addon_prefs.decorator_color_special)
+ faces_color = tool.Blender.transparent_color(self.addon_prefs.decorator_color_special)
tool.Blender.draw_bmesh_face_tris(bm, vertices_coords, faces_color, self.draw_batch)
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
@@ -263,7 +257,7 @@ class ProfileDecorator:
self.draw_batch("LINES", all_vertices, unselected_elements_color, unselected_edges)
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
- self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
+ self.draw_batch("POINTS", unselected_vertices, tool.Blender.transparent_color(unselected_elements_color, 0.5))
self.draw_batch("POINTS", error_vertices, error_elements_color)
self.draw_batch("POINTS", special_vertices, special_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
@@ -354,9 +348,11 @@ class ProfileDecorator:
return points, listEdg
-class PolylineDecorator:
- is_installed = False
- handlers = []
+class PolylineDecorator(tool.Blender.ViewportDecorator):
+ # draw_methods declares only the always-bound handler so the base's
+ # __init_subclass__ validation passes; the override install below
+ # conditionally registers up to four more handlers based on ui_only.
+ draw_methods = (("draw_input_ui", "POST_PIXEL"),)
event = None
input_type = None
input_ui = None
@@ -392,15 +388,6 @@ class PolylineDecorator:
cls.handlers.append(SpaceView3D.draw_handler_add(handler, (context,), "WINDOW", "POST_VIEW"))
cls.is_installed = True
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
@classmethod
def update(
cls,
@@ -459,14 +446,6 @@ class PolylineDecorator:
return {"verts": verts, "edges": edges, "tris": tris}
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
-
def shader_config(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences()
self.decorator_color = self.addon_prefs.decorations_colour
@@ -734,7 +713,9 @@ class PolylineDecorator:
if self.polyline_data.measurement_type == "POLY_AREA" and area:
if float(area) > 0:
tris = self.calculate_polygon(polyline_verts)["tris"]
- self.draw_batch("TRIS", polyline_verts, transparent_color(self.decorator_color_special), tris)
+ self.draw_batch(
+ "TRIS", polyline_verts, tool.Blender.transparent_color(self.decorator_color_special), tris
+ )
# Draw polyline with selected points
self.line_shader.uniform_float("lineWidth", 2.0)
@@ -987,9 +968,8 @@ class PolylineDecorator:
self.draw_batch("LINES", polyline_verts, decorator_color_unselected, polyline_edges)
-class ProductDecorator:
- is_installed = False
- handlers = []
+class ProductDecorator(tool.Blender.ViewportDecorator):
+ draw_method = "draw_product_preview"
preview_mode: Literal["PROFILE_VERTICAL", "PROFILE_HORIZONTAL", "LAYER2", "LAYER3", "GENERIC"]
relating_type = None
obj_data: dict[str, list] = {}
@@ -1032,29 +1012,7 @@ class ProductDecorator:
)
cls.is_installed = True
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
-
def draw_product_preview(self, context):
- def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
self.addon_prefs = tool.Blender.get_addon_preferences()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
@@ -1086,7 +1044,7 @@ class ProductDecorator:
data = self.get_generic_preview_data()
if data:
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
- self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color), data["tris"])
+ self.draw_batch("TRIS", data["verts"], tool.Blender.transparent_color(decorator_color), data["tris"])
def get_wall_preview_data(self):
relating_type = self.relating_type
@@ -1619,34 +1577,8 @@ class ProductDecorator:
return data
-class WallAxisDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_wall_axis, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
+class WallAxisDecorator(tool.Blender.ViewportDecorator):
+ draw_method = "draw_wall_axis"
def draw_wall_axis(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -1685,34 +1617,8 @@ class WallAxisDecorator:
self.draw_batch("LINES", arrow, unselected_elements_color, [(0, 1), (1, 2), (1, 3)])
-class SlabDirectionDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_wall_axis, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
+class SlabDirectionDecorator(tool.Blender.ViewportDecorator):
+ draw_method = "draw_wall_axis"
def draw_wall_axis(self, context):
self.addon_prefs = tool.Blender.get_addon_preferences()
@@ -1742,41 +1648,10 @@ class SlabDirectionDecorator:
self.draw_batch("LINES", base, selected_elements_color, [(0, 1)])
-class FaceAreaDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_face_area, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
+class FaceAreaDecorator(tool.Blender.ViewportDecorator):
+ draw_method = "draw_face_area"
def draw_face_area(self, context):
- def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
self.addon_prefs = tool.Blender.get_addon_preferences()
self.line_shader = gpu.shader.from_builtin("POLYLINE_UNIFORM_COLOR")
self.line_shader.bind() # required to be able to change uniforms of the shader
@@ -1797,12 +1672,16 @@ class FaceAreaDecorator:
if data:
self.draw_batch("POINTS", data["verts"], decorator_color)
self.draw_batch("LINES", data["verts"], decorator_color, data["edges"])
- self.draw_batch("TRIS", data["verts"], transparent_color(decorator_color, alpha=0.5), data["tris"])
+ self.draw_batch(
+ "TRIS", data["verts"], tool.Blender.transparent_color(decorator_color, alpha=0.5), data["tris"]
+ )
-class BoundingBoxDecorator:
- is_installed = False
- handlers = []
+class BoundingBoxDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_bounding_box_wire_cube", "POST_VIEW"),
+ ("draw_dimension_text", "POST_PIXEL"),
+ )
def __init__(self):
context = bpy.context
@@ -1813,31 +1692,6 @@ class BoundingBoxDecorator:
self.decorator_color_wire = (*theme.view_3d.bone_solid, 1)
self.decorator_color_special = tool.Blender.get_addon_preferences().decorator_color_special
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(
- bpy.types.SpaceView3D.draw_handler_add(
- handler.draw_bounding_box_wire_cube, (context,), "WINDOW", "POST_VIEW"
- )
- )
- cls.handlers.append(
- bpy.types.SpaceView3D.draw_handler_add(handler.draw_dimension_text, (context,), "WINDOW", "POST_PIXEL")
- )
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- bpy.types.SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except Exception:
- pass
- cls.handlers.clear()
- cls.is_installed = False
-
@staticmethod
def get_combined_bounding_box_corners(objects):
@@ -1910,14 +1764,6 @@ class BoundingBoxDecorator:
]
return trihedron[best_origin]
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
-
def draw_text_background(self, context, coords_dim, text_dim):
padding = 5
theme = context.preferences.themes.items()[0][1]
diff --git a/src/bonsai/bonsai/bim/module/nest/decorator.py b/src/bonsai/bonsai/bim/module/nest/decorator.py
index 28c3835ba7..346a470172 100644
--- a/src/bonsai/bonsai/bim/module/nest/decorator.py
+++ b/src/bonsai/bonsai/bim/module/nest/decorator.py
@@ -20,7 +20,6 @@ import blf
import bpy
import gpu
import ifcopenshell.util.element
-from bpy.types import SpaceView3D
from bpy_extras import view3d_utils
from gpu_extras.batch import batch_for_shader
from mathutils import Vector
@@ -28,12 +27,6 @@ from mathutils import Vector
import bonsai.tool as tool
-def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
-
def create_bounding_box(objs):
# Initialize the bounding box coordinates
min_x, min_y, min_z = float("inf"), float("inf"), float("inf")
@@ -79,26 +72,8 @@ def create_bounding_box(objs):
return indices, edges
-class NestDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
+class NestDecorator(tool.Blender.ViewportDecorator):
+ draw_method = "draw_nest"
def dotted_line_shader(self):
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
@@ -154,14 +129,6 @@ class NestDecorator:
shader.uniform_float("u_Scale", 25)
batch.draw(shader)
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
-
def draw_nest(self, context: bpy.types.Context) -> None:
props = tool.Nest.get_nest_props()
if props.in_nest_mode:
@@ -226,35 +193,11 @@ class NestDecorator:
self.draw_custom_batch(line, decorator_color_unselected)
-class NestModeDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest_name, (context,), "WINDOW", "POST_PIXEL"))
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_nest_empty, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
+class NestModeDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_nest_name", "POST_PIXEL"),
+ ("draw_nest_empty", "POST_VIEW"),
+ )
def draw_nest_name(self, context):
if context.mode == "EDIT_MESH":
diff --git a/src/bonsai/bonsai/bim/module/project/decorator.py b/src/bonsai/bonsai/bim/module/project/decorator.py
index 72090a769b..7af79d3add 100644
--- a/src/bonsai/bonsai/bim/module/project/decorator.py
+++ b/src/bonsai/bonsai/bim/module/project/decorator.py
@@ -42,12 +42,6 @@ def toggle_decorations_on_load(*args):
# as queried object is linked from separate .blend file.
-def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
-
class ProjectDecorator:
installed = None
@@ -80,11 +74,6 @@ class ProjectDecorator:
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
- def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
@@ -110,7 +99,9 @@ class ProjectDecorator:
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)
+ self.draw_batch(
+ "TRIS", selected_vertices, tool.Blender.transparent_color(selected_elements_color), geom.selected_tris
+ )
class ClippingPlaneDecorator:
@@ -145,11 +136,6 @@ class ClippingPlaneDecorator:
unselected_elements_color = self.addon_prefs.decorator_color_unselected
special_elements_color = self.addon_prefs.decorator_color_special
- def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
gpu.state.point_size_set(6)
gpu.state.blend_set("ALPHA")
@@ -210,37 +196,21 @@ class ClippingPlaneDecorator:
if unselected_edges:
self.draw_batch("LINES", unselected_vertices, special_elements_color, unselected_edges)
- self.draw_batch("TRIS", unselected_vertices, transparent_color(special_elements_color), unselected_tris)
+ self.draw_batch(
+ "TRIS", unselected_vertices, tool.Blender.transparent_color(special_elements_color), unselected_tris
+ )
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)
+ self.draw_batch(
+ "TRIS", selected_vertices, tool.Blender.transparent_color(selected_elements_color), selected_tris
+ )
-class MeasureDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(
- SpaceView3D.draw_handler_add(handler.draw_measurements_text, (context,), "WINDOW", "POST_PIXEL")
- )
- cls.handlers.append(
- SpaceView3D.draw_handler_add(handler.draw_measurements_poly, (context,), "WINDOW", "POST_VIEW")
- )
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
+class MeasureDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_measurements_text", "POST_PIXEL"),
+ ("draw_measurements_poly", "POST_VIEW"),
+ )
def draw_measurements_text(self, context):
PolylineDecorator().select_and_draw_measurements_text(context)
diff --git a/src/bonsai/bonsai/bim/module/spatial/decorator.py b/src/bonsai/bonsai/bim/module/spatial/decorator.py
index 047a9ff555..6f97ae86d9 100644
--- a/src/bonsai/bonsai/bim/module/spatial/decorator.py
+++ b/src/bonsai/bonsai/bim/module/spatial/decorator.py
@@ -18,43 +18,17 @@
import blf
import gpu
-from bpy.types import SpaceView3D
from bpy_extras.view3d_utils import location_3d_to_region_2d
-from gpu_extras.batch import batch_for_shader
from mathutils import Vector
import bonsai.tool as tool
-class GridDecorator:
- is_installed = False
- handlers = []
-
- @classmethod
- def install(cls, context):
- if cls.is_installed:
- cls.uninstall()
- handler = cls()
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw_text, (context,), "WINDOW", "POST_PIXEL"))
- cls.handlers.append(SpaceView3D.draw_handler_add(handler.draw, (context,), "WINDOW", "POST_VIEW"))
- cls.is_installed = True
-
- @classmethod
- def uninstall(cls):
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
- def draw_batch(self, shader_type, content_pos, color, indices=None):
- if not tool.Blender.validate_shader_batch_data(content_pos, indices):
- return
- shader = self.line_shader if shader_type == "LINES" else self.shader
- batch = batch_for_shader(shader, shader_type, {"pos": content_pos}, indices=indices)
- shader.uniform_float("color", color)
- batch.draw(shader)
+class GridDecorator(tool.Blender.ViewportDecorator):
+ draw_methods = (
+ ("draw_text", "POST_PIXEL"),
+ ("draw", "POST_VIEW"),
+ )
def draw_text(self, context):
if not tool.Blender.is_addon_enabled():
diff --git a/src/bonsai/bonsai/bim/module/structural/decorator.py b/src/bonsai/bonsai/bim/module/structural/decorator.py
index 32925be61c..3de45bd0e5 100644
--- a/src/bonsai/bonsai/bim/module/structural/decorator.py
+++ b/src/bonsai/bonsai/bim/module/structural/decorator.py
@@ -31,11 +31,17 @@ import bonsai.tool as tool
from bonsai.bim.module.structural.load_decoration_data import ShaderInfo
-class LoadsDecorator:
+class LoadsDecorator(tool.Blender.ViewportDecorator):
"""Decorator to show structural loads in 3D"""
- is_installed = False
- handlers = []
+ # draw_methods exists to satisfy ViewportDecorator.__init_subclass__'s
+ # method-existence check; the override install below is what actually
+ # registers handlers (the POST_VIEW binding passes no context arg, which
+ # the base's generic install cannot express).
+ draw_methods = (
+ ("draw_load_values", "POST_PIXEL"),
+ ("__call__", "POST_VIEW"),
+ )
decoration_data = None
text_info = []
shader_info = []
@@ -54,15 +60,6 @@ class LoadsDecorator:
cls.update()
cls.is_installed = True
- @classmethod
- def uninstall(cls) -> None:
- for handler in cls.handlers:
- try:
- SpaceView3D.draw_handler_remove(handler, "WINDOW")
- except ValueError:
- pass
- cls.is_installed = False
-
@classmethod
def update(cls) -> None:
cls.decoration_data.update()
diff --git a/src/bonsai/bonsai/bim/module/system/decorator.py b/src/bonsai/bonsai/bim/module/system/decorator.py
index 4c3e810ceb..1770f99e86 100644
--- a/src/bonsai/bonsai/bim/module/system/decorator.py
+++ b/src/bonsai/bonsai/bim/module/system/decorator.py
@@ -31,12 +31,6 @@ ERROR_ELEMENTS_COLOR = (1, 0.2, 0.322, 1) # RED
UNSPECIAL_ELEMENT_COLOR = (0.2, 0.2, 0.2, 1) # GREY
-def transparent_color(color, alpha=0.1):
- color = [i for i in color]
- color[3] = alpha
- return color
-
-
@persistent
def toggle_decorations_on_load(*args):
props = tool.System.get_system_props()
@@ -80,7 +74,7 @@ class SystemDecorator:
def draw_faces(self, bm, vertices_coords):
"""Submit a non-mutating beauty-triangulated TRIS batch over ``bm``'s faces."""
- faces_color = transparent_color(self.addon_prefs.decorator_color_special)
+ faces_color = tool.Blender.transparent_color(self.addon_prefs.decorator_color_special)
tool.Blender.draw_bmesh_face_tris(bm, vertices_coords, faces_color, self.draw_batch)
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
@@ -128,13 +122,15 @@ class SystemDecorator:
self.shader = gpu.shader.from_builtin("UNIFORM_COLOR")
self.shader.bind()
- self.draw_batch("LINES", all_vertices, transparent_color(unselected_elements_color), unselected_edges)
+ self.draw_batch(
+ "LINES", all_vertices, tool.Blender.transparent_color(unselected_elements_color), unselected_edges
+ )
self.draw_batch("LINES", all_vertices, selected_elements_color, selected_edges)
self.draw_batch("LINES", all_vertices, UNSPECIAL_ELEMENT_COLOR, arc_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, preview_edges)
self.draw_batch("LINES", all_vertices, special_elements_color, roof_angle_edges)
- self.draw_batch("POINTS", unselected_vertices, transparent_color(unselected_elements_color, 0.5))
+ self.draw_batch("POINTS", unselected_vertices, tool.Blender.transparent_color(unselected_elements_color, 0.5))
self.draw_batch("POINTS", error_vertices, ERROR_ELEMENTS_COLOR)
self.draw_batch("POINTS", special_vertices, special_elements_color)
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
diff --git a/src/bonsai/pytest.ini b/src/bonsai/pytest.ini
index f4dbb884b6..43c5f82d32 100644
--- a/src/bonsai/pytest.ini
+++ b/src/bonsai/pytest.ini
@@ -7,9 +7,11 @@ markers =
boundary
brick
bsdd
+ clash
classification
clip_box
context
+ contract_guard
cost
covering
debug
diff --git a/src/bonsai/test/bim/module/clash/__init__.py b/src/bonsai/test/bim/module/clash/__init__.py
new file mode 100644
index 0000000000..6fe44a6223
--- /dev/null
+++ b/src/bonsai/test/bim/module/clash/__init__.py
@@ -0,0 +1,17 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2026
+#
+# This file is part of Bonsai.
+#
+# Bonsai is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Bonsai is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Bonsai. If not, see .
diff --git a/src/bonsai/test/bim/module/clash/test_clash_decorator_handlers_cleared.py b/src/bonsai/test/bim/module/clash/test_clash_decorator_handlers_cleared.py
new file mode 100644
index 0000000000..d79956f6f5
--- /dev/null
+++ b/src/bonsai/test/bim/module/clash/test_clash_decorator_handlers_cleared.py
@@ -0,0 +1,52 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2026
+#
+# This file is part of Bonsai.
+#
+# Bonsai is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Bonsai is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Bonsai. If not, see .
+#
+# This file was generated with the assistance of an AI coding tool.
+
+"""Runtime regression: viewport-decorator install / uninstall keeps the
+``handlers`` list empty across repeated cycles.
+
+ClashDecorator is the representative subclass — its lifecycle is now
+inherited from ``tool.Blender.ViewportDecorator``. The contract pinned
+here is the canonical one for every subclass: after each ``uninstall``,
+``cls.handlers`` must be empty and ``cls.is_installed`` must be False."""
+
+import bpy
+import pytest
+
+from bonsai.bim.module.clash.decorator import ClashDecorator
+
+pytestmark = pytest.mark.clash
+
+
+@pytest.fixture(autouse=True)
+def _reset_decorator_state():
+ ClashDecorator.uninstall()
+ yield
+ ClashDecorator.uninstall()
+
+
+def test_clash_decorator_handlers_cleared_across_install_cycles():
+ ctx = bpy.context
+ for _ in range(3):
+ ClashDecorator.install(ctx)
+ assert ClashDecorator.is_installed is True
+ assert len(ClashDecorator.handlers) > 0
+ ClashDecorator.uninstall()
+ assert ClashDecorator.is_installed is False
+ assert ClashDecorator.handlers == []
diff --git a/src/bonsai/test/bim/test_decorator_handlers_clear_forward_compat.py b/src/bonsai/test/bim/test_decorator_handlers_clear_forward_compat.py
new file mode 100644
index 0000000000..9f9cb011e2
--- /dev/null
+++ b/src/bonsai/test/bim/test_decorator_handlers_clear_forward_compat.py
@@ -0,0 +1,86 @@
+# Bonsai - OpenBIM Blender Add-on
+# Copyright (C) 2026
+#
+# This file is part of Bonsai.
+#
+# Bonsai is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Bonsai is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Bonsai. If not, see .
+#
+# This file was generated with the assistance of an AI coding tool.
+
+"""Forward-compat AST contract for viewport decorator lifecycle.
+
+Any class that tracks Blender draw handlers via a class-level ``handlers``
+list MUST subclass ``tool.Blender.ViewportDecorator``. The base sets
+``handlers = []`` and ``is_installed = False`` via ``__init_subclass__`` and
+provides install / uninstall with the correct ``cls.handlers.clear()``.
+A class that declares its own ``handlers = []`` outside the base duplicates
+the lifecycle and is at risk of regressing the handler-clear bug class."""
+
+import ast
+from pathlib import Path
+
+import pytest
+
+pytestmark = pytest.mark.contract_guard
+
+ADDON_ROOT = Path(__file__).parent.parent.parent / "bonsai"
+DECORATORS_GLOB = "bim/module/**/decorator.py"
+
+
+def _is_empty_handlers_list(target: ast.expr, value: ast.expr | None) -> bool:
+ return isinstance(target, ast.Name) and target.id == "handlers" and isinstance(value, ast.List) and not value.elts
+
+
+def _has_handlers_list_class_attr(class_node: ast.ClassDef) -> bool:
+ for node in class_node.body:
+ if isinstance(node, ast.Assign):
+ for target in node.targets:
+ if _is_empty_handlers_list(target, node.value):
+ return True
+ elif isinstance(node, ast.AnnAssign):
+ if _is_empty_handlers_list(node.target, node.value):
+ return True
+ return False
+
+
+def _subclasses_viewport_decorator(class_node: ast.ClassDef) -> bool:
+ for base in class_node.bases:
+ if isinstance(base, ast.Name) and base.id.endswith("ViewportDecorator"):
+ return True
+ if isinstance(base, ast.Attribute) and base.attr.endswith("ViewportDecorator"):
+ return True
+ return False
+
+
+def test_no_decorator_class_duplicates_viewport_lifecycle() -> None:
+ offenders: list[str] = []
+ for path in sorted(ADDON_ROOT.glob(DECORATORS_GLOB)):
+ tree = ast.parse(path.read_text(encoding="utf-8"))
+ for node in ast.walk(tree):
+ if not isinstance(node, ast.ClassDef):
+ continue
+ if not _has_handlers_list_class_attr(node):
+ continue
+ if _subclasses_viewport_decorator(node):
+ continue
+ rel = path.relative_to(ADDON_ROOT)
+ offenders.append(f"{rel.as_posix()}:{node.lineno}: class {node.name}")
+ if offenders:
+ listing = "\n ".join(offenders)
+ pytest.fail(
+ "Class(es) declare ``handlers = []`` at class scope without subclassing "
+ "``tool.Blender.ViewportDecorator``. Migrate to the canonical viewport-lifecycle "
+ "base (which sets handlers/is_installed via __init_subclass__ and provides "
+ "install/uninstall with the correct cls.handlers.clear()):\n " + listing
+ )