From 6367de6102418526c6816387aafd89ec6eecb7a0 Mon Sep 17 00:00:00 2001 From: Gorgious56 Date: Mon, 22 Jun 2026 10:40:01 +0200 Subject: [PATCH] Add tool.Blender.draw_quads utility Promotes the private _fill_quads_alpha helper from bim/module/model/decorator.py to tool.Blender.draw_quads so any feature decorator can reuse the same TRIS-batch path. The new utility accepts an optional outline_color so callers can draw fill, outline, or both in a single call. Migrates the only existing caller (WallGizmoPreviewDecorator in model/wall.py) to the public API and removes the local helper. Generated with the assistance of an AI coding tool. --- .../bonsai/bim/module/model/decorator.py | 40 --------- src/bonsai/bonsai/bim/module/model/wall.py | 3 +- src/bonsai/bonsai/tool/blender.py | 83 +++++++++++++++++++ 3 files changed, 84 insertions(+), 42 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/model/decorator.py b/src/bonsai/bonsai/bim/module/model/decorator.py index 0a2c2678ac..56b0df3bbe 100644 --- a/src/bonsai/bonsai/bim/module/model/decorator.py +++ b/src/bonsai/bonsai/bim/module/model/decorator.py @@ -2069,46 +2069,6 @@ class BoundingBoxDecorator: co2.y -= y_overlap / 2 + min_spacing -def _fill_quads_alpha( - context: bpy.types.Context, - quads: list[ - tuple[ - tuple[float, float, float], - tuple[float, float, float], - tuple[float, float, float], - tuple[float, float, float], - ] - ], - color_rgb: tuple[float, float, float], - alpha: float, -) -> None: - """Render ``quads`` (each a 4-tuple of world-space corner verts in CCW - order) as one TRIS batch with two triangles per quad.""" - if not quads: - return - verts: list[tuple[float, float, float]] = [] - indices: list[tuple[int, int, int]] = [] - for quad in quads: - if len(quad) != 4: - continue - base = len(verts) - verts.extend(tuple(v) for v in quad) - indices.append((base, base + 1, base + 2)) - indices.append((base, base + 2, base + 3)) - if not tool.Blender.validate_shader_batch_data(verts, indices): - return - region = getattr(context, "region", None) - if region is None: - return - shader = gpu.shader.from_builtin("UNIFORM_COLOR") - shader.bind() - shader.uniform_float("color", (*color_rgb, alpha)) - batch = batch_for_shader(shader, "TRIS", {"pos": verts}, indices=indices) - gpu.state.blend_set("ALPHA") - batch.draw(shader) - gpu.state.blend_set("NONE") - - def compute_mep_join_location(): """Midpoint between the closest endpoint pair of two selected MEP segments — the world location where a connecting fitting (bend / diff --git a/src/bonsai/bonsai/bim/module/model/wall.py b/src/bonsai/bonsai/bim/module/model/wall.py index ecbe544eb9..dd054635a7 100644 --- a/src/bonsai/bonsai/bim/module/model/wall.py +++ b/src/bonsai/bonsai/bim/module/model/wall.py @@ -63,7 +63,6 @@ from bonsai.bim.module.model.decorator import ( _BBOX_HIGHLIGHT_LINE_WIDTH, PolylineDecorator, ProductDecorator, - _fill_quads_alpha, bbox_world_edges, draw_polyline_segments, ) @@ -4859,7 +4858,7 @@ class WallGizmoPreviewDecorator(tool.Blender.ViewportDecorator): ], color_rgb: tuple[float, float, float], ) -> None: - _fill_quads_alpha(context, quads, color_rgb, self.QUAD_ALPHA) + tool.Blender.draw_quads(context, quads, fill_color=(*color_rgb, self.QUAD_ALPHA)) @staticmethod def _wall_floor_quad(mw: Matrix, x0: float, x1: float, y0: float, y1: float) -> tuple[ diff --git a/src/bonsai/bonsai/tool/blender.py b/src/bonsai/bonsai/tool/blender.py index c468b0572e..97656c3904 100644 --- a/src/bonsai/bonsai/tool/blender.py +++ b/src/bonsai/bonsai/tool/blender.py @@ -55,9 +55,11 @@ from typing import ( import bmesh import bpy +import gpu import ifcopenshell.util.element import numpy as np import numpy.typing as npt +from gpu_extras.batch import batch_for_shader from ifcopenshell import entity_instance from mathutils import Matrix, Vector @@ -2403,6 +2405,87 @@ class Blender(bonsai.core.tool.Blender): tris = [[loop.vert.index for loop in tri] for tri in bm.calc_loop_triangles()] draw_batch("TRIS", world_vert_coords, color, tris) + @classmethod + def draw_quads( + cls, + context: bpy.types.Context, + quads: Sequence[ + tuple[ + tuple[float, float, float], + tuple[float, float, float], + tuple[float, float, float], + tuple[float, float, float], + ] + ], + *, + fill_color: Optional[tuple[float, float, float, float]] = None, + outline_color: Optional[tuple[float, float, float, float]] = None, + outline_width: float = 1.0, + ) -> None: + """Render ``quads`` (each a 4-tuple of CCW world-space corners) as + a filled TRIS batch, an outline LINES batch, or both. + + Both colors are RGBA 4-tuples. Pass ``fill_color=None`` to skip + the fill pass and ``outline_color=None`` to skip the outline. + Skipping both is a no-op. + + Replaces the per-decorator quad-fill helpers that used to live + inline in each feature module. + """ + if not quads or (fill_color is None and outline_color is None): + return + region = getattr(context, "region", None) + if region is None: + return + + verts: list[tuple[float, float, float]] = [] + tri_indices: list[tuple[int, int, int]] = [] + line_indices: list[tuple[int, int]] = [] + for quad in quads: + if len(quad) != 4: + continue + base = len(verts) + verts.extend(tuple(v) for v in quad) + if fill_color is not None: + tri_indices.append((base, base + 1, base + 2)) + tri_indices.append((base, base + 2, base + 3)) + if outline_color is not None: + line_indices.append((base, base + 1)) + line_indices.append((base + 1, base + 2)) + line_indices.append((base + 2, base + 3)) + line_indices.append((base + 3, base)) + + if not cls.validate_shader_batch_data(verts, None): + return + + gpu.state.blend_set("ALPHA") + try: + if fill_color is not None and tri_indices: + shader = gpu.shader.from_builtin("UNIFORM_COLOR") + shader.bind() + shader.uniform_float("color", fill_color) + batch = batch_for_shader( + shader, "TRIS", {"pos": verts}, indices=tri_indices + ) + batch.draw(shader) + if outline_color is not None and line_indices: + shader = gpu.shader.from_builtin("UNIFORM_COLOR") + shader.bind() + shader.uniform_float("color", outline_color) + # Outline width: the UNIFORM_COLOR shader respects the + # GPU's current line-width state; restore on exit. + prev_width = gpu.state.line_width_get() + gpu.state.line_width_set(outline_width) + try: + batch = batch_for_shader( + shader, "LINES", {"pos": verts}, indices=line_indices + ) + batch.draw(shader) + finally: + gpu.state.line_width_set(prev_width) + finally: + gpu.state.blend_set("NONE") + @classmethod def build_dashed_line_segments( cls,