mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
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:
committed by
Thomas Krijnen
parent
782f25bd31
commit
fbfbe93550
@@ -29,18 +29,6 @@ from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||
|
||||
from . import handler, operator, parametric_lifecycle, prop, ui
|
||||
|
||||
|
||||
def _parametric_gizmo_preference_classes() -> list[type]:
|
||||
"""Resolves the registry-driven ``GizmoPreferences<X>`` classes for the
|
||||
``classes`` list below. ``import bonsai.tool`` is kept local to surface
|
||||
the load-order constraint: it relies on ``from . import handler, …``
|
||||
above having primed the
|
||||
``tool/ifc.py → bim/ifc.py → bim/handler.py → bonsai.tool`` cycle."""
|
||||
import bonsai.tool as tool
|
||||
|
||||
return tool.Parametric.iter_gizmo_preference_classes(ui)
|
||||
|
||||
|
||||
try:
|
||||
from bonsai.translations import translations_dict
|
||||
except ImportError:
|
||||
@@ -171,10 +159,6 @@ classes = [
|
||||
ui.BIM_UL_tab_visibilities,
|
||||
ui.BIM_UL_panel_visibilities,
|
||||
ui.DocPreferences,
|
||||
# Per-parametric-type ``GizmoPreferences<Name>`` classes — must register
|
||||
# before ``ui.GizmoPreferences`` which holds the matching PointerProperty
|
||||
# fields. Driven by ``tool.Parametric.EDIT_TYPES``.
|
||||
*_parametric_gizmo_preference_classes(),
|
||||
ui.GizmoPreferences,
|
||||
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
|
||||
# Tabs panel
|
||||
|
||||
@@ -143,6 +143,16 @@ DOOR_SWING_ANGLE_MAX = 90.0
|
||||
# Default scale factor for billboarded icons (Blender-unit visual size).
|
||||
DEFAULT_BILLBOARD_SCALE = 0.5
|
||||
|
||||
# Shared gizmo color constants. Re-exported as class attributes on
|
||||
# BaseParametricGizmoGroup so callers can use either ``self.COLOR_GREEN``
|
||||
# from inside a gizmo group or the module-level constant from a class body
|
||||
# (e.g. IconSlot declarations) without a forward-reference issue. Match
|
||||
# Blender's axis convention: X=red, Y=green, Z=blue.
|
||||
COLOR_RED = (1.0, 0.2, 0.2)
|
||||
COLOR_GREEN = (0.1, 0.8, 0.1)
|
||||
COLOR_BLUE = (0.3, 0.3, 1.0)
|
||||
COLOR_NEUTRAL = (1.0, 1.0, 1.0)
|
||||
|
||||
PRECISION_MODE_MULTIPLIER = 0.1
|
||||
|
||||
RAY_CAST_DISTANCE = 1000
|
||||
@@ -4971,9 +4981,6 @@ class IconSlot:
|
||||
- ``color`` — RGB tuple. ``None`` falls back to the gizmo group's default
|
||||
decoration color. Use the group's ``COLOR_RED`` / ``COLOR_GREEN`` /
|
||||
``COLOR_BLUE`` literals for state-coded icons.
|
||||
- ``visibility_pref`` — attribute name read off ``get_gizmo_prefs()``.
|
||||
Slot is hidden when that pref is falsy. ``None`` = always visible
|
||||
during edit.
|
||||
- ``extra_gap_before`` — extra spacing past the default uniform gap, in
|
||||
meters. Use sparingly — e.g. to visually separate a destructive
|
||||
action (trash) from the routine edit controls.
|
||||
@@ -4991,7 +4998,6 @@ class IconSlot:
|
||||
scale: float = DEFAULT_BILLBOARD_SCALE
|
||||
color: tuple[float, float, float] | None = None
|
||||
variants: tuple[str, ...] = ()
|
||||
visibility_pref: str | None = None
|
||||
extra_gap_before: float = 0.0
|
||||
operator_props: tuple[tuple[str, Any], ...] = ()
|
||||
|
||||
@@ -5109,11 +5115,13 @@ class BaseParametricGizmoGroup:
|
||||
"""
|
||||
|
||||
# === Gizmo Colors ===
|
||||
# Match Blender axis convention: X=red, Y=green, Z=blue
|
||||
COLOR_RED = (1.0, 0.2, 0.2)
|
||||
COLOR_GREEN = (0.1, 0.8, 0.1)
|
||||
COLOR_BLUE = (0.3, 0.3, 1.0)
|
||||
COLOR_NEUTRAL = (1.0, 1.0, 1.0)
|
||||
# Aliased to the module-level constants so subclass class bodies can
|
||||
# reference either spelling. Match Blender's axis convention:
|
||||
# X=red, Y=green, Z=blue.
|
||||
COLOR_RED = COLOR_RED
|
||||
COLOR_GREEN = COLOR_GREEN
|
||||
COLOR_BLUE = COLOR_BLUE
|
||||
COLOR_NEUTRAL = COLOR_NEUTRAL
|
||||
|
||||
# === Dimension Gizmo Layout (meters) ===
|
||||
ARROW_SCALE = 0.25 # Scale factor for arrow gizmos
|
||||
@@ -5271,27 +5279,13 @@ class BaseParametricGizmoGroup:
|
||||
from_neg_y, from_neg_x = self.get_local_view_direction(context, world_matrix)
|
||||
return ViewDirection(from_negative_y=from_neg_y, from_negative_x=from_neg_x)
|
||||
|
||||
def update_gizmo_visibility(self, gizmo: bpy.types.Gizmo, is_editing: bool, pref_enabled: bool) -> bool:
|
||||
"""Update gizmo visibility based on modal state, editing state, and preference.
|
||||
|
||||
Consolidates the common pattern:
|
||||
if hidden_by_modal:
|
||||
gizmo.hide = True
|
||||
else:
|
||||
gizmo.hide = not is_editing or not pref_enabled
|
||||
|
||||
Args:
|
||||
gizmo: The gizmo to update visibility for
|
||||
is_editing: Whether the element is currently being edited
|
||||
pref_enabled: Whether this gizmo type is enabled in preferences
|
||||
|
||||
Returns:
|
||||
True if the gizmo is now visible (not hidden), False otherwise
|
||||
"""
|
||||
def update_gizmo_visibility(self, gizmo: bpy.types.Gizmo, is_editing: bool) -> bool:
|
||||
"""Hide ``gizmo`` when not editing or when a modal owns the viewport.
|
||||
Returns True if the gizmo is now visible."""
|
||||
if self.is_gizmo_hidden_by_modal(gizmo):
|
||||
gizmo.hide = True
|
||||
return False
|
||||
gizmo.hide = not is_editing or not pref_enabled
|
||||
gizmo.hide = not is_editing
|
||||
return not gizmo.hide
|
||||
|
||||
def get_y_position_for_view(
|
||||
@@ -5533,8 +5527,7 @@ class BaseParametricGizmoGroup:
|
||||
return False
|
||||
if cls.gizmo_pref_name:
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
feature_prefs = getattr(prefs.gizmos, cls.gizmo_pref_name, None)
|
||||
if feature_prefs is not None and not getattr(feature_prefs, "enabled", True):
|
||||
if not getattr(prefs.gizmos, cls.gizmo_pref_name, True):
|
||||
return False
|
||||
if len(tool.Blender.get_selected_objects()) != 1:
|
||||
return False
|
||||
@@ -5623,8 +5616,9 @@ class BaseParametricGizmoGroup:
|
||||
"""
|
||||
pass
|
||||
|
||||
# Subclass should define these class attributes for metadata-driven dispatch
|
||||
# If not defined, subclass must override get_props() and get_gizmo_prefs()
|
||||
# Subclass should define these class attributes for metadata-driven dispatch.
|
||||
# ``gizmo_pref_name`` matches a flat BoolProperty field on
|
||||
# ``GizmoPreferences`` and gates the whole gizmo group's poll.
|
||||
props_getter: Callable[[bpy.types.Object], bpy.types.PropertyGroup] | None = None
|
||||
gizmo_pref_name: str | None = None # e.g., "door"
|
||||
|
||||
@@ -5659,18 +5653,6 @@ class BaseParametricGizmoGroup:
|
||||
prefs = self.get_addon_prefs()
|
||||
return prefs.decorations_colour[:3], prefs.decorator_color_selected[:3]
|
||||
|
||||
def get_gizmo_prefs(self) -> Any:
|
||||
"""Get gizmo preferences for this element type.
|
||||
|
||||
Subclass can either:
|
||||
1. Define class attribute `gizmo_pref_name` (e.g., "door")
|
||||
2. Override this method directly
|
||||
"""
|
||||
if self.gizmo_pref_name:
|
||||
prefs = self.get_addon_prefs()
|
||||
return getattr(prefs.gizmos, self.gizmo_pref_name)
|
||||
raise NotImplementedError("Subclass must define gizmo_pref_name or override get_gizmo_prefs()")
|
||||
|
||||
def is_setup_complete(self) -> bool:
|
||||
"""Check if gizmo setup has been completed.
|
||||
|
||||
@@ -6168,23 +6150,13 @@ class BaseParametricGizmoGroup:
|
||||
scale=0.30,
|
||||
)
|
||||
# Feature slots: per-class IconSlot tuples driven by tuple order.
|
||||
# Hidden slots STILL CONSUME their X position — toggling a pref
|
||||
# mustn't reflow the row (otherwise the array button drifts left
|
||||
# whenever a user disables the rotate icon).
|
||||
# Whole-feature visibility is gated upstream by ``poll()`` against
|
||||
# ``prefs.gizmos.<feature>``; positioning happens unconditionally
|
||||
# whenever the gizmo group polls visible.
|
||||
slot_positions = self._slot_x_positions()
|
||||
# Only fetch prefs if some slot has a visibility_pref — groups
|
||||
# without a prefs entry (e.g. array) would raise on the lookup.
|
||||
needs_prefs = any(slot.visibility_pref for slot in self.feature_slots)
|
||||
gizmo_prefs = self.get_gizmo_prefs() if needs_prefs else None
|
||||
for slot in self.feature_slots:
|
||||
slot_x = self.ICON_VALIDATE_X + slot_positions[slot.name]
|
||||
attrs = slot.gizmo_attrs()
|
||||
if slot.visibility_pref and not getattr(gizmo_prefs, slot.visibility_pref, True):
|
||||
for attr in attrs:
|
||||
gz = getattr(self, attr, None)
|
||||
if gz is not None:
|
||||
gz.hide = True
|
||||
continue
|
||||
if slot.variants:
|
||||
# Multi-variant slot: write matrix on every variant member
|
||||
# at the same anchor so a state flip never reveals a stale
|
||||
|
||||
@@ -28,7 +28,12 @@ from mathutils import Matrix, Vector
|
||||
|
||||
import bonsai.bim.module.drawing.gizmos as gizmo
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig, IconSlot
|
||||
from bonsai.bim.module.drawing.gizmos import (
|
||||
COLOR_GREEN,
|
||||
COLOR_RED,
|
||||
DimensionGizmoConfig,
|
||||
IconSlot,
|
||||
)
|
||||
from bonsai.bim.parametric_lifecycle import ParametricEditMixinBase
|
||||
|
||||
|
||||
@@ -1150,7 +1155,7 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
gizmo_idname="VIEW3D_GT_minus",
|
||||
operator="bim.adjust_array_count",
|
||||
scale=ICON_HELPER_SCALE,
|
||||
color=(1.0, 0.2, 0.2),
|
||||
color=COLOR_RED,
|
||||
operator_props=(("increment", -1),),
|
||||
),
|
||||
IconSlot(
|
||||
@@ -1158,7 +1163,7 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
gizmo_idname="VIEW3D_GT_plus",
|
||||
operator="bim.adjust_array_count",
|
||||
scale=ICON_HELPER_SCALE,
|
||||
color=(0.1, 0.8, 0.1),
|
||||
color=COLOR_GREEN,
|
||||
operator_props=(("increment", 1),),
|
||||
),
|
||||
IconSlot(
|
||||
@@ -1172,7 +1177,7 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
gizmo_idname="VIEW3D_GT_trash",
|
||||
operator="bim.remove_array_layer_from_edit",
|
||||
scale=ICON_HELPER_SCALE,
|
||||
color=(1.0, 0.2, 0.2),
|
||||
color=COLOR_RED,
|
||||
# Extra gap so the destructive action stays visually separated
|
||||
# from the routine edit controls — matches the prior 0.57 gap.
|
||||
extra_gap_before=0.20,
|
||||
|
||||
@@ -893,15 +893,8 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
|
||||
def update_swing_gizmos(self, mw: Matrix, props: "BIMDoorProperties") -> None:
|
||||
"""Update swing gizmo position and color based on editing state."""
|
||||
prefs = self.get_addon_prefs()
|
||||
door_gizmo_prefs = prefs.gizmos.door
|
||||
|
||||
door_type_visible = self.update_gizmo_visibility(
|
||||
self.gizmo_door_type, props.is_editing, door_gizmo_prefs.swing_arc
|
||||
)
|
||||
flip_arc_visible = self.update_gizmo_visibility(
|
||||
self.gizmo_flip_arc, props.is_editing, door_gizmo_prefs.flip_arc
|
||||
)
|
||||
door_type_visible = self.update_gizmo_visibility(self.gizmo_door_type, props.is_editing)
|
||||
flip_arc_visible = self.update_gizmo_visibility(self.gizmo_flip_arc, props.is_editing)
|
||||
|
||||
if not door_type_visible and not flip_arc_visible:
|
||||
return
|
||||
|
||||
@@ -31,7 +31,12 @@ from mathutils import Matrix, Vector
|
||||
import bonsai.core.root
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim.module.drawing import gizmos as gizmo
|
||||
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig, IconSlot
|
||||
from bonsai.bim.module.drawing.gizmos import (
|
||||
COLOR_GREEN,
|
||||
COLOR_RED,
|
||||
DimensionGizmoConfig,
|
||||
IconSlot,
|
||||
)
|
||||
from bonsai.tool.numeric_input import (
|
||||
IntegerInputState,
|
||||
run_integer_input_modal,
|
||||
@@ -484,7 +489,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
gizmo_idname="VIEW3D_GT_plus",
|
||||
operator="bim.adjust_stair_treads",
|
||||
scale=ICON_PLUS_MINUS_SCALE,
|
||||
color=(0.1, 0.8, 0.1),
|
||||
color=COLOR_GREEN,
|
||||
operator_props=(("increment", 1),),
|
||||
),
|
||||
IconSlot(
|
||||
@@ -492,7 +497,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
gizmo_idname="VIEW3D_GT_minus",
|
||||
operator="bim.adjust_stair_treads",
|
||||
scale=ICON_PLUS_MINUS_SCALE,
|
||||
color=(1.0, 0.2, 0.2),
|
||||
color=COLOR_RED,
|
||||
operator_props=(("increment", -1),),
|
||||
),
|
||||
)
|
||||
@@ -639,9 +644,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
the dimension-positioning hook."""
|
||||
if not hasattr(self, "total_length_lock_open_gizmo"):
|
||||
return
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
visible = props.is_editing and gizmo_prefs.lock
|
||||
if not visible:
|
||||
if not props.is_editing:
|
||||
self.total_length_lock_open_gizmo.hide = True
|
||||
self.total_length_lock_closed_gizmo.hide = True
|
||||
return
|
||||
@@ -656,11 +659,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
flip can't reveal both at once."""
|
||||
if not hasattr(self, "tread_lock_open_gizmo"):
|
||||
return
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
visible = props.is_editing and gizmo_prefs.lock
|
||||
# When the pref or edit state hides the slot, hide both members; the
|
||||
# base loop already wrote their matrices so re-showing later is safe.
|
||||
if not visible:
|
||||
if not props.is_editing:
|
||||
self.tread_lock_open_gizmo.hide = True
|
||||
self.tread_lock_closed_gizmo.hide = True
|
||||
return
|
||||
@@ -671,12 +670,9 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
"""Update visibility of +/- tread count gizmos. Positioning is handled in _update_editing_icon_positions."""
|
||||
if not hasattr(self, "plus_gizmo") or not hasattr(self, "minus_gizmo"):
|
||||
return
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.update_gizmo_visibility(self.plus_gizmo, props.is_editing, gizmo_prefs.plus)
|
||||
self.update_gizmo_visibility(self.plus_gizmo, props.is_editing)
|
||||
# Minus has additional condition: number_of_treads > 1
|
||||
self.update_gizmo_visibility(
|
||||
self.minus_gizmo, props.is_editing and props.number_of_treads > 1, gizmo_prefs.minus
|
||||
)
|
||||
self.update_gizmo_visibility(self.minus_gizmo, props.is_editing and props.number_of_treads > 1)
|
||||
|
||||
def _update_dimension_gizmo_positions(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
|
||||
|
||||
@@ -117,6 +117,7 @@ def regenerate_wall_mesh_from_props(obj: bpy.types.Object) -> None:
|
||||
bm.faces.new([verts[1], verts[5], verts[6], verts[2]])
|
||||
|
||||
assert isinstance(obj.data, bpy.types.Mesh)
|
||||
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
|
||||
bm.to_mesh(obj.data)
|
||||
bm.free()
|
||||
obj.data.update()
|
||||
@@ -1999,14 +2000,12 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
gizmo_idname="VIEW3D_GT_offset",
|
||||
variants=("exterior", "center", "interior"),
|
||||
operator="bim.cycle_wall_offset",
|
||||
visibility_pref="cycle",
|
||||
),
|
||||
IconSlot(
|
||||
name="rotate",
|
||||
gizmo_idname="VIEW3D_GT_cycle",
|
||||
operator="bim.rotate_wall_90",
|
||||
scale=0.30,
|
||||
visibility_pref="rotate",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2077,7 +2076,6 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
collide with split, so extend-Z gets bumped further to clear it."""
|
||||
if not hasattr(self, "split_gizmo"):
|
||||
return
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
all_gizmos = (self.extend_x_gizmo, self.extend_z_gizmo, self.split_gizmo)
|
||||
if not props.is_editing:
|
||||
for gz in all_gizmos:
|
||||
@@ -2090,13 +2088,12 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
|
||||
# Candidates ordered by priority (lowest first). Each is (gizmo, local_z).
|
||||
# The local X and Y are common: at the cursor's projected X on the axis.
|
||||
# Only "active" gizmos (enabled + applicable) participate in placement.
|
||||
candidates: list[tuple[bpy.types.Gizmo, float]] = []
|
||||
if gizmo_prefs.extend:
|
||||
candidates.append((self.extend_x_gizmo, 0.0))
|
||||
if gizmo_prefs.extend_height:
|
||||
candidates.append((self.extend_z_gizmo, cursor_local.z))
|
||||
if in_range and gizmo_prefs.scissors:
|
||||
# Split only joins when the cursor sits inside the wall's length range.
|
||||
candidates: list[tuple[bpy.types.Gizmo, float]] = [
|
||||
(self.extend_x_gizmo, 0.0),
|
||||
(self.extend_z_gizmo, cursor_local.z),
|
||||
]
|
||||
if in_range:
|
||||
candidates.append((self.split_gizmo, props.height))
|
||||
|
||||
# Resolve collisions: walk in priority order and ensure each gizmo's
|
||||
@@ -2144,7 +2141,6 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
manually here."""
|
||||
if not hasattr(self, "toggle_openings_gizmo"):
|
||||
return
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
icon_z = self.get_element_height(props) + self.ICON_Z_OFFSET
|
||||
icon_y = self.get_icon_y_offset(context, mw)
|
||||
billboard_rot = self._frame_billboard_rot
|
||||
@@ -2155,7 +2151,7 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
gz = getattr(self, f"baseline_{variant}_gizmo", None)
|
||||
if gz is None:
|
||||
continue
|
||||
if props.is_editing and gizmo_prefs.cycle and variant == active_variant:
|
||||
if props.is_editing and variant == active_variant:
|
||||
gz.hide = self.is_gizmo_hidden_by_modal(gz)
|
||||
else:
|
||||
gz.hide = True
|
||||
@@ -2163,7 +2159,7 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
# --- Idle-row toggle-openings (outside the slot system) ---
|
||||
# Sits at the slot the cancel icon occupies during editing — that way the
|
||||
# pen + openings pair is compact and visually grouped.
|
||||
if not props.is_editing and gizmo_prefs.toggle_openings:
|
||||
if not props.is_editing:
|
||||
self.toggle_openings_gizmo.hide = self.is_gizmo_hidden_by_modal(self.toggle_openings_gizmo)
|
||||
world_pos = mw @ Vector((self.ICON_VALIDATE_X + self.ICON_CANCEL_X, icon_y, icon_z))
|
||||
self.toggle_openings_gizmo.matrix_basis = gizmo.billboarded_at(world_pos, billboard_rot)
|
||||
|
||||
+22
-239
@@ -275,202 +275,33 @@ class BIM_UL_panel_visibilities(bpy.types.UIList):
|
||||
row.prop(item, "is_bookmarked", text="", icon="SOLO_ON" if item.is_bookmarked else "SOLO_OFF", emboss=False)
|
||||
|
||||
|
||||
class GizmoPreferencesDoor(bpy.types.PropertyGroup):
|
||||
"""Property group for door gizmo visibility settings."""
|
||||
|
||||
overall_height: BoolProperty(name="Overall Height", default=True)
|
||||
overall_width: BoolProperty(name="Overall Width", default=True)
|
||||
threshold_thickness: BoolProperty(name="Threshold Thickness", default=True)
|
||||
threshold_depth: BoolProperty(name="Threshold Depth", default=True)
|
||||
threshold_offset: BoolProperty(name="Threshold Offset", default=True)
|
||||
lining_offset: BoolProperty(name="Lining Offset", default=True)
|
||||
lining_depth: BoolProperty(name="Lining Depth", default=True)
|
||||
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
|
||||
transom_offset: BoolProperty(name="Transom Offset", default=True)
|
||||
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
|
||||
casing_thickness: BoolProperty(name="Casing Thickness", default=True)
|
||||
casing_depth: BoolProperty(name="Casing Depth", default=True)
|
||||
swing_arc: BoolProperty(name="Swing Arc", default=True, description="Show door swing direction arc")
|
||||
flip_arc: BoolProperty(name="Flip Arc", default=True, description="Show flip door orientation arc")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
overall_height: bool
|
||||
overall_width: bool
|
||||
threshold_thickness: bool
|
||||
threshold_depth: bool
|
||||
threshold_offset: bool
|
||||
lining_offset: bool
|
||||
lining_depth: bool
|
||||
lining_thickness: bool
|
||||
transom_offset: bool
|
||||
transom_thickness: bool
|
||||
casing_thickness: bool
|
||||
casing_depth: bool
|
||||
swing_arc: bool
|
||||
flip_arc: bool
|
||||
|
||||
|
||||
class GizmoPreferencesWindow(bpy.types.PropertyGroup):
|
||||
"""Property group for window gizmo visibility settings."""
|
||||
|
||||
overall_height: BoolProperty(name="Overall Height", default=True)
|
||||
overall_width: BoolProperty(name="Overall Width", default=True)
|
||||
lining_offset: BoolProperty(name="Lining Offset", default=True)
|
||||
lining_depth: BoolProperty(name="Lining Depth", default=True)
|
||||
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
|
||||
lining_to_panel_offset_x: BoolProperty(name="Lining to Panel Offset X", default=True)
|
||||
lining_to_panel_offset_y: BoolProperty(name="Lining to Panel Offset Y", default=True)
|
||||
frame_depth: BoolProperty(name="Frame Depth", default=True)
|
||||
frame_thickness: BoolProperty(name="Frame Thickness", default=True)
|
||||
mullion_thickness: BoolProperty(name="Mullion Thickness", default=True)
|
||||
first_mullion_offset: BoolProperty(name="First Mullion Offset", default=True)
|
||||
second_mullion_offset: BoolProperty(name="Second Mullion Offset", default=True)
|
||||
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
|
||||
first_transom_offset: BoolProperty(name="First Transom Offset", default=True)
|
||||
second_transom_offset: BoolProperty(name="Second Transom Offset", default=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
overall_height: bool
|
||||
overall_width: bool
|
||||
lining_offset: bool
|
||||
lining_depth: bool
|
||||
lining_thickness: bool
|
||||
lining_to_panel_offset_x: bool
|
||||
lining_to_panel_offset_y: bool
|
||||
frame_depth: bool
|
||||
frame_thickness: bool
|
||||
mullion_thickness: bool
|
||||
first_mullion_offset: bool
|
||||
second_mullion_offset: bool
|
||||
transom_thickness: bool
|
||||
first_transom_offset: bool
|
||||
second_transom_offset: bool
|
||||
|
||||
|
||||
class GizmoPreferencesStair(bpy.types.PropertyGroup):
|
||||
"""Property group for stair gizmo visibility settings."""
|
||||
|
||||
width: BoolProperty(name="Width", default=True)
|
||||
height: BoolProperty(name="Height", default=True)
|
||||
tread_run: BoolProperty(name="Tread Run", default=True)
|
||||
tread_depth: BoolProperty(name="Tread Depth", default=True)
|
||||
riser_height: BoolProperty(name="Riser Height", default=True)
|
||||
nosing_length: BoolProperty(name="Nosing Length", default=True)
|
||||
nosing_depth: BoolProperty(name="Nosing Depth", default=True)
|
||||
total_length_target: BoolProperty(name="Total Length Target", default=True)
|
||||
base_slab_depth: BoolProperty(name="Base Slab Depth", default=True)
|
||||
top_slab_depth: BoolProperty(name="Top Slab Depth", default=True)
|
||||
lock: BoolProperty(name="Total Length Lock", default=True)
|
||||
plus: BoolProperty(name="Add Tread (+)", default=True)
|
||||
minus: BoolProperty(name="Remove Tread (-)", default=True)
|
||||
cycle: BoolProperty(name="Cycle Stair Type", default=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
width: bool
|
||||
height: bool
|
||||
tread_run: bool
|
||||
tread_depth: bool
|
||||
riser_height: bool
|
||||
nosing_length: bool
|
||||
nosing_depth: bool
|
||||
total_length_target: bool
|
||||
base_slab_depth: bool
|
||||
top_slab_depth: bool
|
||||
lock: bool
|
||||
plus: bool
|
||||
minus: bool
|
||||
cycle: bool
|
||||
|
||||
|
||||
class GizmoPreferencesWall(bpy.types.PropertyGroup):
|
||||
"""Property group for wall gizmo visibility settings."""
|
||||
|
||||
length: BoolProperty(
|
||||
name="Length",
|
||||
default=True,
|
||||
description="Show the length dimension gizmo along the wall axis.",
|
||||
)
|
||||
height: BoolProperty(
|
||||
name="Height",
|
||||
default=True,
|
||||
description="Show the height dimension gizmo at the wall's start endpoint.",
|
||||
)
|
||||
height_end: BoolProperty(
|
||||
name="Height (far end, walls > 5m)",
|
||||
default=True,
|
||||
description=(
|
||||
"Show a second height gizmo at the wall's far end so long walls don't "
|
||||
"require panning to reach the handle."
|
||||
),
|
||||
)
|
||||
x_angle: BoolProperty(
|
||||
name="Slope",
|
||||
default=True,
|
||||
description="Show the slope gizmo at the wall top measuring horizontal displacement of the top face.",
|
||||
)
|
||||
cycle: BoolProperty(
|
||||
name="Cycle Offset Baseline",
|
||||
default=True,
|
||||
description="Show the baseline-state icon (Exterior / Centreline / Interior) in the editing icon row.",
|
||||
)
|
||||
scissors: BoolProperty(
|
||||
name="Split at cursor",
|
||||
default=True,
|
||||
description="Show the split icon at the 3D cursor when it lies within the wall's length range.",
|
||||
)
|
||||
extend: BoolProperty(
|
||||
name="Extend length to cursor X",
|
||||
default=True,
|
||||
description="Show the extend-length icon at the 3D cursor's projected wall-axis X.",
|
||||
)
|
||||
extend_height: BoolProperty(
|
||||
name="Extend height to cursor Z",
|
||||
default=True,
|
||||
description="Show the extend-height icon at the 3D cursor's Z, on the wall axis.",
|
||||
)
|
||||
rotate: BoolProperty(
|
||||
name="Rotate 90°",
|
||||
default=True,
|
||||
description="Show the rotate-90 icon in the editing icon row (rotates the wall around its Z axis).",
|
||||
)
|
||||
toggle_openings: BoolProperty(
|
||||
name="Toggle Openings",
|
||||
default=True,
|
||||
description="Show the toggle-openings icon next to the pen (toggles opening fill visibility in the viewport).",
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
length: bool
|
||||
height: bool
|
||||
height_end: bool
|
||||
x_angle: bool
|
||||
cycle: bool
|
||||
scissors: bool
|
||||
extend: bool
|
||||
extend_height: bool
|
||||
rotate: bool
|
||||
toggle_openings: bool
|
||||
|
||||
|
||||
class GizmoPreferences(bpy.types.PropertyGroup):
|
||||
"""Property group for all gizmo visibility settings."""
|
||||
"""Aggregator for parametric gizmo visibility settings. One flat bool per
|
||||
parametric feature; controls whether that feature's gizmo group polls
|
||||
visible in the viewport."""
|
||||
|
||||
draw_gizmos_in_3d_viewport: BoolProperty(
|
||||
name="Draw Gizmos In 3D Viewport",
|
||||
default=True,
|
||||
description="Show interactive gizmos in the 3D viewport for parametric elements",
|
||||
)
|
||||
door: bpy.props.PointerProperty(type=GizmoPreferencesDoor)
|
||||
window: bpy.props.PointerProperty(type=GizmoPreferencesWindow)
|
||||
stair: bpy.props.PointerProperty(type=GizmoPreferencesStair)
|
||||
wall: bpy.props.PointerProperty(type=GizmoPreferencesWall)
|
||||
door: BoolProperty(name="Door", default=True)
|
||||
window: BoolProperty(name="Window", default=True)
|
||||
stair: BoolProperty(name="Stair", default=True)
|
||||
railing: BoolProperty(name="Railing", default=True)
|
||||
roof: BoolProperty(name="Roof", default=True)
|
||||
array: BoolProperty(name="Array", default=True)
|
||||
wall: BoolProperty(name="Wall", default=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
draw_gizmos_in_3d_viewport: bool
|
||||
door: GizmoPreferencesDoor
|
||||
window: GizmoPreferencesWindow
|
||||
stair: GizmoPreferencesStair
|
||||
wall: GizmoPreferencesWall
|
||||
door: bool
|
||||
window: bool
|
||||
stair: bool
|
||||
railing: bool
|
||||
roof: bool
|
||||
array: bool
|
||||
wall: bool
|
||||
|
||||
|
||||
class DocPreferences(bpy.types.PropertyGroup):
|
||||
@@ -913,61 +744,13 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
)
|
||||
|
||||
def draw_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
"""Render one enabled-toggle per parametric feature."""
|
||||
layout.label(text="Toggle visibility of gizmos in editing mode")
|
||||
box = layout.box()
|
||||
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Door", self.draw_door_gizmo_parameters)
|
||||
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Window", self.draw_window_gizmo_parameters)
|
||||
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Stair", self.draw_stair_gizmo_parameters)
|
||||
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Wall", self.draw_wall_gizmo_parameters)
|
||||
|
||||
def _draw_parametric_gizmo_parameters(
|
||||
self,
|
||||
layout: bpy.types.UILayout,
|
||||
gizmo_pg: bpy.types.PropertyGroup,
|
||||
dimension_gizmo_class: type,
|
||||
special_gizmo_names: frozenset[str] = frozenset(),
|
||||
) -> None:
|
||||
"""Draw the per-element gizmo visibility toggles. Surfaces every annotation
|
||||
on ``gizmo_pg`` that either maps to one of ``dimension_gizmo_class``'s
|
||||
dimension gizmos or is named in ``special_gizmo_names`` (non-dimension icons
|
||||
like baseline cycle, scissors, rotate, …)."""
|
||||
visible_names = {p.attr_name for p in dimension_gizmo_class.dimension_gizmo_props} | special_gizmo_names
|
||||
try:
|
||||
annotations = gizmo_pg.__annotations__
|
||||
except AttributeError:
|
||||
annotations = type(gizmo_pg).__annotations__
|
||||
for prop in annotations:
|
||||
if prop in visible_names:
|
||||
layout.prop(gizmo_pg, prop)
|
||||
|
||||
def draw_door_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
from bonsai.bim.module.model.door import GizmoDoorEdition
|
||||
|
||||
self._draw_parametric_gizmo_parameters(
|
||||
layout, self.gizmos.door, GizmoDoorEdition, frozenset({"swing_arc", "flip_arc"})
|
||||
)
|
||||
|
||||
def draw_window_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
from bonsai.bim.module.model.window import GizmoWindowEdition
|
||||
|
||||
self._draw_parametric_gizmo_parameters(layout, self.gizmos.window, GizmoWindowEdition)
|
||||
|
||||
def draw_stair_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
from bonsai.bim.module.model.stair import GizmoStairEdition
|
||||
|
||||
self._draw_parametric_gizmo_parameters(
|
||||
layout, self.gizmos.stair, GizmoStairEdition, frozenset({"lock", "plus", "minus", "cycle"})
|
||||
)
|
||||
|
||||
def draw_wall_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
from bonsai.bim.module.model.wall import GizmoWallEdition
|
||||
|
||||
self._draw_parametric_gizmo_parameters(
|
||||
layout,
|
||||
self.gizmos.wall,
|
||||
GizmoWallEdition,
|
||||
frozenset({"cycle", "scissors", "extend", "extend_height", "rotate", "toggle_openings"}),
|
||||
)
|
||||
annotations = type(self.gizmos).__annotations__
|
||||
for feature in tool.Parametric.EDIT_TYPES:
|
||||
if feature.name in annotations:
|
||||
box.prop(self.gizmos, feature.name)
|
||||
|
||||
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
|
||||
layout.prop(self, "occurrence_name_style")
|
||||
|
||||
@@ -383,29 +383,6 @@ class Parametric(bonsai.core.tool.Parametric):
|
||||
if hasattr(bpy.types.Object, feature.props_attr):
|
||||
delattr(bpy.types.Object, feature.props_attr)
|
||||
|
||||
@classmethod
|
||||
def iter_gizmo_preference_classes(cls, ui_module) -> list[type]:
|
||||
"""``GizmoPreferences<Name>`` classes that exist on ``ui_module`` for
|
||||
every registry entry, plus the shared ``GizmoPreferencesFeature`` if
|
||||
present. Order matches ``EDIT_TYPES``. Used by ``bim/__init__.py`` to
|
||||
inject the per-type ``GizmoPreferences<X>`` classes at the correct
|
||||
point — before ``ui.GizmoPreferences``, which references them via
|
||||
``PointerProperty``."""
|
||||
# FIXME(PR5): drop the per-feature loop once PR4 consolidates
|
||||
# bim/ui.py to use a single shared GizmoPreferencesFeature class
|
||||
# and rewrites GizmoPreferences accordingly. The shared-class
|
||||
# branch is the forward-compat path; the per-feature loop keeps
|
||||
# v0.8.0's bim/ui.py working until then.
|
||||
out: list[type] = []
|
||||
for feature in cls.EDIT_TYPES:
|
||||
gpref = getattr(ui_module, f"GizmoPreferences{feature.name.capitalize()}", None)
|
||||
if gpref is not None:
|
||||
out.append(gpref)
|
||||
shared = getattr(ui_module, "GizmoPreferencesFeature", None)
|
||||
if shared is not None:
|
||||
out.append(shared)
|
||||
return out
|
||||
|
||||
# --- Feature-kind predicates ------------------------------------------------
|
||||
# One predicate per registered parametric type. Each is total: accepts any
|
||||
# IFC entity (or None), returns a bool, never raises. Predicates live with
|
||||
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user