Replace hardcoded icon-X constants with IconSlot layout manager

The parametric edit toolbar row used to assign each feature icon its
own ICON_<NAME>_X constant, with a separate FEATURE_ICON_MAX_X override
each subclass had to bump whenever a new icon was added. Forgetting the
bump silently collided icons — wall's rotate icon and the array button
both landed at X=1.24 in edit mode.

The new IconSlot dataclass + feature_slots tuple replace the
constants-and-override pattern with order-driven positioning: the
layout manager assigns each slot an X from its tuple index plus a
uniform ICON_ARRAY_GAP. Adding an icon is now a one-line append; the
"forget to bump" failure mode is structurally impossible.

Slot capabilities cover every existing icon-row shape:
* Single icon (wall rotate, array delete).
* N-variant slots — N gizmos at the same X with one visible per frame
  via a subclass picker (stair tread-lock open/closed, wall baseline
  exterior/center/interior). Pair becomes the N=2 case; triplet the
  N=3 case. Variant idnames can be authored either as a tuple of
  explicit names or as a string prefix that auto-suffixes _<variant>.
* Visibility prefs gate slot rendering without reflowing the row —
  hidden slots still consume their X position.
* Extra per-slot gap before for visual separation (array's delete
  trails the routine controls by an extra 0.2 m).
* Operator props forwarded to target_set_operator so adjusters
  (+/-, increment) and generic toggles (property_name=...) work.

When the cycle slot is unused, feature slots collapse into the cycle
position so the row stays tight — that's how wall's baseline triplet
sits at X=0.87 without a gap before it.

Three subclasses migrate to the new system:
* wall.py — rotate icon + baseline triplet variants. Drops
  ICON_ROTATE_X, _BASELINE_GIZMO_ATTRS, the manual triplet creation
  loop, and the matching positioning block in _update_icon_row_extras
  (it now just picks variant visibility).
