mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Add IconSlot placeholders + stair xN tread label
Add a clickable "xN" badge to GizmoStairEdition's edit row, mirroring the array's popup-input UX: click opens a number dialog (no more shift+click-into-modal). Text-only — no 2x2 grid glyph. Structural changes that enable this cleanly: * IconSlot.placeholder=True: slots reserve an X position in the row without auto-creating a gizmo. Subclasses resolve the reserved X via _slot_x_positions()[name] to place their own dynamic gizmos. Drops the brittle "remember to add extra_gap_before" workaround that would silently rot on slot reorders. * Array bug fix: the count badge collided with the "-" icon because the slot manager placed count_minus at the cycle position (X=0.87) where ICON_NUMBER_X also lives. Migrating the badge to a placeholder slot lets the manager allocate the X naturally and the "-" no longer overlaps. ICON_NUMBER_X constant removed. * IntegerInputDialogMixin in parametric_lifecycle.py: extracts the popup-dialog plumbing shared between InputArrayCount and the new InputStairTreads. Subclasses declare an IntProperty + attr_name + props_getter; the mixin owns invoke/execute. _resolve_props helper factors the common obj/props/requires_editing prologue. Tests: BIM_GT_count_label registration; IconSlot placeholder contract (no gizmo_idname required; gizmo_attrs() returns empty); the stair edit-row slot layout reserves the label position between tread_lock and plus at one ICON_ARRAY_GAP each; visibility propagates from props.is_editing. Partly generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -154,6 +154,7 @@ classes = (
|
||||
gizmos.GizmoArrayParent,
|
||||
gizmos.GizmoArrayAll,
|
||||
gizmos.GizmoArrayLayerIndicator,
|
||||
gizmos.GizmoCountLabel,
|
||||
gizmos.GizmoMerge,
|
||||
gizmos.GizmoSplit,
|
||||
gizmos.GizmoUnjoin,
|
||||
|
||||
@@ -3871,6 +3871,44 @@ class GizmoArrayLayerIndicator(bpy.types.Gizmo):
|
||||
draw_array_layer_children_bbox(context, parent_element, self._layer_index)
|
||||
|
||||
|
||||
class GizmoCountLabel(bpy.types.Gizmo):
|
||||
"""``xN`` text label rendered from 7-segment digit triangles.
|
||||
|
||||
Mirrors a caller-supplied integer (set via :meth:`set_count`) into a
|
||||
live count badge. No icon glyph; the gizmo is the number alone."""
|
||||
|
||||
bl_idname = "BIM_GT_count_label"
|
||||
|
||||
__slots__ = ("custom_shape", "_count", "_built_count", "_outlined_batch")
|
||||
|
||||
def setup(self) -> None:
|
||||
self._count = 0
|
||||
self._built_count = -1
|
||||
tris = _count_label_tris(self._count, 0.0, 0.0)
|
||||
self.custom_shape = self.new_custom_shape("TRIS", tris)
|
||||
self._outlined_batch = batch_for_shader(_get_static_tris_shader(), "TRIS", {"pos": tris})
|
||||
self._built_count = 0
|
||||
|
||||
def set_count(self, count: int) -> None:
|
||||
self._count = int(count)
|
||||
|
||||
def _ensure_shape(self) -> None:
|
||||
if self._built_count != self._count:
|
||||
tris = _count_label_tris(self._count, 0.0, 0.0)
|
||||
self.custom_shape = self.new_custom_shape("TRIS", tris)
|
||||
self._outlined_batch = batch_for_shader(_get_static_tris_shader(), "TRIS", {"pos": tris})
|
||||
self._built_count = self._count
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
self._ensure_shape()
|
||||
color = (*self.color_highlight, 1.0) if self.is_highlight else (*self.color, 1.0)
|
||||
draw_tris_with_outline(self._outlined_batch, self.matrix_basis @ self.matrix_offset, color)
|
||||
|
||||
def draw_select(self, context: bpy.types.Context, select_id: int) -> None:
|
||||
self._ensure_shape()
|
||||
self.draw_custom_shape(self.custom_shape, select_id=select_id)
|
||||
|
||||
|
||||
class GizmoMerge(StaticTrisGizmoMixin, bpy.types.Gizmo):
|
||||
"""Two arrows pointing inward toward each other — conveys joining/merging elements."""
|
||||
|
||||
@@ -4986,11 +5024,16 @@ class IconSlot:
|
||||
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)."""
|
||||
+/- adjuster, ``property_name="..."`` for a generic toggle).
|
||||
- ``placeholder`` — when ``True``, the slot reserves an X position in
|
||||
the row but no auto-managed gizmo is created. Subclasses look the X
|
||||
up via ``_slot_x_positions()[name]`` to place their own dynamically-
|
||||
built gizmos (e.g. a live count label). ``gizmo_idname`` / ``operator``
|
||||
are unused for placeholders."""
|
||||
|
||||
name: str
|
||||
gizmo_idname: str | tuple[str, ...]
|
||||
operator: 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
|
||||
@@ -5000,10 +5043,13 @@ class IconSlot:
|
||||
variants: tuple[str, ...] = ()
|
||||
extra_gap_before: float = 0.0
|
||||
operator_props: tuple[tuple[str, Any], ...] = ()
|
||||
placeholder: bool = False
|
||||
|
||||
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.placeholder:
|
||||
return
|
||||
if self.variants:
|
||||
if isinstance(self.gizmo_idname, str):
|
||||
pass # prefix form — idname auto-suffixed per variant
|
||||
@@ -5015,10 +5061,11 @@ class IconSlot:
|
||||
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):
|
||||
elif not isinstance(self.gizmo_idname, str) or not self.gizmo_idname:
|
||||
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)"
|
||||
f"got {self.gizmo_idname!r} (set variants=(...) if you want a multi-variant "
|
||||
f"slot, or placeholder=True for a reserved-position slot)"
|
||||
)
|
||||
|
||||
def variant_idnames(self) -> tuple[str, ...]:
|
||||
@@ -5034,7 +5081,10 @@ class IconSlot:
|
||||
|
||||
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."""
|
||||
Returns one for a single slot, N for an N-variant slot, and an empty
|
||||
tuple for placeholder slots (which reserve X without an auto-gizmo)."""
|
||||
if self.placeholder:
|
||||
return ()
|
||||
if self.variants:
|
||||
return tuple(f"{self.name}_{variant}_gizmo" for variant in self.variants)
|
||||
return (f"{self.name}_gizmo",)
|
||||
@@ -5879,8 +5929,12 @@ class BaseParametricGizmoGroup:
|
||||
# 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.
|
||||
# the subclass picks which is visible per frame. Placeholder slots
|
||||
# only reserve an X position — the subclass creates its own gizmo
|
||||
# there in ``setup_element_specific_gizmos``.
|
||||
for slot in self.feature_slots:
|
||||
if slot.placeholder:
|
||||
continue
|
||||
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()):
|
||||
@@ -6155,6 +6209,8 @@ class BaseParametricGizmoGroup:
|
||||
# whenever the gizmo group polls visible.
|
||||
slot_positions = self._slot_x_positions()
|
||||
for slot in self.feature_slots:
|
||||
if slot.placeholder:
|
||||
continue
|
||||
slot_x = self.ICON_VALIDATE_X + slot_positions[slot.name]
|
||||
attrs = slot.gizmo_attrs()
|
||||
if slot.variants:
|
||||
|
||||
@@ -210,6 +210,7 @@ classes = (
|
||||
stair.ToggleStairProperty,
|
||||
stair.AdjustStairTreads,
|
||||
stair.SetStairTreads,
|
||||
stair.InputStairTreads,
|
||||
stair.CycleStairType,
|
||||
stair.GizmoStairEdition,
|
||||
sverchok_modifier.CreateNewSverchokGraph,
|
||||
|
||||
@@ -34,7 +34,10 @@ from bonsai.bim.module.drawing.gizmos import (
|
||||
DimensionGizmoConfig,
|
||||
IconSlot,
|
||||
)
|
||||
from bonsai.bim.parametric_lifecycle import ParametricEditMixinBase
|
||||
from bonsai.bim.parametric_lifecycle import (
|
||||
IntegerInputDialogMixin,
|
||||
ParametricEditMixinBase,
|
||||
)
|
||||
|
||||
|
||||
def _wipe_array_children(layers: list) -> None:
|
||||
@@ -1052,11 +1055,10 @@ class RemoveArrayLayerFromEdit(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class InputArrayCount(bpy.types.Operator):
|
||||
"""Open a number-input dialog so the user can type a new ``count`` during
|
||||
an active edit lifecycle. Bound to the world-space count gizmo in the edit
|
||||
row (between cancel and minus) — for users who'd rather type a value than
|
||||
repeatedly click +/-."""
|
||||
class InputArrayCount(IntegerInputDialogMixin, bpy.types.Operator):
|
||||
"""Popup-dialog entry point for typing a new draft ``count`` during an
|
||||
active array edit lifecycle. Bound to the world-space count gizmo in
|
||||
the edit row."""
|
||||
|
||||
bl_idname = "bim.input_array_count"
|
||||
bl_label = "Set Array Count"
|
||||
@@ -1064,26 +1066,9 @@ class InputArrayCount(bpy.types.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
count: bpy.props.IntProperty(name="Count", default=1, min=1)
|
||||
|
||||
def invoke(self, context, event):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
props = tool.Model.get_array_props(obj)
|
||||
if not props.is_editing:
|
||||
return {"CANCELLED"}
|
||||
self.count = max(1, props.count)
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
props = tool.Model.get_array_props(obj)
|
||||
if not props.is_editing:
|
||||
return {"CANCELLED"}
|
||||
props.count = max(1, self.count)
|
||||
return {"FINISHED"}
|
||||
attr_name = "count"
|
||||
props_getter = staticmethod(tool.Model.get_array_props)
|
||||
requires_editing = True
|
||||
|
||||
|
||||
class AdjustArrayCount(bpy.types.Operator):
|
||||
@@ -1138,18 +1123,18 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
hide_pen_button = True
|
||||
|
||||
# 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
|
||||
# The ``count_label`` placeholder reserves the position for the
|
||||
# dynamically-built ``xN`` gizmo; the +/-/method/trash icons live in
|
||||
# ``feature_slots`` below and 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.
|
||||
|
||||
# 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_label", placeholder=True),
|
||||
IconSlot(
|
||||
name="count_minus",
|
||||
gizmo_idname="VIEW3D_GT_minus",
|
||||
@@ -1290,12 +1275,11 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
``feature_slots`` and are auto-created by the base class."""
|
||||
default_color, highlight_color = self.get_decoration_colors()
|
||||
# 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``.
|
||||
# input dialog 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. Position is
|
||||
# reserved by the ``count_label`` placeholder slot in
|
||||
# ``feature_slots``; the matrix is set per-frame below.
|
||||
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
|
||||
@@ -1431,7 +1415,7 @@ class GizmoArrayEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
self.count_label_gizmo.set_count(int(props.count))
|
||||
world_pos = mw @ Vector(
|
||||
(
|
||||
self.ICON_VALIDATE_X + self.ICON_NUMBER_X,
|
||||
self.ICON_VALIDATE_X + self._slot_x_positions()["count_label"],
|
||||
icon_y,
|
||||
icon_z,
|
||||
)
|
||||
|
||||
@@ -37,6 +37,7 @@ from bonsai.bim.module.drawing.gizmos import (
|
||||
DimensionGizmoConfig,
|
||||
IconSlot,
|
||||
)
|
||||
from bonsai.bim.parametric_lifecycle import IntegerInputDialogMixin
|
||||
from bonsai.tool.numeric_input import (
|
||||
IntegerInputState,
|
||||
run_integer_input_modal,
|
||||
@@ -383,6 +384,20 @@ class AdjustStairTreads(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class InputStairTreads(IntegerInputDialogMixin, bpy.types.Operator):
|
||||
"""Popup-dialog entry point for typing a new ``number_of_treads`` value.
|
||||
Bound to the world-space ``xN`` count label in the stair edit row."""
|
||||
|
||||
bl_idname = "bim.input_stair_treads"
|
||||
bl_label = "Set Number of Treads"
|
||||
bl_description = "Type the number of treads for this stair"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
number_of_treads: IntProperty(name="Number of Treads", default=1, min=1)
|
||||
attr_name = "number_of_treads"
|
||||
props_getter = staticmethod(tool.Model.get_stair_props)
|
||||
|
||||
|
||||
class SetStairTreads(bpy.types.Operator):
|
||||
"""Set the number of treads to a specific value."""
|
||||
|
||||
@@ -468,11 +483,12 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
bl_options = {"3D", "PERSISTENT"}
|
||||
|
||||
# === Stair-Specific Icon Layout ===
|
||||
# Row order: [Validate] [Cancel] [Cycle] [TreadLock] [Plus] [Minus]
|
||||
# Row order: [Validate] [Cancel] [Cycle] [TreadLock] [xN] [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_COUNT_LABEL_SCALE = 0.36 # Scale for the xN tread-count label
|
||||
ICON_Z_OFFSET = 0.5 # Z offset above geometry for editing icons
|
||||
|
||||
feature_slots: ClassVar[tuple[IconSlot, ...]] = (
|
||||
@@ -484,6 +500,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
color=(1.0, 1.0, 1.0),
|
||||
operator_props=(("property_name", "custom_tread_lock"),),
|
||||
),
|
||||
IconSlot(name="tread_count_label", placeholder=True),
|
||||
IconSlot(
|
||||
name="plus",
|
||||
gizmo_idname="VIEW3D_GT_plus",
|
||||
@@ -618,16 +635,28 @@ 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 the total-length lock as an open/closed pair. Click toggles
|
||||
"""Create the total-length lock as an open/closed pair plus the
|
||||
``xN`` tread-count label. Lock 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."""
|
||||
than the toolbar slot system.
|
||||
|
||||
The count label binds to ``bim.input_stair_treads`` (popup dialog)
|
||||
for click-to-type input and sits at the X reserved by the
|
||||
``tread_count_label`` placeholder slot in ``feature_slots``."""
|
||||
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,
|
||||
property_name="total_length_lock",
|
||||
)
|
||||
default_color, highlight_color = self.get_decoration_colors()
|
||||
self.tread_count_label_gizmo = self.gizmos.new("BIM_GT_count_label")
|
||||
self.tread_count_label_gizmo.use_draw_scale = False
|
||||
self.tread_count_label_gizmo.color = default_color
|
||||
self.tread_count_label_gizmo.color_highlight = highlight_color
|
||||
self.tread_count_label_gizmo.alpha = 0.8
|
||||
self.tread_count_label_gizmo.target_set_operator("bim.input_stair_treads")
|
||||
|
||||
def _refresh_element_specific(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
|
||||
@@ -667,12 +696,15 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
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."""
|
||||
"""Update visibility of the +/- tread count gizmos and the ``xN``
|
||||
label. Positioning is handled in ``_update_editing_icon_positions``."""
|
||||
if not hasattr(self, "plus_gizmo") or not hasattr(self, "minus_gizmo"):
|
||||
return
|
||||
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)
|
||||
if hasattr(self, "tread_count_label_gizmo"):
|
||||
self.update_gizmo_visibility(self.tread_count_label_gizmo, props.is_editing)
|
||||
|
||||
def _update_dimension_gizmo_positions(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" # noqa: ARG002
|
||||
@@ -800,3 +832,14 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
self.set_icon_gizmo_position(
|
||||
"minus_gizmo", mw, slot_x["minus"], y_pos, icon_z, billboard_rot, scale=self.ICON_PLUS_MINUS_SCALE
|
||||
)
|
||||
if hasattr(self, "tread_count_label_gizmo"):
|
||||
self.tread_count_label_gizmo.set_count(int(props.number_of_treads))
|
||||
self.set_icon_gizmo_position(
|
||||
"tread_count_label_gizmo",
|
||||
mw,
|
||||
slot_x["tread_count_label"],
|
||||
y_pos,
|
||||
icon_z,
|
||||
billboard_rot,
|
||||
scale=self.ICON_COUNT_LABEL_SCALE,
|
||||
)
|
||||
|
||||
@@ -528,6 +528,51 @@ class PickTypeMixin(TypeAccessorBase):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class IntegerInputDialogMixin:
|
||||
"""Operator mixin that mirrors a per-feature ``IntProperty`` on the
|
||||
operator into a draft attribute on the active object's parametric props,
|
||||
via Blender's ``invoke_props_dialog`` popup.
|
||||
|
||||
Subclasses declare:
|
||||
|
||||
- ``attr_name`` — name of the IntProperty on the subclass AND of the
|
||||
attribute on the resolved props (same name on both sides).
|
||||
- ``props_getter`` — ``staticmethod(tool.Model.get_<feature>_props)``.
|
||||
- ``requires_editing`` — True iff the operator must no-op outside an
|
||||
active edit lifecycle. Default False.
|
||||
- ``value_min`` — minimum value to clamp to. Default 1."""
|
||||
|
||||
attr_name: ClassVar[str] = ""
|
||||
props_getter: ClassVar[Callable[[bpy.types.Object], bpy.types.PropertyGroup]]
|
||||
requires_editing: ClassVar[bool] = False
|
||||
value_min: ClassVar[int] = 1
|
||||
|
||||
def _resolve_props(self, context: bpy.types.Context) -> bpy.types.PropertyGroup | None:
|
||||
"""Return the active object's parametric props if the operator is
|
||||
allowed to fire, ``None`` otherwise (caller bails with ``CANCELLED``)."""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return None
|
||||
props = self.props_getter(obj)
|
||||
if self.requires_editing and not props.is_editing:
|
||||
return None
|
||||
return props
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: # noqa: ARG002
|
||||
props = self._resolve_props(context)
|
||||
if props is None:
|
||||
return {"CANCELLED"}
|
||||
setattr(self, self.attr_name, max(self.value_min, getattr(props, self.attr_name)))
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> set[str]:
|
||||
props = self._resolve_props(context)
|
||||
if props is None:
|
||||
return {"CANCELLED"}
|
||||
setattr(props, self.attr_name, max(self.value_min, getattr(self, self.attr_name)))
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# --- Undo-resync registry ----------------------------------------------------
|
||||
#
|
||||
# Per-type regenerators called from ``resync_parametric_drafts_after_undo``
|
||||
|
||||
@@ -133,3 +133,75 @@ def test_set_icon_gizmo_position_does_not_apply_object_rotation():
|
||||
for row_a, row_b in zip(stub.matrix_basis, expected):
|
||||
for va, vb in zip(row_a, row_b):
|
||||
assert abs(va - vb) < 1e-6
|
||||
|
||||
|
||||
def test_icon_slot_placeholder_skips_validation_and_returns_no_attrs():
|
||||
"""Placeholder slots reserve an X position without an auto-created gizmo:
|
||||
construction must not require ``gizmo_idname`` / ``operator``, and
|
||||
``gizmo_attrs()`` must return an empty tuple so the base class's
|
||||
setup/positioning loops naturally skip the slot."""
|
||||
from bonsai.bim.module.drawing.gizmos import IconSlot
|
||||
|
||||
slot = IconSlot(name="my_label", placeholder=True)
|
||||
assert slot.placeholder is True
|
||||
assert slot.gizmo_attrs() == ()
|
||||
|
||||
with pytest.raises(TypeError, match="gizmo_idname"):
|
||||
IconSlot(name="broken")
|
||||
|
||||
|
||||
def test_count_label_gizmo_is_registered():
|
||||
"""The shared text-only ``xN`` gizmo must register so the stair group's
|
||||
``gizmos.new("BIM_GT_count_label")`` resolves."""
|
||||
from bonsai.bim.module.drawing.gizmos import GizmoCountLabel
|
||||
|
||||
assert GizmoCountLabel.bl_idname == "BIM_GT_count_label"
|
||||
assert bpy.types.Gizmo.bl_rna_get_subclass_py("BIM_GT_count_label") is GizmoCountLabel
|
||||
|
||||
|
||||
def test_stair_edit_row_reserves_label_slot_between_tread_lock_and_plus():
|
||||
"""The ``tread_count_label`` placeholder slot must sit one
|
||||
``ICON_ARRAY_GAP`` past the tread-lock and one gap before the plus
|
||||
icon, so the layout naturally allocates the count label's X without
|
||||
any subclass-side gap math."""
|
||||
from bonsai.bim.module.drawing.gizmos import BaseParametricGizmoGroup
|
||||
from bonsai.bim.module.model.stair import GizmoStairEdition
|
||||
|
||||
slot_x = GizmoStairEdition._slot_x_positions()
|
||||
gap = BaseParametricGizmoGroup.ICON_ARRAY_GAP
|
||||
|
||||
assert "tread_count_label" in slot_x
|
||||
assert slot_x["tread_count_label"] - slot_x["tread_lock"] == pytest.approx(gap)
|
||||
assert slot_x["plus"] - slot_x["tread_count_label"] == pytest.approx(gap)
|
||||
assert slot_x["minus"] - slot_x["plus"] == pytest.approx(gap)
|
||||
|
||||
|
||||
def test_update_tread_count_gizmos_toggles_label_with_editing():
|
||||
"""``update_tread_count_gizmos`` must propagate ``props.is_editing``
|
||||
to the label's hide state so the badge appears only inside edit mode."""
|
||||
from bonsai.bim.module.drawing.gizmos import BaseParametricGizmoGroup
|
||||
from bonsai.bim.module.model.stair import GizmoStairEdition
|
||||
|
||||
class _GizmoStub:
|
||||
def __init__(self):
|
||||
self.hide = False
|
||||
|
||||
plus_gz = _GizmoStub()
|
||||
minus_gz = _GizmoStub()
|
||||
label_gz = _GizmoStub()
|
||||
|
||||
fake_self = types.SimpleNamespace(
|
||||
plus_gizmo=plus_gz,
|
||||
minus_gizmo=minus_gz,
|
||||
tread_count_label_gizmo=label_gz,
|
||||
update_gizmo_visibility=lambda g, v: BaseParametricGizmoGroup.update_gizmo_visibility(fake_self, g, v),
|
||||
is_gizmo_hidden_by_modal=lambda g: False,
|
||||
)
|
||||
|
||||
props_editing = types.SimpleNamespace(is_editing=True, number_of_treads=5)
|
||||
GizmoStairEdition.update_tread_count_gizmos(fake_self, props_editing)
|
||||
assert label_gz.hide is False
|
||||
|
||||
props_idle = types.SimpleNamespace(is_editing=False, number_of_treads=5)
|
||||
GizmoStairEdition.update_tread_count_gizmos(fake_self, props_idle)
|
||||
assert label_gz.hide is True
|
||||
|
||||
Reference in New Issue
Block a user