mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
Fix decorator face-tri overlay artifacts
ProfileDecorator.draw_faces (used by the roof path-edit overlay) and SystemDecorator.draw_faces called bmesh.ops.triangulate on the live bmesh — both mutated the input and produced ear-clip fans that rendered as visible streaks across n-gon roof faces at alpha 0.1. The opening DecorationsHandler edit-mode branch had a separate bug: it computed triangles from obj.data.calc_loop_triangles() while iterating the edit-mode bmesh, so any topology added mid-edit desynced the indices. Centralise the correct draw path on tool.Blender.draw_bmesh_face_tris (wraps bm.calc_loop_triangles, non-mutating, beauty triangulator) and route all three call-sites through it. A forward-compat AST guard walks every *Decorator / DecorationsHandler class under bim/module/ and pins the no-bmesh.ops.triangulate rule against future regressions. Generated with the assistance of an AI coding tool.
This commit is contained in:
committed by
Thomas Krijnen
parent
0319763376
commit
ab2fddb170
@@ -15,6 +15,8 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was modified with the assistance of an AI coding tool.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -93,15 +95,9 @@ class ProfileDecorator:
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_faces(self, bm, vertices_coords):
|
||||
"""mutates original bm (triangulates it)
|
||||
so the triangulation edges will be shown too
|
||||
"""
|
||||
traingulated_bm = bm
|
||||
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
|
||||
|
||||
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
|
||||
"""Submit a non-mutating beauty-triangulated TRIS batch over ``bm``'s faces."""
|
||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
||||
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
|
||||
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):
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was modified with the assistance of an AI coding tool.
|
||||
|
||||
from collections.abc import Sequence
|
||||
from math import radians
|
||||
@@ -1278,9 +1280,7 @@ class DecorationsHandler:
|
||||
self.draw_batch("LINES", verts, selected_elements_color, selected_edges)
|
||||
self.draw_batch("POINTS", unselected_vertices, unselected_elements_color)
|
||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||
obj.data.calc_loop_triangles()
|
||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
||||
self.draw_batch("TRIS", verts, transparent_color(special_elements_color), tris)
|
||||
tool.Blender.draw_bmesh_face_tris(bm, verts, transparent_color(special_elements_color), self.draw_batch)
|
||||
else:
|
||||
line_verts, verts, edges_indices, tris = _get_cached_world_draw_data(obj)
|
||||
color = selected_elements_color if obj in context.selected_objects else special_elements_color
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was modified with the assistance of an AI coding tool.
|
||||
|
||||
|
||||
import bmesh
|
||||
import bpy
|
||||
import gpu
|
||||
from bpy.app.handlers import persistent
|
||||
@@ -78,15 +79,9 @@ class SystemDecorator:
|
||||
batch.draw(shader)
|
||||
|
||||
def draw_faces(self, bm, vertices_coords):
|
||||
"""mutates original bm (triangulates it)
|
||||
so the triangulation edges will be shown too
|
||||
"""
|
||||
traingulated_bm = bm
|
||||
bmesh.ops.triangulate(traingulated_bm, faces=traingulated_bm.faces)
|
||||
|
||||
face_indices = [[v.index for v in f.verts] for f in traingulated_bm.faces]
|
||||
"""Submit a non-mutating beauty-triangulated TRIS batch over ``bm``'s faces."""
|
||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
||||
self.draw_batch("TRIS", vertices_coords, faces_color, face_indices)
|
||||
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):
|
||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||
|
||||
@@ -2242,6 +2242,24 @@ class Blender(bonsai.core.tool.Blender):
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def draw_bmesh_face_tris(
|
||||
cls,
|
||||
bm: bmesh.types.BMesh,
|
||||
world_vert_coords: list,
|
||||
color: Any,
|
||||
draw_batch: Callable[[str, list, Any, list], None],
|
||||
) -> None:
|
||||
"""Submit a non-mutating beauty-triangulated TRIS batch for ``bm``'s faces.
|
||||
|
||||
``world_vert_coords`` must be indexed by ``bm.verts`` index. Never call
|
||||
``bmesh.ops.triangulate`` on a live bmesh to compute draw indices — it
|
||||
mutates the input and produces ear-clip fans that render as visible
|
||||
streaks at low alpha.
|
||||
"""
|
||||
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 extract_error_reports(cls, exception: RuntimeError) -> list[str]:
|
||||
"""Extracts error report lines from a runtime exception during operator execution.
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
# This file was generated with the assistance of an AI coding tool.
|
||||
|
||||
"""Forward-compat AST contract: decorators do not triangulate in-place.
|
||||
|
||||
``bmesh.ops.triangulate(bm, faces=bm.faces)`` mutates its input — adding tri
|
||||
edges and faces — and uses ear-clip fan triangulation that renders as visible
|
||||
streaks across n-gon faces at the low alphas decorators favour. The canonical
|
||||
draw path is ``tool.Blender.draw_bmesh_face_tris`` (wraps ``bm.calc_loop_triangles``,
|
||||
non-mutating, beauty triangulator)."""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.model
|
||||
|
||||
|
||||
BONSAI_ROOT = Path(__file__).parent.parent.parent / "bonsai"
|
||||
BIM_MODULE_DIR = BONSAI_ROOT / "bim" / "module"
|
||||
|
||||
|
||||
def _iter_guarded_files():
|
||||
yield from sorted(BIM_MODULE_DIR.glob("*/decorator.py"))
|
||||
yield BIM_MODULE_DIR / "model" / "opening.py"
|
||||
|
||||
|
||||
def _is_guarded_class(node: ast.ClassDef) -> bool:
|
||||
return node.name.endswith("Decorator") or node.name == "DecorationsHandler"
|
||||
|
||||
|
||||
def _is_mutating_triangulate_call(node: ast.AST) -> bool:
|
||||
if not isinstance(node, ast.Call):
|
||||
return False
|
||||
func = node.func
|
||||
if not isinstance(func, ast.Attribute) or func.attr != "triangulate":
|
||||
return False
|
||||
receiver = func.value
|
||||
if not isinstance(receiver, ast.Attribute) or receiver.attr != "ops":
|
||||
return False
|
||||
inner = receiver.value
|
||||
return isinstance(inner, ast.Name) and inner.id == "bmesh"
|
||||
|
||||
|
||||
def test_no_decorator_calls_bmesh_ops_triangulate() -> None:
|
||||
violations: list[str] = []
|
||||
guarded_files = list(_iter_guarded_files())
|
||||
assert guarded_files, "Search root contains no decorator modules — test needs updating."
|
||||
|
||||
for path in guarded_files:
|
||||
try:
|
||||
tree = ast.parse(path.read_text(encoding="utf-8"))
|
||||
except (SyntaxError, FileNotFoundError):
|
||||
continue
|
||||
for class_node in (n for n in ast.walk(tree) if isinstance(n, ast.ClassDef)):
|
||||
if not _is_guarded_class(class_node):
|
||||
continue
|
||||
for sub in ast.walk(class_node):
|
||||
if _is_mutating_triangulate_call(sub):
|
||||
violations.append(f"{path}:{sub.lineno} {class_node.name} calls bmesh.ops.triangulate")
|
||||
|
||||
assert not violations, (
|
||||
"Decorator classes must not call bmesh.ops.triangulate — it mutates "
|
||||
"the input bmesh and produces fan-clip artefacts at low alpha. "
|
||||
"Use tool.Blender.draw_bmesh_face_tris (wraps bm.calc_loop_triangles). "
|
||||
"Violations:\n " + "\n ".join(violations)
|
||||
)
|
||||
Reference in New Issue
Block a user