* stair.py — tread_lock pair (open/closed) + plus + minus.
  _update_editing_icon_positions reads slot X via _slot_x_positions
  instead of three hardcoded constants. Also fixes the standalone
  total_length_lock gizmo, which was broken since PR4 split
  VIEW3D_GT_lock into open/closed pair (caller wasn't updated).
* array.py — count_minus + count_plus + method + delete (with
  extra_gap_before=0.20 to separate the destructive action).
  Drops the manual edit-row positioning loop entirely; the base
  loop handles it. GizmoArrayChild now inherits BillboardingGizmoGroupMixin
  and uses the shared setup_icon_gizmo helper, dropping its
  duplicated _make_icon wrapper.

Two helpers added on BillboardingGizmoGroupMixin to fold the duplicated
prefs/color preamble that appeared at the top of six wall gizmo setups
plus the array-child setup:
* get_decoration_colors() — (decorations_colour, decorator_color_selected),
  the active-state pair.
* get_unselected_decoration_colors() — (decorator_color_unselected,
  decorator_color_selected) for gizmos surfaced on already-selected
  geometry that should not pull focus.

Verified: headless smoke green at 1267 BIM_OT_ classes,
test_parametric_registry.py 8/8 pass, wall lane 29/29 pass,
model lane unchanged at 135 pass + 7 pre-existing v0.8.0 failures
(no regressions). ruff + black clean.

Generated with the assistance of an AI coding tool.
This commit is contained in:
Gorgious56
2026-06-01 16:19:42 +02:00
parent 44723e83b6
commit 4cf34b69d2
4 changed files with 427 additions and 264 deletions
+205 -16
View File
@@ -4924,12 +4924,116 @@ class BillboardingGizmoGroupMixin:
"""Convenience wrapper over `setup_icon_gizmo` for subclasses."""
return setup_icon_gizmo(self, gizmo_type, color, highlight_color, operator, alpha)
def get_decoration_colors(self) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
"""Standard (default, highlight) color pair for active-state gizmos.
Pulls from the addon preferences same source consumed by every
Bonsai decorator. Hover-class gizmos that should not pull focus
should use ``get_unselected_decoration_colors`` instead."""
prefs = tool.Blender.get_addon_preferences()
return prefs.decorations_colour[:3], prefs.decorator_color_selected[:3]
def get_unselected_decoration_colors(self) -> tuple[tuple[float, float, float], tuple[float, float, float]]:
"""Lower-priority (unselected default, highlight) pair for gizmos
that surface on already-selected geometry and shouldn't compete
visually with the selection outline (e.g. array-child navigation)."""
prefs = tool.Blender.get_addon_preferences()
return prefs.decorator_color_unselected[:3], prefs.decorator_color_selected[:3]
def position_gizmos(self, context: bpy.types.Context) -> None:
raise NotImplementedError(
f"{type(self).__name__} must implement position_gizmos(context) when using BillboardingGizmoGroupMixin."
)
@dataclass(frozen=True)
class IconSlot:
"""One slot in a parametric edit gizmo's icon toolbar row.
The slot's X coordinate is COMPUTED from its index in ``feature_slots`` —
never set explicitly. Adding an icon is a one-line append; the layout
manager resolves the X. Hidden slots STILL CONSUME their X position so
toggling a visibility preference doesn't shift the row.
Fields:
- ``gizmo_idname`` for single-icon slots, the full Blender gizmo
idname. For multi-variant slots, EITHER a string PREFIX that auto-
suffixes ``_<variant>`` per member (the common case e.g.
``"VIEW3D_GT_lock"`` + variants ``("open", "closed")`` becomes
``VIEW3D_GT_lock_open`` / ``VIEW3D_GT_lock_closed``) OR a tuple of
explicit idnames matching the variant count when the variants
don't share a prefix. Attributes created on the gizmo group are
``self.<name>_gizmo`` for single slots, ``self.<name>_<variant>_gizmo``
for each variant in multi-variant slots.
- ``variants`` variant suffixes, e.g. ``("open", "closed")`` for a
lock pair, ``("exterior", "center", "interior")`` for a baseline
cycle. Empty tuple = single icon.
- ``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.
- ``operator_props`` tuple of (key, value) pairs forwarded to
``target_set_operator``'s return value (e.g. ``increment=1`` for a
+/- adjuster, ``property_name="..."`` for a generic toggle)."""
name: str
gizmo_idname: str | tuple[str, ...]
operator: str
# Matches DEFAULT_BILLBOARD_SCALE — the scale validate/cancel render at,
# so slots that don't override land at the same visual size by default.
# Helper icons (+/- count adjusters, lock pairs, delete) override with
# smaller values (0.20 - 0.35) to signal secondary affordance.
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], ...] = ()
def __post_init__(self) -> None:
# Validate shape at class-definition time so a typo doesn't surface
# as a runtime error in the gizmo group's setup() three layers deep.
if self.variants:
if isinstance(self.gizmo_idname, str):
pass # prefix form — idname auto-suffixed per variant
elif isinstance(self.gizmo_idname, tuple) and len(self.gizmo_idname) == len(self.variants):
pass # explicit-tuple form
else:
raise TypeError(
f"IconSlot({self.name!r}): variants={self.variants} requires gizmo_idname "
f"to be either a string prefix (auto-suffixed as <prefix>_<variant>) or a "
f"tuple of {len(self.variants)} explicit idnames, got {self.gizmo_idname!r}"
)
elif not isinstance(self.gizmo_idname, str):
raise TypeError(
f"IconSlot({self.name!r}): single-icon slot requires gizmo_idname str, "
f"got {self.gizmo_idname!r} (set variants=(...) if you want a multi-variant slot)"
)
def variant_idnames(self) -> tuple[str, ...]:
"""Resolve per-variant gizmo idnames. For prefix form, suffix each
variant onto the prefix; for tuple form, return as is. Single-icon
slots return a one-element tuple containing the idname."""
if not self.variants:
assert isinstance(self.gizmo_idname, str)
return (self.gizmo_idname,)
if isinstance(self.gizmo_idname, str):
return tuple(f"{self.gizmo_idname}_{variant}" for variant in self.variants)
return self.gizmo_idname
def gizmo_attrs(self) -> tuple[str, ...]:
"""Names of every ``self.*`` attribute this slot writes during setup.
Returns one for a single slot, N for an N-variant slot."""
if self.variants:
return tuple(f"{self.name}_{variant}_gizmo" for variant in self.variants)
return (f"{self.name}_gizmo",)
class BaseParametricGizmoGroup:
"""Base mixin for parametric element gizmo groups (doors, windows, stairs, etc.).
@@ -5028,17 +5132,14 @@ class BaseParametricGizmoGroup:
ICON_VALIDATE_X = 0.0 # X position of validate (checkmark) icon
ICON_CANCEL_X = 0.5 # X offset from validate for cancel (X) icon
ICON_CYCLE_X = 0.87 # X offset from validate for cycle (arrow) icon
# Rightmost local-X used by feature-specific icons (across both idle and
# edit states). Subclasses override when they add icons past the cycle
# slot at 0.87 — currently wall (rotate at 1.24) and stair (minus at
# 1.98). Drives both the ARRAY button position (this class) AND the
# array-layer-icons start position (``GizmoArrayEdition`` runtime lookup),
# so non-colliding features get a tight layout while wall / stair shift
# the array-related slots outward to avoid stomping on the rotate /
# tread-lock / +/- icons.
FEATURE_ICON_MAX_X: float = 0.87
# Gap between the last feature icon and the ARRAY button (or the first
# array layer icon in idle state).
# Subclasses append to declare feature icons in the edit-mode toolbar row.
# The layout manager assigns each slot an X position from its tuple
# index — adding a new icon is a one-line append, no hardcoded X
# constant, no "remember to bump the right edge" rule. The trailing
# ARRAY button is positioned past the last slot automatically.
feature_slots: ClassVar[tuple[IconSlot, ...]] = ()
# Gap between adjacent slots past the leading validate/cancel/cycle
# triplet, AND between the last slot and the ARRAY button.
ICON_ARRAY_GAP: float = 0.37
ICON_Z_OFFSET = 0.5 # Height above element for icons
ICON_Y_OFFSET = GIZMO_OFFSET * 2 # Y offset to keep icons clear of geometry
@@ -5060,6 +5161,37 @@ class BaseParametricGizmoGroup:
super().__init_subclass__(**kwargs)
BaseParametricGizmoGroup.REGISTRY.append(cls)
@classmethod
def _slot_x_positions(cls) -> dict[str, float]:
"""Map each ``feature_slot`` name to its X coordinate in the row.
Slots are laid out from the cycle position onward at uniform
``ICON_ARRAY_GAP`` spacing, plus any per-slot ``extra_gap_before``.
When the cycle slot is unused (no ``cycle_type_operator`` /
``pick_type_operator``), the first feature slot collapses into the
cycle position so the row stays tight that's how wall's baseline
triplet ends up at X=0.87 without a gap before it. Tuple order is
the only thing that controls X; rearranging the tuple rearranges
the row."""
positions: dict[str, float] = {}
has_cycle = bool(cls.cycle_type_operator) or bool(cls.pick_type_operator)
next_x = (cls.ICON_CYCLE_X + cls.ICON_ARRAY_GAP) if has_cycle else cls.ICON_CYCLE_X
for slot in cls.feature_slots:
next_x += slot.extra_gap_before
positions[slot.name] = next_x
next_x += cls.ICON_ARRAY_GAP
return positions
@classmethod
def _feature_row_right_edge(cls) -> float:
"""Right edge of the feature icon row, fed to the trailing ARRAY
button's X. Computed strictly from slot order + gaps; empty
``feature_slots`` collapses to the cycle position."""
positions = cls._slot_x_positions()
if not positions:
return cls.ICON_CYCLE_X
return max(positions.values())
@classmethod
def pick_visible_anchor(cls, context: bpy.types.Context, world_base: Vector, world_top: Vector) -> Vector:
"""Choose between two anchor candidates so vertical separation stays
@@ -5762,6 +5894,17 @@ class BaseParametricGizmoGroup:
"VIEW3D_GT_menu", default_color, self.pick_type_operator, highlight_color
)
# Feature-specific edit-row icons. Subclasses declare them via
# ``feature_slots``; multi-variant slots create one gizmo per
# variant at the same X (e.g. a lock pair, a baseline triplet) and
# the subclass picks which is visible per frame.
for slot in self.feature_slots:
slot_color = slot.color if slot.color is not None else default_color
kwargs = dict(slot.operator_props)
for attr, idname in zip(slot.gizmo_attrs(), slot.variant_idnames()):
gz = self.create_icon_gizmo(idname, slot_color, slot.operator, **kwargs)
setattr(self, attr, gz)
# ARRAY button — visible during the feature edit lifecycle only (positioned by
# ``update_editing_gizmos``). Click commits the current edit and adds a
# Blender-vanilla-defaulted array (count=2, X-offset = bbox extent). The
@@ -6024,10 +6167,51 @@ class BaseParametricGizmoGroup:
billboard_rot=billboard_rot,
scale=0.30,
)
# ARRAY button sits past the last feature-specific icon. Each
# gizmo group declares its own ``FEATURE_ICON_MAX_X`` (default
# 0.87 past the cycle slot; wall / stair override it) so the
# ARRAY button never lands on top of a rotate / tread-lock icon.
# 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).
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
# pose. The subclass's per-frame hook picks which member
# is visible — this loop doesn't toggle hide flags.
world_pos = mw @ Vector((slot_x, icon_y, icon_z))
matrix = billboarded_at(world_pos, billboard_rot, scale=slot.scale)
for attr in attrs:
gz = getattr(self, attr, None)
if gz is not None:
gz.matrix_basis = matrix
continue
gz = getattr(self, attrs[0], None)
if gz is None:
continue
gz.hide = self.is_gizmo_hidden_by_modal(gz)
self.set_icon_gizmo_position(
attrs[0],
mw=mw,
x=slot_x,
y=icon_y,
z=icon_z,
billboard_rot=billboard_rot,
scale=slot.scale,
)
# ARRAY button sits past the last feature-specific icon. Slot-based
# subclasses derive the right edge from the slot count.
if hasattr(self, "array_gizmo"):
self.array_gizmo.hide = self.is_gizmo_hidden_by_modal(self.array_gizmo)
# 30% smaller than the editing-icon-row default (0.50 → 0.35):
@@ -6037,7 +6221,7 @@ class BaseParametricGizmoGroup:
self.set_icon_gizmo_position(
"array_gizmo",
mw=mw,
x=self.ICON_VALIDATE_X + self.FEATURE_ICON_MAX_X + self.ICON_ARRAY_GAP,
x=self.ICON_VALIDATE_X + self._feature_row_right_edge() + self.ICON_ARRAY_GAP,
y=icon_y,
z=icon_z,
billboard_rot=billboard_rot,
@@ -6061,6 +6245,11 @@ class BaseParametricGizmoGroup:
self.cancel_gizmo.hide = True
if self.cycle_type_operator or self.pick_type_operator:
self.cycle_gizmo.hide = True
for slot in self.feature_slots:
for attr in slot.gizmo_attrs():
gz = getattr(self, attr, None)
if gz is not None:
gz.hide = True
if hasattr(self, "array_gizmo"):
self.array_gizmo.hide = True
+68 -103
View File
@@ -28,7 +28,7 @@ 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
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig, IconSlot
from bonsai.bim.parametric_lifecycle import ParametricEditMixinBase
@@ -1132,22 +1132,53 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
# collapses to a single per-feature pen.
hide_pen_button = True
# Local-X positions of the editing-row extras (count label + adjuster icons).
# Layout left-to-right at the same Y/Z as validate (ICON_VALIDATE_X = 0.0) and
# cancel (ICON_VALIDATE_X + ICON_CANCEL_X = 0.5):
# validate | cancel | xN | - | + | method-toggle | trash.
# Spacing mirrors stair's editing-row constants so the icons match in visual rhythm.
# Trash sits past the method toggle with a slightly wider gap so the destructive
# action stays visually separated from the routine edit controls.
# Editing row layout: validate | cancel | xN | - | + | method | trash.
# The count-label (xN) gizmo sits at the cycle slot (X = 0.87), positioned
# manually in ``_refresh_element_specific`` because it's a label that
# replaces the cycle icon rather than a row slot. The +/-/method/trash
# icons live in ``feature_slots`` below — the base class assigns X
# positions from tuple order; the trash carries ``extra_gap_before`` to
# visually separate the destructive action from the routine controls.
ICON_NUMBER_X = 0.87
ICON_MINUS_X = 1.24
ICON_PLUS_X = 1.61
ICON_METHOD_X = 1.98
ICON_DELETE_X = 2.55
# Render scale for the +/- and method-toggle icons — ~70% of the standard
# 0.5 used for validate/cancel. Makes the helpers look secondary.
ICON_HELPER_SCALE = 0.35
feature_slots: ClassVar[tuple[IconSlot, ...]] = (
IconSlot(
name="count_minus",
gizmo_idname="VIEW3D_GT_minus",
operator="bim.adjust_array_count",
scale=ICON_HELPER_SCALE,
color=(1.0, 0.2, 0.2),
operator_props=(("increment", -1),),
),
IconSlot(
name="count_plus",
gizmo_idname="VIEW3D_GT_plus",
operator="bim.adjust_array_count",
scale=ICON_HELPER_SCALE,
color=(0.1, 0.8, 0.1),
operator_props=(("increment", 1),),
),
IconSlot(
name="method",
gizmo_idname="VIEW3D_GT_cycle",
operator="bim.toggle_array_method",
scale=ICON_HELPER_SCALE,
),
IconSlot(
name="delete",
gizmo_idname="VIEW3D_GT_trash",
operator="bim.remove_array_layer_from_edit",
scale=ICON_HELPER_SCALE,
color=(1.0, 0.2, 0.2),
# 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,
),
)
# Per-layer ARRAY icons — one shown in idle state per existing array
# layer, surfaced to the right of the pen. Pre-allocated at setup time
# (Blender's gizmo API doesn't support creating gizmos on demand at draw
@@ -1249,36 +1280,23 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
return tool.Parametric.is_array(element)
def setup_element_specific_gizmos(self, context: bpy.types.Context) -> None:
"""Create the +/- count adjusters, the method toggle, and the
per-layer ARRAY entry icons (one per existing array layer)."""
self.count_plus_gizmo = self.create_icon_gizmo(
"VIEW3D_GT_plus", self.COLOR_GREEN, "bim.adjust_array_count", increment=1
)
self.count_minus_gizmo = self.create_icon_gizmo(
"VIEW3D_GT_minus", self.COLOR_RED, "bim.adjust_array_count", increment=-1
)
# Method toggle uses the cycle icon (circular arrow) — same affordance
# the stair / roof type-cycle gizmos use, signalling "click to swap".
"""Create the world-space count label and per-layer ARRAY entry icons.
The +/- count adjusters, method toggle, and delete button live in
``feature_slots`` and are auto-created by the base class."""
default_color, highlight_color = self.get_decoration_colors()
self.method_gizmo = self.create_icon_gizmo("VIEW3D_GT_cycle", default_color, "bim.toggle_array_method")
# World-space count display for the edit row. Click opens a numeric
# input dialog (``bim.input_array_count``) so the user can type a
# value directly instead of clicking +/- repeatedly. Renders the same
# ``xN`` glyph as the idle-state per-layer icons for visual consistency.
# Sits at the cycle slot — it's a label that replaces the cycle icon,
# not a row slot, so it's positioned manually below rather than via
# ``feature_slots``.
self.count_label_gizmo = self.gizmos.new("BIM_GT_array_layer_indicator")
self.count_label_gizmo.use_draw_scale = False
self.count_label_gizmo.color = default_color
self.count_label_gizmo.color_highlight = highlight_color
self.count_label_gizmo.alpha = 0.8
self.count_label_gizmo.target_set_operator("bim.input_array_count")
# Destructive delete button at the far right of the edit row — red
# like the minus gizmo so the user reads "destructive" before the
# tooltip even appears. The dispatcher cancels the in-progress edit
# first (clearing edit state + unhiding children) then removes the
# layer in one click.
self.delete_gizmo = self.create_icon_gizmo(
"VIEW3D_GT_trash", self.COLOR_RED, "bim.remove_array_layer_from_edit"
)
# Per-layer ARRAY icons — pre-allocated up to ``MAX_LAYER_GIZMOS`` and
# shown/hidden in ``_refresh_element_specific`` based on the actual
@@ -1354,25 +1372,21 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
return match is not None and match.name != "array"
def _refresh_element_specific(self, context: bpy.types.Context, mw: Matrix, props) -> None:
"""Position the editing-row extras and the per-layer ARRAY icons.
"""Position the count label and per-layer ARRAY icons.
Idle (``not props.is_editing``): show one ARRAY icon per existing
array layer to the right of the pen. The +/-, method, and editing-row
icons stay hidden.
array layer to the right of the pen. The +/-, method, and delete
slot icons stay hidden (handled by the base).
Active edit: show the editing row (validate, cancel, / +, method);
hide the per-layer icons so they don't clutter the edit UX."""
Active edit: show the count label; hide the per-layer icons. The
+/-, method, and delete icons are positioned by the base class
via the slot layout — no manual positioning needed here."""
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
if not props.is_editing:
# Idle: show one ARRAY icon per existing layer. Per-edit helpers off.
self.count_plus_gizmo.hide = True
self.count_minus_gizmo.hide = True
self.method_gizmo.hide = True
self.count_label_gizmo.hide = True
self.delete_gizmo.hide = True
layers = self._read_array_layers(context)
layer_count = min(len(layers), self.MAX_LAYER_GIZMOS)
# Feature-aware start X — pushes the layer icons past any
@@ -1399,34 +1413,10 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
gz.matrix_basis = gizmo.billboarded_at(world_pos, billboard_rot, scale=0.5)
return
# Active edit: hide the layer icons, show the editing row. The
# layer-hover bbox lives inside each layer gizmo's draw method, so
# hiding the gizmos is enough to stop the hover highlight too.
# Active edit: hide the layer icons, show the count label. The
# +/-, method, and delete slot icons are positioned by the base.
for gz in self.layer_gizmos:
gz.hide = True
# Same Y/Z as the validate/cancel icons set by the base
# ``update_editing_gizmos`` — they form one horizontal row. Helper
# icons (+/-, method) use ``ICON_HELPER_SCALE`` so they look secondary.
for gizmo_name, local_x in (
("count_minus_gizmo", self.ICON_VALIDATE_X + self.ICON_MINUS_X),
("count_plus_gizmo", self.ICON_VALIDATE_X + self.ICON_PLUS_X),
("method_gizmo", self.ICON_VALIDATE_X + self.ICON_METHOD_X),
("delete_gizmo", self.ICON_VALIDATE_X + self.ICON_DELETE_X),
):
gizmo_obj = getattr(self, gizmo_name)
if self.is_gizmo_hidden_by_modal(gizmo_obj):
gizmo_obj.hide = True
continue
gizmo_obj.hide = False
self.set_icon_gizmo_position(
gizmo_name,
mw=mw,
x=local_x,
y=icon_y,
z=icon_z,
billboard_rot=billboard_rot,
scale=self.ICON_HELPER_SCALE,
)
# Clickable world-space ``xN`` between cancel and minus. Mirrors the
# draft ``props.count`` so the displayed value tracks +/- drags live.
if self.is_gizmo_hidden_by_modal(self.count_label_gizmo):
@@ -1492,26 +1482,19 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
return cls._FEATURE_IDLE_MAX_X.get(match.name, 0.0)
class GizmoArrayChild(bpy.types.GizmoGroup):
"""Three helper icons surfaced on each array child, mirroring the panel
actions for that array — Regenerate, Select Parent, Select All Array Objects.
class GizmoArrayChild(bpy.types.GizmoGroup, gizmo.BillboardingGizmoGroupMixin):
"""Two navigation icons on each array child:
Standalone gizmo group (not a ``BaseParametricGizmoGroup`` subclass) because
the base's ``poll`` early-returns on array children (the mutual-exclusion
safeguard for the per-feature gizmo groups) and none of the editing-lifecycle
scaffolding applies — there's nothing to edit on a managed replica.
Two navigation icons:
- ``VIEW3D_GT_array_parent`` (hierarchy tree) → modifier-aware select via
``bim.array_parent_gizmo_click``: click selects the parent, Shift+click
selects the whole family, Ctrl+click selects only the children.
- ``VIEW3D_GT_array_all`` (2×2 grid) → jump to the parent and enter
array edit (mirrors the per-layer ARRAY icon on the parent, so the
child has a one-click path into the same edit flow).
array edit.
Regenerate isn't surfaced here — it's a maintenance action the panel
still exposes, and adding it as a child gizmo just clutters the viewport
without giving anything the panel doesn't."""
Standalone gizmo group (not a ``BaseParametricGizmoGroup`` subclass)
because the base's ``poll`` early-returns on array children — there's
nothing to edit on a managed replica. Regenerate isn't surfaced as a
child gizmo; the panel still exposes it."""
bl_idname = "OBJECT_GGT_bim_array_child"
bl_label = "Array Child Helpers"
@@ -1525,7 +1508,6 @@ class GizmoArrayChild(bpy.types.GizmoGroup):
ICON_ALL_X = 0.5
ICON_Z_OFFSET = 0.5
ICON_SCALE = 0.5
ICON_ALPHA = 0.8
@classmethod
def poll(cls, context):
@@ -1542,32 +1524,15 @@ class GizmoArrayChild(bpy.types.GizmoGroup):
return tool.Blender.Modifier.is_array_child(element)
def setup(self, context: bpy.types.Context) -> None:
prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorator_color_unselected[:3]
highlight_color = prefs.decorator_color_selected[:3]
self.parent_gizmo = self._make_icon(
default_color, highlight_color = self.get_unselected_decoration_colors()
self.parent_gizmo = self.setup_icon_gizmo(
"VIEW3D_GT_array_parent", default_color, highlight_color, "bim.array_parent_gizmo_click"
)
self.all_gizmo = self._make_icon(
self.all_gizmo = self.setup_icon_gizmo(
"VIEW3D_GT_array_all", default_color, highlight_color, "bim.edit_array_from_child"
)
def _make_icon(
self,
gizmo_type: str,
color: tuple[float, float, float],
highlight_color: tuple[float, float, float],
operator: str,
) -> bpy.types.Gizmo:
gz = self.gizmos.new(gizmo_type)
gz.use_draw_scale = False
gz.color = color
gz.color_highlight = highlight_color
gz.alpha = self.ICON_ALPHA
gz.target_set_operator(operator)
return gz
def draw_prepare(self, context: bpy.types.Context) -> None:
def position_gizmos(self, context: bpy.types.Context) -> None:
obj = context.active_object
if obj is None or not obj.bound_box:
return
+87 -44
View File
@@ -31,7 +31,7 @@ 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
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig, IconSlot
from bonsai.tool.numeric_input import (
IntegerInputState,
run_integer_input_modal,
@@ -39,7 +39,7 @@ from bonsai.tool.numeric_input import (
)
V_ = tool.Blender.V_
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, ClassVar
from bmesh.types import BMVert
from bpy.props import IntProperty
@@ -462,16 +462,41 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
# === Stair-Specific Icon Layout (meters) ===
# Additional icons for stair editing, positioned after standard icons:
# [Validate] [Cancel] [Cycle] [TreadLock] [Plus] [Minus]
ICON_TREAD_LOCK_X = 1.24 # X position for tread lock toggle icon
ICON_PLUS_X = 1.61 # X position for add tread (+) icon
ICON_MINUS_X = 1.98 # X position for remove tread (-) icon
# === Stair-Specific Icon Layout ===
# Row order: [Validate] [Cancel] [Cycle] [TreadLock] [Plus] [Minus]
# The base class assigns X positions from ``feature_slots`` tuple order —
# adding an icon is a one-line append, no hardcoded X constant.
ICON_PLUS_MINUS_SCALE = 0.24 # Scale for plus/minus icons (slightly larger)
ICON_CYCLE_SCALE = 0.3 # Scale for cycle type icon
ICON_Z_OFFSET = 0.5 # Z offset above geometry for editing icons
feature_slots: ClassVar[tuple[IconSlot, ...]] = (
IconSlot(
name="tread_lock",
gizmo_idname="VIEW3D_GT_lock",
variants=("open", "closed"),
operator="bim.toggle_stair_property",
color=(1.0, 1.0, 1.0),
operator_props=(("property_name", "custom_tread_lock"),),
),
IconSlot(
name="plus",
gizmo_idname="VIEW3D_GT_plus",
operator="bim.adjust_stair_treads",
scale=ICON_PLUS_MINUS_SCALE,
color=(0.1, 0.8, 0.1),
operator_props=(("increment", 1),),
),
IconSlot(
name="minus",
gizmo_idname="VIEW3D_GT_minus",
operator="bim.adjust_stair_treads",
scale=ICON_PLUS_MINUS_SCALE,
color=(1.0, 0.2, 0.2),
operator_props=(("increment", -1),),
),
)
enable_editing_operator = "bim.enable_editing_stair"
finish_editing_operator = "bim.finish_editing_stair"
cancel_editing_operator = "bim.cancel_editing_stair"
@@ -588,25 +613,16 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
return tool.Blender.Modifier.is_stair(element)
def setup_element_specific_gizmos(self, context: bpy.types.Context) -> None:
"""Create stair-specific icon gizmos (lock, plus, minus)."""
self.lock_gizmo = self.create_icon_gizmo(
"VIEW3D_GT_lock",
"""Create the total-length lock as an open/closed pair. Click toggles
``props.total_length_lock``; the per-frame update hook picks which
member is visible. Anchored to the stair's far X end (not the edit
row) so it's positioned by ``_update_lock_gizmo_position`` rather
than the toolbar slot system."""
self.total_length_lock_open_gizmo, self.total_length_lock_closed_gizmo = self.create_icon_gizmo_lock_pair(
"bim.toggle_stair_property",
self.COLOR_BLUE,
"bim.toggle_stair_property",
property_name="total_length_lock",
)
self.tread_lock_gizmo = self.create_icon_gizmo(
"VIEW3D_GT_lock",
(1.0, 1.0, 1.0),
"bim.toggle_stair_property",
property_name="custom_tread_lock",
)
self.plus_gizmo = self.create_icon_gizmo(
"VIEW3D_GT_plus", self.COLOR_GREEN, "bim.adjust_stair_treads", increment=1
)
self.minus_gizmo = self.create_icon_gizmo(
"VIEW3D_GT_minus", self.COLOR_RED, "bim.adjust_stair_treads", increment=-1
)
def _refresh_element_specific(
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
@@ -618,19 +634,38 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
self.update_tread_count_gizmos(props)
def update_lock_gizmo(self, props: "BIMStairProperties") -> None:
"""Update lock gizmo color and visibility. Positioning is handled
per-frame by the dimension-positioning hook."""
gizmo_prefs = self.get_gizmo_prefs()
if not self.update_gizmo_visibility(self.lock_gizmo, props.is_editing, gizmo_prefs.lock):
return # Hidden, skip color update
self.lock_gizmo.color = self.COLOR_RED if props.total_length_lock else self.COLOR_GREEN
def update_tread_lock_gizmo(self, props: "BIMStairProperties") -> None:
"""Update visibility of tread lock gizmo. Positioning is handled in _update_editing_icon_positions."""
if not hasattr(self, "tread_lock_gizmo"):
"""Show the open/closed total-length lock variant matching
``props.total_length_lock``. Positioning is handled per-frame by
the dimension-positioning hook."""
if not hasattr(self, "total_length_lock_open_gizmo"):
return
gizmo_prefs = self.get_gizmo_prefs()
self.update_gizmo_visibility(self.tread_lock_gizmo, props.is_editing, gizmo_prefs.lock)
visible = props.is_editing and gizmo_prefs.lock
if not visible:
self.total_length_lock_open_gizmo.hide = True
self.total_length_lock_closed_gizmo.hide = True
return
self.total_length_lock_open_gizmo.hide = props.total_length_lock
self.total_length_lock_closed_gizmo.hide = not props.total_length_lock
def update_tread_lock_gizmo(self, props: "BIMStairProperties") -> None:
"""Show the open/closed lock variant matching ``props.custom_tread_lock``.
Both pair members share an X position (set by the base's slot
positioning); this picks which one is visible per frame so a state
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:
self.tread_lock_open_gizmo.hide = True
self.tread_lock_closed_gizmo.hide = True
return
self.tread_lock_open_gizmo.hide = props.custom_tread_lock
self.tread_lock_closed_gizmo.hide = not props.custom_tread_lock
def update_tread_count_gizmos(self, props: "BIMStairProperties") -> None:
"""Update visibility of +/- tread count gizmos. Positioning is handled in _update_editing_icon_positions."""
@@ -719,10 +754,12 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
billboard_rot: Matrix,
total_run: float,
) -> None:
"""Update lock gizmo position based on Y view direction."""
"""Update lock gizmo pair position based on Y view direction. Writes
the matrix on both members so a state flip can't reveal a stale pose."""
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=True)
self.set_icon_gizmo_position(
"lock_gizmo",
self.set_icon_gizmo_pair_position(
"total_length_lock_open_gizmo",
"total_length_lock_closed_gizmo",
mw,
total_run + self.ICON_Z_OFFSET,
y_pos,
@@ -734,30 +771,36 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
def _update_editing_icon_positions(
self, mw: Matrix, props: "BIMStairProperties", viewing_from_negative_y: bool, billboard_rot: Matrix
) -> None:
"""Update editing icon positions, flipping Y based on viewing angle."""
"""Reposition the editing icons at stair's view-dependent Y. The base
class's update_editing_gizmos already placed them at the default
``get_icon_y_offset`` Y this overrides with the stair-specific
``get_icon_y_for_view`` flip so the icons land on the side the
camera is looking from."""
if not props.is_editing:
return
icon_z = props.height + self.ICON_Z_OFFSET
y_pos = self.get_icon_y_for_view(props, viewing_from_negative_y)
slot_x = self._slot_x_positions()
self.set_icon_gizmo_position("validate_gizmo", mw, 0, y_pos, icon_z, billboard_rot)
self.set_icon_gizmo_position("cancel_gizmo", mw, self.ICON_CANCEL_X, y_pos, icon_z, billboard_rot)
self.set_icon_gizmo_position(
"cycle_gizmo", mw, self.ICON_CYCLE_X, y_pos, icon_z, billboard_rot, scale=self.ICON_CYCLE_SCALE
)
self.set_icon_gizmo_position(
"tread_lock_gizmo",
self.set_icon_gizmo_pair_position(
"tread_lock_open_gizmo",
"tread_lock_closed_gizmo",
mw,
self.ICON_TREAD_LOCK_X,
slot_x["tread_lock"],
y_pos,
icon_z - self.EDITING_ICON_SCALE / 2,
billboard_rot,
scale=self.EDITING_ICON_SCALE,
)
self.set_icon_gizmo_position(
"plus_gizmo", mw, self.ICON_PLUS_X, y_pos, icon_z, billboard_rot, scale=self.ICON_PLUS_MINUS_SCALE
"plus_gizmo", mw, slot_x["plus"], y_pos, icon_z, billboard_rot, scale=self.ICON_PLUS_MINUS_SCALE
)
self.set_icon_gizmo_position(
"minus_gizmo", mw, self.ICON_MINUS_X, y_pos, icon_z, billboard_rot, scale=self.ICON_PLUS_MINUS_SCALE
"minus_gizmo", mw, slot_x["minus"], y_pos, icon_z, billboard_rot, scale=self.ICON_PLUS_MINUS_SCALE
)
+67 -101
View File
@@ -53,7 +53,7 @@ import bonsai.core.root
import bonsai.tool as tool
from bonsai.bim.ifc import IfcStore
from bonsai.bim.module.drawing import gizmos as gizmo
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig, IconSlot
from bonsai.bim.module.model import preview_base
from bonsai.bim.module.model.decorator import PolylineDecorator, ProductDecorator
from bonsai.bim.module.model.polyline import PolylineOperator
@@ -1988,19 +1988,27 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
(0, 0, 1),
)
# X offsets in the editing icon row, additive from ICON_VALIDATE_X (0.0).
# Matches the cadence used by the base class (0.0 / 0.5 / 0.87 = step ≈ 0.37).
# The baseline icons (EXT / CEN / INT) all share ICON_CYCLE_X — only one is
# ever visible at a time so they don't overlap.
ICON_ROTATE_X = 1.24
# Mapping from BIMWallProperties.desired_offset_baseline value to the
# attribute on `self` that holds the corresponding state icon.
_BASELINE_GIZMO_ATTRS: ClassVar[dict[str, str]] = {
"EXTERIOR": "offset_exterior_gizmo",
"CENTER": "offset_center_gizmo",
"INTERIOR": "offset_interior_gizmo",
}
# Row layout: validate / cancel / baseline-triplet / rotate / array.
# Wall has no ``cycle_type_operator``, so the cycle slot collapses and
# the baseline triplet takes the cycle X position (0.87). Rotate
# follows at 1.24. Both slots are declared here — the layout manager
# assigns the X positions from tuple order.
feature_slots: ClassVar[tuple[IconSlot, ...]] = (
IconSlot(
name="baseline",
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",
),
)
def setup_element_specific_gizmos(self, context: bpy.types.Context) -> None:
"""Wall-specific gizmos.
@@ -2015,16 +2023,15 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
wall top (Z=height in wall-local). Clicking extends the wall's height to
the cursor's Z.
Icon-row (always visible during edit mode, fixed position):
Idle-row icon outside the toolbar slot system:
- ``offset_{exterior,center,interior}_gizmo`` three state-specific icons,
only one visible at a time. Reflects ``props.desired_offset_baseline``.
Clicking any of them cycles the baseline (the operator is the same).
- ``rotate_gizmo`` rotates the wall 90° around Z (Shift+R). Uses the
revolving-arrows icon now that the cycle slot is occupied by the
stateful baseline icons.
- ``toggle_openings_gizmo`` toggles opening fill visibility (Alt+O).
"""
- ``toggle_openings_gizmo`` toggles opening fill visibility (Alt+O),
surfaced in idle state next to the pen.
The baseline-state triplet (exterior/center/interior) and the rotate-90
icon live in ``feature_slots`` the base class handles creation and
edit-row positioning; this group only picks variant visibility per
frame in ``_update_icon_row_extras``."""
default_color, highlight_color = self.get_decoration_colors()
self.split_gizmo = self._setup_icon_gizmo(
"VIEW3D_GT_split",
@@ -2044,26 +2051,6 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
"bim.extend_wall_height_to_cursor",
highlight_color,
)
# Three baseline-state icons — only one is visible at a time, picked by
# the current props.desired_offset_baseline. All point to the same cycle
# operator so clicking any of them advances the cycle.
for baseline, attr_name in self._BASELINE_GIZMO_ATTRS.items():
setattr(
self,
attr_name,
self._setup_icon_gizmo(
f"VIEW3D_GT_offset_{baseline.lower()}",
default_color,
"bim.cycle_wall_offset",
highlight_color,
),
)
self.rotate_gizmo = self._setup_icon_gizmo(
"VIEW3D_GT_cycle",
default_color,
"bim.rotate_wall_90",
highlight_color,
)
self.toggle_openings_gizmo = self._setup_icon_gizmo(
"VIEW3D_GT_add_opening",
default_color,
@@ -2132,57 +2119,48 @@ class GizmoWallEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
gz.matrix_basis = gizmo.billboarded_at(world_pos, billboard_rot)
_apply_wall_extend_flips(gz, self, world_pos, mw, cursor_local, props, billboard_rot)
# Map ``props.desired_offset_baseline`` (storage form) to the slot variant
# name. Centralised here so the variant strings stay aligned with the slot
# declaration in feature_slots.
_BASELINE_TO_VARIANT: ClassVar[dict[str, str]] = {
"EXTERIOR": "exterior",
"CENTER": "center",
"INTERIOR": "interior",
}
def _update_icon_row_extras(self, context: bpy.types.Context, mw: Matrix, props: "BIMWallProperties") -> None:
"""Position the wall-specific icons in the icon row.
"""Pick which baseline variant is visible during edit, and position
the idle-row toggle-openings icon.
Edit-mode icons (visible only when ``props.is_editing``):
Baseline triplet: the base class's slot loop already wrote a billboard
matrix on each variant member at the same X (the cycle slot, since
wall has no ``cycle_type_operator``). This hook only flips ``hide``
on each member based on ``props.desired_offset_baseline`` so exactly
one variant shows. The rotate-90 icon is a single-icon feature slot
and is fully handled by the base.
- Three baseline icons (Exterior / Centreline / Interior) share the cycle
slot only the one matching ``props.desired_offset_baseline`` shows.
- Rotate-90 icon at ``ICON_ROTATE_X``.
Non-edit-mode icons (visible alongside the pen icon, hidden during edit):
- Toggle-openings icon next to the pen. Lives outside edit mode because
opening visibility is a viewport-display concern, not a wall-edit action.
Calls ``billboarded_at`` directly rather than routing through
``set_icon_gizmo_position`` because the icon row has wall-specific
visibility/state branching (baseline-indicator selection, edit-mode
toggle for opening-visibility) that the helper does not model."""
if not hasattr(self, "rotate_gizmo"):
Toggle-openings is NOT in the slot system it surfaces in IDLE
state (alongside the pen, not in the edit row), so it's positioned
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
# --- Edit-mode icons (baseline indicator + rotate-90) ---
if props.is_editing:
# Stateful baseline indicator at the cycle slot. Show exactly one of the
# three icons (the one matching the current baseline), hide the others.
for baseline, attr in self._BASELINE_GIZMO_ATTRS.items():
gz = getattr(self, attr)
if gizmo_prefs.cycle and baseline == props.desired_offset_baseline:
gz.hide = self.is_gizmo_hidden_by_modal(gz)
world_pos = mw @ Vector((self.ICON_VALIDATE_X + self.ICON_CYCLE_X, icon_y, icon_z))
gz.matrix_basis = gizmo.billboarded_at(world_pos, billboard_rot)
else:
gz.hide = True
if gizmo_prefs.rotate:
self.rotate_gizmo.hide = self.is_gizmo_hidden_by_modal(self.rotate_gizmo)
world_pos = mw @ Vector((self.ICON_VALIDATE_X + self.ICON_ROTATE_X, icon_y, icon_z))
# VIEW3D_GT_cycle is authored for the base class's 0.30 scale; at 0.5
# it looks roughly 2x too big next to the validate / cancel icons.
self.rotate_gizmo.matrix_basis = gizmo.billboarded_at(world_pos, billboard_rot, scale=0.30)
# --- Baseline variant visibility ---
active_variant = self._BASELINE_TO_VARIANT.get(props.desired_offset_baseline)
for variant in ("exterior", "center", "interior"):
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:
gz.hide = self.is_gizmo_hidden_by_modal(gz)
else:
self.rotate_gizmo.hide = True
else:
for attr in self._BASELINE_GIZMO_ATTRS.values():
getattr(self, attr).hide = True
self.rotate_gizmo.hide = True
gz.hide = True
# --- Non-edit-mode icons (toggle openings) ---
# --- 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:
@@ -3309,9 +3287,7 @@ class GizmoWallAddOpening(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMixin
return True
def setup(self, context: bpy.types.Context) -> None:
prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorations_colour[:3]
highlight_color = prefs.decorator_color_selected[:3]
default_color, highlight_color = self.get_decoration_colors()
self.add_opening_icon = self.setup_icon_gizmo(
"VIEW3D_GT_add_opening", default_color, highlight_color, "bim.add_opening"
)
@@ -3376,9 +3352,7 @@ class GizmoWallExtendVertically(bpy.types.GizmoGroup, _WallGeomCachedBillboardin
return True
def setup(self, context: bpy.types.Context) -> None:
prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorations_colour[:3]
highlight_color = prefs.decorator_color_selected[:3]
default_color, highlight_color = self.get_decoration_colors()
self.extend_vertical_icon = self.setup_icon_gizmo(
"VIEW3D_GT_extend_vertical",
default_color,
@@ -3456,9 +3430,7 @@ class GizmoWallJoinIntersection(bpy.types.GizmoGroup, _WallGeomCachedBillboardin
ICON_STACK_OFFSET_Y: ClassVar[float] = 0.4
def setup(self, context: bpy.types.Context) -> None:
prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorations_colour[:3]
highlight_color = prefs.decorator_color_selected[:3]
default_color, highlight_color = self.get_decoration_colors()
self.unjoin_icon = self.setup_icon_gizmo("VIEW3D_GT_split", default_color, highlight_color, "bim.unjoin_walls")
self.merge_icon = self.setup_icon_gizmo("VIEW3D_GT_merge", default_color, highlight_color, "bim.merge_wall")
self.join_icon = self.setup_icon_gizmo(
@@ -3618,9 +3590,7 @@ class GizmoWallUnjoinSingle(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMix
return True
def setup(self, context: bpy.types.Context) -> None:
prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorations_colour[:3]
highlight_color = prefs.decorator_color_selected[:3]
default_color, highlight_color = self.get_decoration_colors()
# Bind the operator on each pool icon ONCE at setup time and keep the returned
# OperatorProperties handles. target_set_operator allocates a fresh handle on
# every call, so calling it from position_gizmos (which fires every redraw
@@ -3720,9 +3690,7 @@ class GizmoWallFilletPreview(bpy.types.GizmoGroup):
return True
def setup(self, context: bpy.types.Context) -> None:
prefs = tool.Blender.get_addon_preferences()
default_color = tuple(prefs.decorations_colour[:3])
highlight_color = tuple(prefs.decorator_color_selected[:3])
default_color, highlight_color = self.get_decoration_colors()
# Lazy-fetched closures re-resolve the Scene per call so the freed-RNA
# crash on file open / undo doesn't hit the gizmo callbacks.
@@ -3991,9 +3959,7 @@ class GizmoWallFilletReedit(bpy.types.GizmoGroup, _WallGeomCachedBillboardingMix
return tool.Parametric.is_fillet_corner_wall(element)
def setup(self, context: bpy.types.Context) -> None:
prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorations_colour[:3]
highlight_color = prefs.decorator_color_selected[:3]
default_color, highlight_color = self.get_decoration_colors()
self.edit_icon = self.setup_icon_gizmo(
"VIEW3D_GT_pen",
default_color,