mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-17 14:02:27 +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
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# 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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -93,15 +95,9 @@ class ProfileDecorator:
|
|||||||
batch.draw(shader)
|
batch.draw(shader)
|
||||||
|
|
||||||
def draw_faces(self, bm, vertices_coords):
|
def draw_faces(self, bm, vertices_coords):
|
||||||
"""mutates original bm (triangulates it)
|
"""Submit a non-mutating beauty-triangulated TRIS batch over ``bm``'s faces."""
|
||||||
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]
|
|
||||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
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):
|
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
#
|
#
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# 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 collections.abc import Sequence
|
||||||
from math import radians
|
from math import radians
|
||||||
@@ -1278,9 +1280,7 @@ class DecorationsHandler:
|
|||||||
self.draw_batch("LINES", verts, selected_elements_color, selected_edges)
|
self.draw_batch("LINES", verts, selected_elements_color, selected_edges)
|
||||||
self.draw_batch("POINTS", unselected_vertices, unselected_elements_color)
|
self.draw_batch("POINTS", unselected_vertices, unselected_elements_color)
|
||||||
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
self.draw_batch("POINTS", selected_vertices, selected_elements_color)
|
||||||
obj.data.calc_loop_triangles()
|
tool.Blender.draw_bmesh_face_tris(bm, verts, transparent_color(special_elements_color), self.draw_batch)
|
||||||
tris = [tuple(t.vertices) for t in obj.data.loop_triangles]
|
|
||||||
self.draw_batch("TRIS", verts, transparent_color(special_elements_color), tris)
|
|
||||||
else:
|
else:
|
||||||
line_verts, verts, edges_indices, tris = _get_cached_world_draw_data(obj)
|
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
|
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
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with Bonsai. If not, see <http://www.gnu.org/licenses/>.
|
# 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 bpy
|
||||||
import gpu
|
import gpu
|
||||||
from bpy.app.handlers import persistent
|
from bpy.app.handlers import persistent
|
||||||
@@ -78,15 +79,9 @@ class SystemDecorator:
|
|||||||
batch.draw(shader)
|
batch.draw(shader)
|
||||||
|
|
||||||
def draw_faces(self, bm, vertices_coords):
|
def draw_faces(self, bm, vertices_coords):
|
||||||
"""mutates original bm (triangulates it)
|
"""Submit a non-mutating beauty-triangulated TRIS batch over ``bm``'s faces."""
|
||||||
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]
|
|
||||||
faces_color = transparent_color(self.addon_prefs.decorator_color_special)
|
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):
|
def __call__(self, context, get_custom_bmesh=None, draw_faces=False, exit_edit_mode_callback=None):
|
||||||
self.addon_prefs = tool.Blender.get_addon_preferences()
|
self.addon_prefs = tool.Blender.get_addon_preferences()
|
||||||
|
|||||||
@@ -2242,6 +2242,24 @@ class Blender(bonsai.core.tool.Blender):
|
|||||||
return False
|
return False
|
||||||
return True
|
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
|
@classmethod
|
||||||
def extract_error_reports(cls, exception: RuntimeError) -> list[str]:
|
def extract_error_reports(cls, exception: RuntimeError) -> list[str]:
|
||||||
"""Extracts error report lines from a runtime exception during operator execution.
|
"""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