Drop per-gizmo preferences + fix dynamic-wall face normals + DRY colors

Three related cleanups in one pass:

* **Per-gizmo preferences removed.** The ``visibility_pref`` field on
  IconSlot, the ``prefs.gizmos.<feature>.<icon>`` PropertyGroups, and
  the dispatcher that surfaced them in the addon preferences UI are
  all gone. ``update_gizmo_visibility`` loses its ``pref_enabled``
  parameter — visibility is now driven purely by editing state and
  modal gating. bim/ui.py drops ~257 lines of dead PropertyGroup
  definitions; bim/__init__.py and tool/parametric.py shed their
  matching wiring; door / wall slot declarations stop referencing
  the now-nonexistent prefs.

* **Dynamic-wall face normals fixed.** ``regenerate_wall_mesh_from_props``
  in wall.py now calls ``bmesh.ops.recalc_face_normals`` before writing
  the mesh. Without it, walls regenerated from the parametric edit
  draft could ship with inward-facing normals on some faces, which
  rendered as visual holes under any backface-cull or normal-aware
  shading. ``test/bim/module/model/test_wall_preview_mesh.py`` pins
  the invariant (every face's normal points away from the wall centre).

* **Color constants DRY.** ``COLOR_RED`` / ``COLOR_GREEN`` /
  ``COLOR_BLUE`` / ``COLOR_NEUTRAL`` now live at module scope in
  gizmos.py; the BaseParametricGizmoGroup class attributes alias the
  same tuples so ``self.COLOR_GREEN`` keeps working. IconSlot
  declarations in stair.py (plus / minus) and array.py (count_minus /
  count_plus / delete) now reference the named constants instead of
  duplicating the RGB tuples inline.

Verified: headless smoke green at 1267 BIM_OT_ classes,
test_parametric_registry.py 8/8, wall lane 31/31 (includes the new
preview-mesh test). ruff + black clean on the touched files.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-01 18:32:54 +02:00
committed by Thomas Krijnen
parent 782f25bd31
commit fbfbe93550
10 changed files with 175 additions and 401 deletions
@@ -0,0 +1,79 @@
# 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.
"""Pins the outward-normals invariant of the parametric-wall draft preview mesh.
``regenerate_wall_mesh_from_props`` rebuilds ``obj.data`` as a fresh bmesh
box from ``BIMWallProperties`` every time a gizmo handle moves. The hand
authored face windings carry no guarantee of outward orientation, so the
function must normalise face windings before writing the mesh back —
otherwise the viewport renders the draft with inverted shading and
back-face culling hides faces the user expects to see."""
import types
from unittest.mock import patch
import bpy
import pytest
from mathutils import Vector
pytestmark = pytest.mark.wall
@pytest.fixture(autouse=True)
def _require_real_bpy():
if not isinstance(bpy, types.ModuleType) or hasattr(bpy, "_mock_name"):
pytest.skip("requires real Blender (bpy is mocked or absent)")
def test_regenerate_wall_mesh_from_props_outward_normals():
"""Every face of the preview box must have its normal pointing away
from the box centroid — the contract every other preview-mesh builder
in ``bim/module/model`` (door / window / roof / railing) holds."""
from bonsai.bim.module.model.wall import regenerate_wall_mesh_from_props
mesh = bpy.data.meshes.new("preview_mesh")
obj = bpy.data.objects.new("preview_wall", mesh)
fake_props = types.SimpleNamespace(
length=2.0,
height=3.0,
thickness=0.2,
offset=0.0,
x_angle=0.0,
anchor_x=0.0,
mesh_dirty=False,
)
try:
with patch("bonsai.tool.Model.get_wall_props", return_value=fake_props):
regenerate_wall_mesh_from_props(obj)
assert len(mesh.polygons) == 6, f"expected 6 faces, got {len(mesh.polygons)}"
centroid = sum((v.co for v in mesh.vertices), Vector()) / len(mesh.vertices)
for face in mesh.polygons:
outward = (face.center - centroid).normalized()
dot = face.normal.dot(outward)
assert dot > 0.5, (
f"face {face.index} normal {tuple(face.normal)} points inward "
f"(outward direction {tuple(outward)}, dot={dot:.3f})"
)
finally:
bpy.data.objects.remove(obj)
bpy.data.meshes.remove(mesh)
+14 -25
View File
@@ -22,9 +22,10 @@
The registry is the single source of truth for which parametric element types
exist. Every consumer (auto-commit on save, finish/cancel chains, the
``PointerProperty`` attachment, the ``GizmoPreferences<X>`` registration) derives
identifiers from each entry's short ``name`` token. Forget any downstream
registration and the silent-desync the framework exists to prevent will ship.
``PointerProperty`` attachment, the ``GizmoPreferences`` per-feature toggle)
derives identifiers from each entry's short ``name`` token. Forget any
downstream registration and the silent-desync the framework exists to prevent
will ship.
These tests pin the registry-to-runtime contract: for every entry the operator
``bl_idname``s resolve to registered ``bpy.ops.bim.*`` callables, the
@@ -122,19 +123,14 @@ def test_every_predicate_does_not_raise_on_non_matching_element(registry):
)
def test_gizmo_preferences_attached_when_class_exists(registry):
"""For every registry entry whose ``GizmoPreferences<Name>`` class exists in
``bonsai.bim.ui``, the matching sub-PointerProperty must be declared on
``ui.GizmoPreferences`` under the registry entry's ``name`` token.
Catches the silent-skip behaviour of the registry-driven gizmo-prefs
discovery: a typo in the class name or a dropped registration would
otherwise produce a missing sub-panel at runtime with no error.
Entries without a ``GizmoPreferences<Name>`` class are allowed — not
every parametric type ships gizmo prefs.
def test_gizmo_preferences_field_per_registry_entry(registry):
"""Every registry entry must have a matching ``<name>: BoolProperty`` field
on ``ui.GizmoPreferences`` so the addon-preferences UI auto-renders a
toggle for it and ``BaseParametricGizmoGroup.poll`` can gate the whole
gizmo group on ``prefs.gizmos.<name>``.
Checks ``__annotations__`` rather than ``hasattr`` because Blender's
PropertyGroup syntax (``field: bpy.props.PointerProperty(...)``) is an
PropertyGroup syntax (``field: bpy.props.BoolProperty(...)``) is an
annotation-only assignment — the attribute only materialises on the
class after Blender's metaclass installs the bpy_struct descriptor,
which depends on registration timing. Reading ``__annotations__``
@@ -142,16 +138,9 @@ def test_gizmo_preferences_attached_when_class_exists(registry):
from bonsai.bim import ui
annotations = getattr(ui.GizmoPreferences, "__annotations__", {})
missing = []
for feature in registry:
prefs_class_name = f"GizmoPreferences{feature.name.capitalize()}"
if not hasattr(ui, prefs_class_name):
continue
if feature.name not in annotations:
missing.append((feature.name, prefs_class_name))
missing = [feature.name for feature in registry if feature.name not in annotations]
assert not missing, (
f"ui.GizmoPreferences missing sub-PointerProperty field(s) for: {missing}"
f"each registered ``GizmoPreferences<Name>`` class must have a matching "
f"``<name>: PointerProperty(type=GizmoPreferences<Name>)`` field on "
f"``ui.GizmoPreferences``"
f"ui.GizmoPreferences missing BoolProperty field(s) for: {missing}"
f"each registry entry must have a matching ``<name>: BoolProperty(...)`` "
f"field on ``ui.GizmoPreferences`` so the preferences UI surfaces a toggle"
)