mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 09:21:46 +00:00
Major refactor of parametric gizmo system: dimension lines and code consolidation
Major changes: - Relocate gizmo infrastructure from bim/gizmo.py to module/drawing/gizmos.py - Replace arrow-based property gizmos with dimension line gizmos throughout - Add view-dependent positioning: gizmos automatically reposition based on camera angle to stay visible and avoid overlapping geometry - Add special icons for stair to deal with edge cases Code quality: - Add DRY helper methods for gizmo positioning across stair, door, and window - Remove redundant visibility logic - Extract integer input handling to dedicated module for reuse - Add comprehensive documentation for gizmo architecture
This commit is contained in:
@@ -21,7 +21,7 @@ import bpy
|
||||
import bpy.utils.previews
|
||||
import importlib
|
||||
from bpy_extras.io_utils import ImportHelper, ExportHelper
|
||||
from . import handler, ui, prop, operator, gizmo
|
||||
from . import handler, ui, prop, operator
|
||||
from typing import Union
|
||||
from collections.abc import Callable
|
||||
|
||||
@@ -206,19 +206,6 @@ classes = [
|
||||
ui.BIM_PT_section_with_cappings,
|
||||
ui.BIM_PT_decorators_overlay,
|
||||
ui.BIM_PT_snappping,
|
||||
# Gizmos
|
||||
gizmo.BIM_OT_gizmo_value_input,
|
||||
gizmo.GizmoArrow,
|
||||
gizmo.GizmoArrow2D,
|
||||
gizmo.GizmoCone,
|
||||
gizmo.GizmoLock,
|
||||
gizmo.GizmoArc,
|
||||
gizmo.GizmoPen,
|
||||
gizmo.GizmoValidate,
|
||||
gizmo.GizmoCancel,
|
||||
gizmo.GizmoPlus,
|
||||
gizmo.GizmoMinus,
|
||||
gizmo.GizmoCycle,
|
||||
]
|
||||
|
||||
for mod in modules.values():
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -111,9 +111,22 @@ classes = (
|
||||
ui.BIM_PT_text,
|
||||
ui.BIM_UL_drawinglist,
|
||||
ui.BIM_UL_sheets,
|
||||
# Core gizmos (shared across modules)
|
||||
gizmos.BIM_OT_gizmo_value_input,
|
||||
gizmos.GizmoArrow,
|
||||
gizmos.GizmoArrow2D,
|
||||
gizmos.GizmoCone,
|
||||
gizmos.GizmoDimension,
|
||||
gizmos.GizmoLock,
|
||||
gizmos.GizmoArc,
|
||||
gizmos.GizmoPen,
|
||||
gizmos.GizmoValidate,
|
||||
gizmos.GizmoCancel,
|
||||
gizmos.GizmoPlus,
|
||||
gizmos.GizmoMinus,
|
||||
gizmos.GizmoCycle,
|
||||
# Drawing-specific gizmos
|
||||
gizmos.UglyDotGizmo,
|
||||
gizmos.DotGizmo,
|
||||
gizmos.DimensionLabelGizmo,
|
||||
gizmos.ExtrusionGuidesGizmo,
|
||||
gizmos.ExtrusionWidget,
|
||||
workspace.LaunchAnnotationTypeManager,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -162,7 +162,9 @@ classes = (
|
||||
stair.EnableEditingStair,
|
||||
stair.RemoveStair,
|
||||
stair.ToggleStairTotalLengthLock,
|
||||
stair.ToggleStairCustomTreadLock,
|
||||
stair.AdjustStairTreads,
|
||||
stair.SetStairTreads,
|
||||
stair.CycleStairType,
|
||||
stair.GizmoStairEdition,
|
||||
sverchok_modifier.CreateNewSverchokGraph,
|
||||
|
||||
@@ -33,8 +33,8 @@ import bonsai.core.geometry
|
||||
import bonsai.core.geometry as core
|
||||
import bonsai.core.root
|
||||
from bonsai.bim.module.model.window import create_bm_window, create_bm_box
|
||||
from bonsai.bim import gizmo
|
||||
from bonsai.bim.gizmo import GizmoPropConfig
|
||||
from bonsai.bim.module.drawing import gizmos as gizmo
|
||||
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig
|
||||
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
@@ -786,31 +786,33 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
cancel_editing_operator = "bim.cancel_editing_door"
|
||||
cycle_type_operator = "bim.cycle_door_type"
|
||||
|
||||
gizmo_props = [
|
||||
GizmoPropConfig("overall_height", (0, 0, 1)),
|
||||
GizmoPropConfig("overall_width", (1, 0, 0)),
|
||||
GizmoPropConfig("threshold_thickness", (0, 0, 1)),
|
||||
GizmoPropConfig("threshold_depth", (0, 1, 0)),
|
||||
GizmoPropConfig("lining_depth", (0, 1, 0)),
|
||||
GizmoPropConfig("lining_thickness", (-1, 0, 0)),
|
||||
GizmoPropConfig("transom_offset", (0, 0, 1)),
|
||||
GizmoPropConfig("transom_thickness", (0, 0, 1)),
|
||||
GizmoPropConfig("casing_thickness", (-1, 0, 0)),
|
||||
GizmoPropConfig("casing_depth", (0, 1, 0)),
|
||||
dimension_gizmo_props = [
|
||||
DimensionGizmoConfig(attr_name="overall_width", axis=(1, 0, 0), min_value=0.01, text_offset_sign=-1),
|
||||
DimensionGizmoConfig(attr_name="overall_height", axis=(0, 0, 1), min_value=0.01, text_alignment="start"),
|
||||
DimensionGizmoConfig(attr_name="threshold_thickness", axis=(0, 0, 1)),
|
||||
DimensionGizmoConfig(attr_name="threshold_depth", axis=(0, 1, 0)),
|
||||
DimensionGizmoConfig(attr_name="threshold_offset", axis=(0, 1, 0)),
|
||||
DimensionGizmoConfig(attr_name="lining_offset", axis=(0, 1, 0)),
|
||||
DimensionGizmoConfig(attr_name="lining_depth", axis=(0, 1, 0)),
|
||||
DimensionGizmoConfig(attr_name="lining_thickness", axis=(-1, 0, 0)),
|
||||
DimensionGizmoConfig(attr_name="transom_offset", axis=(0, 0, 1)),
|
||||
DimensionGizmoConfig(attr_name="transom_thickness", axis=(0, 0, 1)),
|
||||
DimensionGizmoConfig(attr_name="casing_thickness", axis=(-1, 0, 0)),
|
||||
DimensionGizmoConfig(attr_name="casing_depth", axis=(0, 1, 0)),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
return tool.Blender.Modifier.is_door(element)
|
||||
|
||||
def get_props(self, obj):
|
||||
def get_props(self, obj: bpy.types.Object):
|
||||
return tool.Model.get_door_props(obj)
|
||||
|
||||
def get_gizmo_prefs(self):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return prefs.gizmos.door
|
||||
|
||||
def should_hide_gizmo(self, attr_name, props):
|
||||
def should_hide_gizmo(self, attr_name: str, props) -> bool:
|
||||
"""Door-specific visibility rules for gizmos."""
|
||||
if not props.is_editing:
|
||||
return True
|
||||
@@ -824,90 +826,100 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_gizmo_matrix_overall_height(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float:
|
||||
"""Get Y offset for icons based on view direction.
|
||||
|
||||
def get_gizmo_matrix_overall_width(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_threshold_thickness(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(
|
||||
props.overall_width / 2,
|
||||
props.threshold_offset + props.threshold_depth / 2 + props.lining_offset,
|
||||
props.threshold_thickness,
|
||||
Positions icons further than the furthest geometry:
|
||||
max(threshold_offset + threshold_depth, lining_offset + lining_depth) + 2 * GIZMO_OFFSET
|
||||
"""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return self.ICON_Y_OFFSET
|
||||
props = self.get_props(obj)
|
||||
furthest_y = (
|
||||
max(
|
||||
props.threshold_offset + props.threshold_depth,
|
||||
props.lining_offset + props.lining_depth,
|
||||
)
|
||||
+ 2 * self.GIZMO_OFFSET
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_threshold_depth(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.threshold_depth + props.lining_offset, props.threshold_thickness / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
viewing_from_negative_y, _ = self.get_local_view_direction(context, mw)
|
||||
if viewing_from_negative_y:
|
||||
return -furthest_y
|
||||
return furthest_y
|
||||
|
||||
def get_gizmo_matrix_lining_depth(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
|
||||
def get_dimension_matrix_threshold_thickness(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width / 2, props.threshold_offset + props.threshold_depth, 0), (0, 0, 1)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_lining_thickness(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width - props.lining_thickness, props.lining_depth / 2, props.overall_height / 2)
|
||||
def get_dimension_matrix_threshold_depth(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width / 2, props.threshold_offset, props.threshold_thickness), (0, 1, 0)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_transom_offset(self, props):
|
||||
# Position at the bottom of the transom (transom extends upward from offset)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.transom_offset)
|
||||
def get_dimension_matrix_threshold_offset(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width / 2 - 0.1, 0, props.threshold_thickness), (0, 1, 0)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_transom_thickness(self, props):
|
||||
# Position at the top of the transom (offset + thickness)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.transom_offset + props.transom_thickness)
|
||||
def get_dimension_matrix_lining_offset(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(V_(0, 0, 0), (0, 1, 0))
|
||||
|
||||
def get_dimension_matrix_lining_depth(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width, props.lining_offset, props.overall_height), (0, 1, 0)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_lining_thickness(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width, props.lining_depth / 2, props.overall_height / 2), (-1, 0, 0)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_transom_offset(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width / 2, props.lining_offset, 0), (0, 0, 1)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_transom_thickness(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.transom_offset), (0, 0, 1)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
@staticmethod
|
||||
def _get_casing_gizmo_base_position(props) -> tuple[float, float, float]:
|
||||
"""Get common base position for casing gizmos."""
|
||||
x = -props.casing_thickness + props.lining_thickness
|
||||
x = props.lining_thickness
|
||||
y_base = props.lining_depth + props.lining_offset
|
||||
z = props.overall_height / 2
|
||||
return x, y_base, z
|
||||
|
||||
def get_gizmo_matrix_casing_thickness(self, props):
|
||||
def get_dimension_matrix_casing_thickness(self, props) -> Matrix:
|
||||
x, y_base, z = self._get_casing_gizmo_base_position(props)
|
||||
translation = Matrix.Translation(V_(x, y_base + props.casing_depth / 2, z))
|
||||
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
|
||||
return translation @ rotation
|
||||
return self.compose_gizmo_matrix(V_(x, y_base + props.casing_depth / 2, z), (-1, 0, 0))
|
||||
|
||||
def get_gizmo_matrix_casing_depth(self, props):
|
||||
def get_dimension_matrix_casing_depth(self, props) -> Matrix:
|
||||
x, y_base, z = self._get_casing_gizmo_base_position(props)
|
||||
translation = Matrix.Translation(V_(x, y_base + props.casing_depth, z))
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
return self.compose_gizmo_matrix(V_(x - props.casing_thickness, y_base, z), (0, 1, 0))
|
||||
|
||||
def setup(self, context):
|
||||
# Use base class methods for common gizmos
|
||||
self.setup_property_gizmos(context)
|
||||
def get_dimension_matrix_overall_width(self, props) -> Matrix:
|
||||
"""Position width dimension below the door."""
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(0, props.lining_offset - self.GIZMO_OFFSET, -self.GIZMO_OFFSET), (1, 0, 0)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_overall_height(self, props) -> Matrix:
|
||||
"""Position height dimension to the side of the door."""
|
||||
casing_offset = props.casing_thickness if props.lining_offset == 0.0 else 0.0
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width + casing_offset + self.GIZMO_OFFSET, props.lining_offset - self.GIZMO_OFFSET, 0),
|
||||
(0, 0, 1),
|
||||
)
|
||||
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
self.setup_editing_gizmos(context)
|
||||
|
||||
# Door-specific: swing arc gizmos
|
||||
self.setup_dimension_gizmos(context)
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
highlight_color = prefs.decorator_color_selected[:3]
|
||||
inactive_color = prefs.decorator_color_background[:3]
|
||||
@@ -922,7 +934,6 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
op = self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
|
||||
op.flip_geometry = False
|
||||
|
||||
# Flip arc gizmo - mirrors the swing arc along X axis, flips door and toggles direction when clicked
|
||||
self.gizmo_flip_arc = self.gizmos.new("VIEW3D_GT_arc")
|
||||
self.gizmo_flip_arc.use_draw_scale = False
|
||||
self.gizmo_flip_arc.color = inactive_color
|
||||
@@ -933,40 +944,65 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
op.flip_geometry = True
|
||||
op.flip_local_axes = "XY"
|
||||
|
||||
def refresh(self, context):
|
||||
def refresh(self, context: bpy.types.Context) -> None:
|
||||
if not self.is_setup_complete():
|
||||
return
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
props = self.get_props(obj)
|
||||
mw = obj.matrix_world
|
||||
self.update_property_gizmos(mw, props)
|
||||
self.update_swing_gizmos(mw, props)
|
||||
self.update_editing_gizmos(context, mw, props)
|
||||
self.update_dimension_gizmos(mw, props)
|
||||
|
||||
def update_swing_gizmos(self, mw, props):
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: Matrix, props) -> None:
|
||||
"""Update dimension gizmo positions based on camera view direction."""
|
||||
viewing_from_negative_y, viewing_from_negative_x = self.get_local_view_direction(context, mw)
|
||||
y_pos = self.get_lining_y_position_for_view(props, viewing_from_negative_y)
|
||||
|
||||
self.set_dimension_gizmo_position("overall_width", mw, V_(0, y_pos, -self.GIZMO_OFFSET), (1, 0, 0))
|
||||
|
||||
casing_offset = props.casing_thickness if props.lining_offset == 0.0 else 0.0
|
||||
if viewing_from_negative_x:
|
||||
x_pos = -casing_offset - self.GIZMO_OFFSET
|
||||
else:
|
||||
x_pos = props.overall_width + casing_offset + self.GIZMO_OFFSET
|
||||
self.set_dimension_gizmo_position("overall_height", mw, V_(x_pos, y_pos, 0), (0, 0, 1))
|
||||
|
||||
def update_swing_gizmos(self, mw: Matrix, props) -> None:
|
||||
"""Update swing gizmo position and color based on editing state."""
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
door_gizmo_prefs = prefs.gizmos.door
|
||||
door_type_hidden_by_modal = self.is_gizmo_hidden_by_modal(self.gizmo_door_type)
|
||||
flip_arc_hidden_by_modal = self.is_gizmo_hidden_by_modal(self.gizmo_flip_arc)
|
||||
|
||||
self.gizmo_door_type.hide = not props.is_editing or not door_gizmo_prefs.swing_arc
|
||||
self.gizmo_flip_arc.hide = not props.is_editing or not door_gizmo_prefs.flip_arc
|
||||
if door_type_hidden_by_modal:
|
||||
self.gizmo_door_type.hide = True
|
||||
else:
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
door_gizmo_prefs = prefs.gizmos.door
|
||||
self.gizmo_door_type.hide = not props.is_editing or not door_gizmo_prefs.swing_arc
|
||||
|
||||
if flip_arc_hidden_by_modal:
|
||||
self.gizmo_flip_arc.hide = True
|
||||
else:
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
door_gizmo_prefs = prefs.gizmos.door
|
||||
self.gizmo_flip_arc.hide = not props.is_editing or not door_gizmo_prefs.flip_arc
|
||||
|
||||
# If both are hidden, no need to calculate transforms
|
||||
if self.gizmo_door_type.hide and self.gizmo_flip_arc.hide:
|
||||
return
|
||||
|
||||
# Calculate base swing transformation (common to both arcs)
|
||||
swing_x_offset = props.overall_width if "RIGHT" in props.door_type else 0.0
|
||||
base_swing_transform = Matrix.Translation(V_(swing_x_offset, props.lining_offset, 0)) @ Matrix.Scale(
|
||||
props.overall_width, 4
|
||||
)
|
||||
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if not self.gizmo_door_type.hide:
|
||||
self.gizmo_door_type.matrix_basis = mw @ base_swing_transform
|
||||
self.gizmo_door_type.color = prefs.decorations_colour[:3]
|
||||
|
||||
if not self.gizmo_flip_arc.hide:
|
||||
# Mirror the flip arc along Y axis to show the opposite swing direction
|
||||
mirror_y = Matrix.Scale(-1, 4, (0, 1, 0))
|
||||
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_y
|
||||
|
||||
@@ -27,9 +27,12 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.core.root
|
||||
import bonsai.tool as tool
|
||||
from bonsai.bim import gizmo
|
||||
from bonsai.bim.gizmo import GizmoPropConfig
|
||||
from bonsai.bim.module.drawing import gizmos as gizmo
|
||||
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig
|
||||
from bonsai.tool.numeric_input import IntegerInputState, run_integer_input_modal, update_header
|
||||
from mathutils import Vector, Matrix
|
||||
|
||||
V_ = tool.Blender.V_
|
||||
from bmesh.types import BMVert
|
||||
from bpy.types import Operator
|
||||
from bpy.props import FloatProperty, IntProperty
|
||||
@@ -218,6 +221,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_stair"
|
||||
bl_label = "Cancel Editing Stair"
|
||||
bl_description = "Cancel editing and revert stair parameters to their previous values"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -239,6 +243,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_stair"
|
||||
bl_label = "Finish Editing Stair"
|
||||
bl_description = "Apply changes and finish editing stair parameters"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -266,6 +271,7 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_stair"
|
||||
bl_label = "Enable Editing Stair"
|
||||
bl_description = "Enter edit mode to modify stair parameters interactively"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -317,8 +323,26 @@ class ToggleStairTotalLengthLock(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleStairCustomTreadLock(bpy.types.Operator):
|
||||
"""Toggle custom first/last tread runs. When unlocked, first and last treads can have different lengths."""
|
||||
|
||||
bl_idname = "bim.toggle_stair_custom_tread_lock"
|
||||
bl_label = "Toggle Custom Tread Lock"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
props.custom_tread_lock = not props.custom_tread_lock
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class AdjustStairTreads(bpy.types.Operator):
|
||||
"""Adjust the number of treads"""
|
||||
"""Adjust the number of treads. Shift+click to enter a specific number."""
|
||||
|
||||
bl_idname = "bim.adjust_stair_treads"
|
||||
bl_label = "Adjust Stair Treads"
|
||||
@@ -326,6 +350,12 @@ class AdjustStairTreads(bpy.types.Operator):
|
||||
|
||||
increment: IntProperty(name="Increment", default=1)
|
||||
|
||||
def invoke(self, context, event):
|
||||
if event.shift:
|
||||
bpy.ops.bim.set_stair_treads("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
@@ -339,6 +369,51 @@ class AdjustStairTreads(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class SetStairTreads(bpy.types.Operator):
|
||||
"""Set the number of treads to a specific value."""
|
||||
|
||||
bl_idname = "bim.set_stair_treads"
|
||||
bl_label = "Set Number of Treads"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def invoke(self, context, event): # noqa: ARG002
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
self._input = IntegerInputState.from_value(props.number_of_treads, min_value=1)
|
||||
self._original_value = props.number_of_treads
|
||||
|
||||
bpy.ops.ed.undo_push(message="Set Number of Treads")
|
||||
context.window_manager.modal_handler_add(self)
|
||||
update_header(context, self._format_header())
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def modal(self, context, event):
|
||||
return run_integer_input_modal(self, context, event)
|
||||
|
||||
def _apply_value(self, context) -> None:
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
value = self._input.get_value()
|
||||
if value is not None:
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
props.number_of_treads = value
|
||||
|
||||
def _restore_value(self, context) -> None:
|
||||
obj = context.active_object
|
||||
if obj:
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
props.number_of_treads = self._original_value
|
||||
|
||||
def _format_header(self) -> str:
|
||||
input_str = self._input.get_input_string()
|
||||
validity = "" if self._input.is_valid else " [must be >= 1]"
|
||||
return f"Number of Treads: {input_str}_{validity} | Enter to confirm, Esc to cancel"
|
||||
|
||||
|
||||
class CycleStairType(bpy.types.Operator):
|
||||
"""Cycle through stair types. Shift+click to cycle in reverse."""
|
||||
|
||||
@@ -366,6 +441,26 @@ class CycleStairType(bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def _compute_first_tread_run(props) -> float:
|
||||
"""Get the first custom tread run value from the tuple property."""
|
||||
return props.custom_first_last_tread_run[0]
|
||||
|
||||
|
||||
def _apply_first_tread_run(props, value: float) -> None:
|
||||
"""Apply a new first custom tread run value, preserving the second value."""
|
||||
props.custom_first_last_tread_run = (max(0.01, value), props.custom_first_last_tread_run[1])
|
||||
|
||||
|
||||
def _compute_last_tread_run(props) -> float:
|
||||
"""Get the last custom tread run value from the tuple property."""
|
||||
return props.custom_first_last_tread_run[1]
|
||||
|
||||
|
||||
def _apply_last_tread_run(props, value: float) -> None:
|
||||
"""Apply a new last custom tread run value, preserving the first value."""
|
||||
props.custom_first_last_tread_run = (props.custom_first_last_tread_run[0], max(0.01, value))
|
||||
|
||||
|
||||
class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
bl_idname = "OBJECT_GGT_bim_stair_edition"
|
||||
bl_label = "Stair Editing Gizmo"
|
||||
@@ -373,171 +468,197 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
bl_region_type = "WINDOW"
|
||||
bl_options = {"3D", "PERSISTENT"}
|
||||
|
||||
# Gizmo layout offsets (meters)
|
||||
GIZMO_X_OFFSET = 0.5 # Horizontal offset from stair end
|
||||
GIZMO_PLUS_X_OFFSET = 0.25 # Additional X offset for plus button
|
||||
GIZMO_MINUS_X_OFFSET = 0.5 # Additional X offset for minus button
|
||||
GIZMO_BUTTON_Z_OFFSET = 0.15 # Vertical offset for +/- buttons above lock
|
||||
GIZMO_CYCLE_Z_OFFSET = 0.5 # Vertical offset for cycle gizmo above lock
|
||||
# === 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
|
||||
ICON_PLUS_MINUS_SCALE = 0.24 # Scale for plus/minus icons (slightly larger)
|
||||
|
||||
enable_editing_operator = "bim.enable_editing_stair"
|
||||
finish_editing_operator = "bim.finish_editing_stair"
|
||||
cancel_editing_operator = "bim.cancel_editing_stair"
|
||||
cycle_type_operator = "bim.cycle_stair_type"
|
||||
|
||||
gizmo_props = [
|
||||
GizmoPropConfig("width", (0, 1, 0)),
|
||||
GizmoPropConfig("height", (0, 0, 1)),
|
||||
GizmoPropConfig("tread_run", (1, 0, 0)),
|
||||
GizmoPropConfig("tread_depth", (0, 0, -1)),
|
||||
GizmoPropConfig("nosing_length", (-1, 0, 0)),
|
||||
GizmoPropConfig("nosing_depth", (0, 0, -1)),
|
||||
GizmoPropConfig("total_length_target", (1, 0, 0)),
|
||||
GizmoPropConfig("base_slab_depth", (0, 0, -1)),
|
||||
GizmoPropConfig("top_slab_depth", (0, 0, -1)),
|
||||
def get_icon_y_offset(self, context, mw):
|
||||
"""Get Y offset for icons based on view direction.
|
||||
|
||||
Positions icons further than the furthest geometry:
|
||||
stair_width + 2 * GIZMO_OFFSET
|
||||
"""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return self.ICON_Y_OFFSET
|
||||
props = self.get_props(obj)
|
||||
furthest_y = props.width + 2 * self.GIZMO_OFFSET
|
||||
|
||||
viewing_from_negative_y, _ = self.get_local_view_direction(context, mw)
|
||||
if viewing_from_negative_y:
|
||||
return -furthest_y
|
||||
return furthest_y
|
||||
|
||||
dimension_gizmo_props = [
|
||||
DimensionGizmoConfig(
|
||||
attr_name="total_length_target",
|
||||
axis=(1, 0, 0),
|
||||
prop_name="Total Length",
|
||||
min_value=0.01,
|
||||
text_offset_sign=-1,
|
||||
),
|
||||
DimensionGizmoConfig(attr_name="height", axis=(0, 0, 1), min_value=0.01, text_alignment="start"),
|
||||
DimensionGizmoConfig(attr_name="width", axis=(0, 1, 0), min_value=0.01),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="tread_run",
|
||||
axis=(1, 0, 0),
|
||||
min_value=0.01,
|
||||
visibility_condition=lambda props: props.custom_tread_lock or props.number_of_treads > 2,
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="custom_first_tread_run",
|
||||
axis=(1, 0, 0),
|
||||
prop_name="First Tread",
|
||||
min_value=0.01,
|
||||
visibility_condition=lambda props: not props.custom_tread_lock,
|
||||
compute_value=_compute_first_tread_run,
|
||||
apply_value=_apply_first_tread_run,
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="custom_last_tread_run",
|
||||
axis=(1, 0, 0),
|
||||
prop_name="Last Tread",
|
||||
min_value=0.01,
|
||||
visibility_condition=lambda props: not props.custom_tread_lock,
|
||||
compute_value=_compute_last_tread_run,
|
||||
apply_value=_apply_last_tread_run,
|
||||
),
|
||||
DimensionGizmoConfig(attr_name="nosing_length", axis=(-1, 0, 0)),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="tread_depth",
|
||||
axis=(0, 0, -1),
|
||||
visibility_condition=lambda props: props.stair_type != "GENERIC",
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="riser_height",
|
||||
axis=(0, 0, 1),
|
||||
min_value=0.01,
|
||||
text_alignment="start",
|
||||
compute_value=lambda props: props.height / (props.number_of_treads + 1),
|
||||
apply_value=lambda props, value: setattr(props, "height", max(0.01, value) * (props.number_of_treads + 1)),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="nosing_depth",
|
||||
axis=(0, 0, -1),
|
||||
visibility_condition=lambda props: props.nosing_length != 0.0 and props.stair_type != "WOOD/STEEL",
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="base_slab_depth",
|
||||
axis=(0, 0, -1),
|
||||
visibility_condition=lambda props: props.stair_type == "CONCRETE",
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="top_slab_depth",
|
||||
axis=(0, 0, -1),
|
||||
visibility_condition=lambda props: props.stair_type == "CONCRETE",
|
||||
),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
return tool.Blender.Modifier.is_stair(element)
|
||||
|
||||
def get_props(self, obj):
|
||||
def get_props(self, obj: bpy.types.Object):
|
||||
return tool.Model.get_stair_props(obj)
|
||||
|
||||
def get_gizmo_prefs(self):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return prefs.gizmos.stair
|
||||
|
||||
def should_hide_gizmo(self, attr_name, props):
|
||||
"""Stair-specific visibility rules for gizmos."""
|
||||
if not props.is_editing:
|
||||
return True
|
||||
# Hide concrete-specific gizmos for non-concrete stairs
|
||||
if attr_name in ("base_slab_depth", "top_slab_depth") and props.stair_type != "CONCRETE":
|
||||
return True
|
||||
# Hide tread_depth for generic stairs (has no tread geometry)
|
||||
if attr_name == "tread_depth" and props.stair_type == "GENERIC":
|
||||
return True
|
||||
# Hide nosing_depth when nosing_length is 0 or for Wood/Steel stair types
|
||||
if attr_name == "nosing_depth" and (props.nosing_length == 0.0 or props.stair_type == "WOOD/STEEL"):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _get_stair_total_run(props) -> float:
|
||||
"""Calculate the total horizontal run of the stair."""
|
||||
return props.tread_run * props.number_of_treads
|
||||
"""Calculate the total horizontal run of the stair.
|
||||
|
||||
Takes into account custom first/last tread runs when custom_tread_lock is False.
|
||||
"""
|
||||
number_of_rises = props.number_of_treads + 1
|
||||
total_run = 0.0
|
||||
default_rises = number_of_rises
|
||||
|
||||
if not props.custom_tread_lock:
|
||||
if props.custom_first_last_tread_run[0] is not None: # May be 0 though
|
||||
default_rises -= 1
|
||||
total_run += props.custom_first_last_tread_run[0]
|
||||
if props.custom_first_last_tread_run[1] is not None: # May be 0 though
|
||||
default_rises -= 1
|
||||
total_run += props.custom_first_last_tread_run[1]
|
||||
|
||||
total_run += props.tread_run * default_rises
|
||||
return total_run
|
||||
|
||||
@staticmethod
|
||||
def _get_first_riser_height(props) -> float:
|
||||
"""Calculate the height of the first riser."""
|
||||
return props.height / (props.number_of_treads + 1)
|
||||
|
||||
def get_gizmo_matrix_width(self, props):
|
||||
translation = Matrix.Translation(Vector((0, props.width, 0)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
def get_dimension_matrix_total_length_target(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(V_(0, -self.GIZMO_OFFSET, -self.GIZMO_OFFSET), (1, 0, 0))
|
||||
|
||||
def get_gizmo_matrix_width_top(self, props):
|
||||
"""Position for the secondary width gizmo at the top of the stairs."""
|
||||
def get_dimension_matrix_height(self, props) -> Matrix:
|
||||
total_run = self._get_stair_total_run(props)
|
||||
translation = Matrix.Translation(Vector((total_run, props.width, props.height)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
return self.compose_gizmo_matrix(V_(total_run + self.GIZMO_OFFSET, -self.GIZMO_OFFSET, 0), (0, 0, 1))
|
||||
|
||||
def get_gizmo_matrix_height(self, props):
|
||||
def get_dimension_matrix_width(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(V_(self.GIZMO_OFFSET, 0, -self.GIZMO_OFFSET), (0, 1, 0))
|
||||
|
||||
def get_dimension_matrix_tread_run(self, props) -> Matrix:
|
||||
"""Position depends on custom_tread_lock state."""
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
if props.custom_tread_lock:
|
||||
x_offset = 0
|
||||
z_offset = riser_height
|
||||
else:
|
||||
x_offset = props.custom_first_last_tread_run[0]
|
||||
z_offset = riser_height * 2
|
||||
return self.compose_gizmo_matrix(V_(x_offset, 0, z_offset), (1, 0, 0))
|
||||
|
||||
def get_dimension_matrix_custom_first_tread_run(self, props) -> Matrix:
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
return self.compose_gizmo_matrix(V_(0, 0, riser_height), (1, 0, 0))
|
||||
|
||||
def get_dimension_matrix_custom_last_tread_run(self, props) -> Matrix:
|
||||
total_run = self._get_stair_total_run(props)
|
||||
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
x_offset = total_run - props.custom_first_last_tread_run[1]
|
||||
return self.compose_gizmo_matrix(V_(x_offset, 0, props.height), (1, 0, 0))
|
||||
|
||||
def get_gizmo_matrix_tread_run(self, props):
|
||||
def get_dimension_matrix_nosing_length(self, props) -> Matrix:
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
translation = Matrix.Translation(Vector((props.tread_run, props.width / 2, riser_height)))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
return self.compose_gizmo_matrix(V_(0, props.width / 2, riser_height), (-1, 0, 0))
|
||||
|
||||
def get_gizmo_matrix_tread_depth(self, props):
|
||||
def get_dimension_matrix_tread_depth(self, props) -> Matrix:
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
translation = Matrix.Translation(
|
||||
Vector((props.tread_run / 2, props.width / 2, riser_height - props.tread_depth))
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, -1))
|
||||
return translation @ rotation
|
||||
return self.compose_gizmo_matrix(V_(0, 0, riser_height), (0, 0, -1))
|
||||
|
||||
def get_gizmo_matrix_nosing_length(self, props):
|
||||
def get_dimension_matrix_riser_height(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(V_(props.tread_run, props.width, 0), (0, 0, 1))
|
||||
|
||||
def get_dimension_matrix_nosing_depth(self, props) -> Matrix:
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
translation = Matrix.Translation(Vector((-props.nosing_length, props.width / 2, riser_height)))
|
||||
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
|
||||
return translation @ rotation
|
||||
return self.compose_gizmo_matrix(V_(-props.nosing_length, props.width / 2, riser_height), (0, 0, -1))
|
||||
|
||||
def get_gizmo_matrix_nosing_depth(self, props):
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
translation = Matrix.Translation(
|
||||
Vector((-props.nosing_length, props.width / 2, riser_height - props.nosing_depth))
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, -1))
|
||||
return translation @ rotation
|
||||
def get_dimension_matrix_base_slab_depth(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(V_(0, props.width / 2, 0), (0, 0, -1))
|
||||
|
||||
def get_gizmo_matrix_total_length_target(self, props):
|
||||
translation = Matrix.Translation(Vector((props.total_length_target, props.width / 2, props.height)))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_base_slab_depth(self, props):
|
||||
translation = Matrix.Translation(Vector((0, props.width / 2, -props.base_slab_depth)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, -1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_top_slab_depth(self, props):
|
||||
def get_dimension_matrix_top_slab_depth(self, props) -> Matrix:
|
||||
total_run = self._get_stair_total_run(props)
|
||||
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height - props.top_slab_depth)))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, -1))
|
||||
return translation @ rotation
|
||||
return self.compose_gizmo_matrix(V_(total_run, props.width / 2, props.height), (0, 0, -1))
|
||||
|
||||
def setup(self, context):
|
||||
self.setup_property_gizmos(context)
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
self.setup_editing_gizmos(context)
|
||||
self.setup_dimension_gizmos(context)
|
||||
|
||||
# Stair-specific gizmos
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
highlight_color = prefs.decorator_color_selected[:3]
|
||||
|
||||
# Secondary width gizmo at the top of the stairs (linked to same width property)
|
||||
self.gizmo_width_top = self.gizmos.new("BIM_GT_gizmo_arrow_2d")
|
||||
|
||||
def make_width_get():
|
||||
def move_get():
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return 0.0
|
||||
props = self.get_props(obj)
|
||||
return props.width
|
||||
return move_get
|
||||
|
||||
def make_width_set():
|
||||
def move_set(value):
|
||||
obj = bpy.context.active_object
|
||||
if not obj:
|
||||
return
|
||||
props = self.get_props(obj)
|
||||
props.width = max(0.0, value)
|
||||
return move_set
|
||||
|
||||
self.gizmo_width_top.move_get_cb = make_width_get()
|
||||
self.gizmo_width_top.move_set_cb = make_width_set()
|
||||
self.gizmo_width_top.axis = Vector((0, 1, 0))
|
||||
self.gizmo_width_top.local_axis = Vector((0, 1, 0))
|
||||
self.gizmo_width_top.invert_delta = False
|
||||
self.gizmo_width_top.delta_scale = 1.0
|
||||
self.gizmo_width_top.prop_name = "Width"
|
||||
self.gizmo_width_top.gizmo_group = self
|
||||
self.gizmo_width_top.color = self.COLOR_GREEN
|
||||
self.gizmo_width_top.color_highlight = highlight_color
|
||||
self.gizmo_width_top.alpha = 0.99
|
||||
self.gizmo_width_top.use_draw_modal = True
|
||||
self.gizmo_width_top.use_draw_scale = True
|
||||
|
||||
# Total length lock gizmo
|
||||
self.lock_gizmo = self.gizmos.new("VIEW3D_GT_lock")
|
||||
self.lock_gizmo.use_draw_scale = False
|
||||
self.lock_gizmo.color = self.COLOR_BLUE
|
||||
@@ -546,7 +667,14 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
self.lock_gizmo.prop_path = "BIMStairProperties.total_length_lock"
|
||||
self.lock_gizmo.target_set_operator("bim.toggle_stair_total_length_lock")
|
||||
|
||||
# Plus gizmo for increasing treads
|
||||
self.tread_lock_gizmo = self.gizmos.new("VIEW3D_GT_lock")
|
||||
self.tread_lock_gizmo.use_draw_scale = False
|
||||
self.tread_lock_gizmo.color = (1.0, 1.0, 1.0)
|
||||
self.tread_lock_gizmo.color_highlight = highlight_color
|
||||
self.tread_lock_gizmo.alpha = 0.8
|
||||
self.tread_lock_gizmo.prop_path = "BIMStairProperties.custom_tread_lock"
|
||||
self.tread_lock_gizmo.target_set_operator("bim.toggle_stair_custom_tread_lock")
|
||||
|
||||
self.plus_gizmo = self.gizmos.new("VIEW3D_GT_plus")
|
||||
self.plus_gizmo.use_draw_scale = False
|
||||
self.plus_gizmo.color = self.COLOR_GREEN
|
||||
@@ -555,7 +683,6 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
op = self.plus_gizmo.target_set_operator("bim.adjust_stair_treads")
|
||||
op.increment = 1
|
||||
|
||||
# Minus gizmo for decreasing treads
|
||||
self.minus_gizmo = self.gizmos.new("VIEW3D_GT_minus")
|
||||
self.minus_gizmo.use_draw_scale = False
|
||||
self.minus_gizmo.color = self.COLOR_RED
|
||||
@@ -564,16 +691,9 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
op = self.minus_gizmo.target_set_operator("bim.adjust_stair_treads")
|
||||
op.increment = -1
|
||||
|
||||
# Cycle gizmo for stair type
|
||||
default_color = prefs.decorations_colour[:3]
|
||||
self.cycle_gizmo = self.gizmos.new("VIEW3D_GT_cycle")
|
||||
self.cycle_gizmo.use_draw_scale = False
|
||||
self.cycle_gizmo.color = default_color
|
||||
self.cycle_gizmo.color_highlight = highlight_color
|
||||
self.cycle_gizmo.alpha = 0.8
|
||||
self.cycle_gizmo.target_set_operator("bim.cycle_stair_type")
|
||||
|
||||
def refresh(self, context):
|
||||
def refresh(self, context: bpy.types.Context) -> None:
|
||||
if not self.is_setup_complete():
|
||||
return
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
@@ -581,26 +701,17 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
props = self.get_props(obj)
|
||||
mw = obj.matrix_world
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
self.update_property_gizmos(mw, props)
|
||||
self.update_editing_gizmos(context, mw, props)
|
||||
self.update_width_top_gizmo(mw, props)
|
||||
self.update_lock_gizmo(mw, props, billboard_rot)
|
||||
self.update_tread_count_gizmos(mw, props, billboard_rot)
|
||||
self.update_cycle_gizmo(mw, props, billboard_rot)
|
||||
self.update_tread_lock_gizmo(props)
|
||||
self.update_tread_count_gizmos(props)
|
||||
self.update_dimension_gizmos(mw, props)
|
||||
|
||||
def update_width_top_gizmo(self, mw, props):
|
||||
"""Update the secondary width gizmo at the top of the stairs."""
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
# Use same visibility as the main width gizmo
|
||||
self.gizmo_width_top.hide = not props.is_editing or not getattr(gizmo_prefs, "width", True)
|
||||
|
||||
if self.gizmo_width_top.hide:
|
||||
def update_lock_gizmo(self, mw: Matrix, props, billboard_rot: Matrix) -> None:
|
||||
if self.is_gizmo_hidden_by_modal(self.lock_gizmo):
|
||||
self.lock_gizmo.hide = True
|
||||
return
|
||||
|
||||
self.gizmo_width_top.matrix_basis = mw @ self.get_gizmo_matrix_width_top(props)
|
||||
self.gizmo_width_top.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
|
||||
|
||||
def update_lock_gizmo(self, mw, props, billboard_rot):
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.lock_gizmo.hide = not props.is_editing or not gizmo_prefs.lock
|
||||
|
||||
@@ -610,75 +721,146 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
self.lock_gizmo.color = self.COLOR_RED if props.total_length_lock else self.COLOR_GREEN
|
||||
|
||||
total_run = self._get_stair_total_run(props)
|
||||
lock_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
|
||||
local_transform = (
|
||||
Matrix.Translation(Vector((lock_x, props.width / 2, props.height)))
|
||||
Matrix.Translation(Vector((total_run + 0.5, -self.GIZMO_OFFSET, -self.GIZMO_OFFSET)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(0.2, 4)
|
||||
@ Matrix.Scale(self.EDITING_ICON_SCALE, 4)
|
||||
)
|
||||
self.lock_gizmo.matrix_basis = mw @ local_transform
|
||||
|
||||
def update_tread_count_gizmos(self, mw, props, billboard_rot):
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
|
||||
self.plus_gizmo.hide = not props.is_editing or not gizmo_prefs.plus
|
||||
self.minus_gizmo.hide = not props.is_editing or props.number_of_treads <= 1 or not gizmo_prefs.minus
|
||||
|
||||
if self.plus_gizmo.hide and self.minus_gizmo.hide:
|
||||
def update_tread_lock_gizmo(self, props) -> None:
|
||||
"""Update visibility of tread lock gizmo. Positioning is handled in _update_editing_icon_positions."""
|
||||
if not hasattr(self, "tread_lock_gizmo"):
|
||||
return
|
||||
|
||||
total_run = self._get_stair_total_run(props)
|
||||
base_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
|
||||
|
||||
if not self.plus_gizmo.hide:
|
||||
plus_local_transform = (
|
||||
Matrix.Translation(Vector((
|
||||
base_x + self.GIZMO_PLUS_X_OFFSET,
|
||||
props.width / 2,
|
||||
props.height + self.GIZMO_BUTTON_Z_OFFSET
|
||||
)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(0.2, 4)
|
||||
)
|
||||
self.plus_gizmo.matrix_basis = mw @ plus_local_transform
|
||||
|
||||
if not self.minus_gizmo.hide:
|
||||
minus_local_transform = (
|
||||
Matrix.Translation(Vector((
|
||||
base_x + self.GIZMO_MINUS_X_OFFSET,
|
||||
props.width / 2,
|
||||
props.height + self.GIZMO_BUTTON_Z_OFFSET
|
||||
)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(0.2, 4)
|
||||
)
|
||||
self.minus_gizmo.matrix_basis = mw @ minus_local_transform
|
||||
|
||||
def update_cycle_gizmo(self, mw, props, billboard_rot):
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.cycle_gizmo.hide = not props.is_editing or not gizmo_prefs.cycle
|
||||
|
||||
if self.cycle_gizmo.hide:
|
||||
if self.is_gizmo_hidden_by_modal(self.tread_lock_gizmo):
|
||||
self.tread_lock_gizmo.hide = True
|
||||
return
|
||||
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.tread_lock_gizmo.hide = not props.is_editing or not gizmo_prefs.lock
|
||||
|
||||
def update_tread_count_gizmos(self, props) -> None:
|
||||
"""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
|
||||
|
||||
plus_hidden_by_modal = self.is_gizmo_hidden_by_modal(self.plus_gizmo)
|
||||
minus_hidden_by_modal = self.is_gizmo_hidden_by_modal(self.minus_gizmo)
|
||||
|
||||
if plus_hidden_by_modal:
|
||||
self.plus_gizmo.hide = True
|
||||
else:
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.plus_gizmo.hide = not props.is_editing or not gizmo_prefs.plus
|
||||
|
||||
if minus_hidden_by_modal:
|
||||
self.minus_gizmo.hide = True
|
||||
else:
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.minus_gizmo.hide = not props.is_editing or props.number_of_treads <= 1 or not gizmo_prefs.minus
|
||||
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: Matrix, props) -> None:
|
||||
"""Update dimension gizmo positions based on camera view direction."""
|
||||
viewing_from_negative_y, viewing_from_negative_x = self.get_local_view_direction(context, mw)
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
total_run = self._get_stair_total_run(props)
|
||||
cycle_x = total_run + props.tread_run + self.GIZMO_X_OFFSET
|
||||
local_transform = (
|
||||
Matrix.Translation(Vector((
|
||||
cycle_x,
|
||||
props.width / 2,
|
||||
props.height + self.GIZMO_CYCLE_Z_OFFSET
|
||||
)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(0.2, 4)
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
|
||||
self._update_overall_dimension_gizmos(mw, props, viewing_from_negative_y, viewing_from_negative_x, total_run)
|
||||
self._update_tread_dimension_gizmos(mw, props, viewing_from_negative_y, total_run, riser_height)
|
||||
self._update_detail_dimension_gizmos(mw, props, viewing_from_negative_y, riser_height)
|
||||
self._update_lock_gizmo_position(mw, props, viewing_from_negative_y, billboard_rot, total_run)
|
||||
self._update_editing_icon_positions(mw, props, viewing_from_negative_y, billboard_rot)
|
||||
|
||||
def _update_overall_dimension_gizmos(
|
||||
self, mw: Matrix, props, viewing_from_negative_y: bool, viewing_from_negative_x: bool, total_run: float
|
||||
) -> None:
|
||||
"""Update overall dimension gizmos (total_length, width, height)."""
|
||||
if gizmo := self.get_dimension_gizmo_if_visible("total_length_target"):
|
||||
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=True)
|
||||
gizmo.matrix_basis = mw @ self.compose_gizmo_matrix(
|
||||
V_(0, y_pos, -self.GIZMO_OFFSET), (1, 0, 0)
|
||||
)
|
||||
|
||||
if gizmo := self.get_dimension_gizmo_if_visible("width"):
|
||||
x_pos = total_run + self.GIZMO_OFFSET if viewing_from_negative_x else -self.GIZMO_OFFSET
|
||||
gizmo.matrix_basis = mw @ self.compose_gizmo_matrix(
|
||||
V_(x_pos, 0, -self.GIZMO_OFFSET), (0, 1, 0)
|
||||
)
|
||||
|
||||
if gizmo := self.get_dimension_gizmo_if_visible("height"):
|
||||
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=True)
|
||||
gizmo.matrix_basis = mw @ self.compose_gizmo_matrix(
|
||||
V_(total_run + self.GIZMO_OFFSET, y_pos, 0), (0, 0, 1)
|
||||
)
|
||||
|
||||
def _update_tread_dimension_gizmos(
|
||||
self, mw: Matrix, props, viewing_from_negative_y: bool, total_run: float, riser_height: float
|
||||
) -> None:
|
||||
"""Update tread-related dimension gizmos (tread_run, custom first/last tread)."""
|
||||
if gizmo := self.get_dimension_gizmo_if_visible("tread_run"):
|
||||
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=False)
|
||||
if props.custom_tread_lock:
|
||||
x_offset, z_offset = 0, riser_height
|
||||
else:
|
||||
x_offset = props.custom_first_last_tread_run[0]
|
||||
z_offset = riser_height * 2
|
||||
gizmo.matrix_basis = mw @ self.compose_gizmo_matrix(
|
||||
V_(x_offset, y_pos, z_offset), (1, 0, 0)
|
||||
)
|
||||
|
||||
if gizmo := self.get_dimension_gizmo_if_visible("custom_first_tread_run"):
|
||||
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=False)
|
||||
gizmo.matrix_basis = mw @ self.compose_gizmo_matrix(
|
||||
V_(0, y_pos, riser_height), (1, 0, 0)
|
||||
)
|
||||
|
||||
if gizmo := self.get_dimension_gizmo_if_visible("custom_last_tread_run"):
|
||||
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=False)
|
||||
x_offset = total_run - props.custom_first_last_tread_run[1]
|
||||
gizmo.matrix_basis = mw @ self.compose_gizmo_matrix(
|
||||
V_(x_offset, y_pos, props.height), (1, 0, 0)
|
||||
)
|
||||
|
||||
def _update_detail_dimension_gizmos(
|
||||
self, mw: Matrix, props, viewing_from_negative_y: bool, riser_height: float
|
||||
) -> None:
|
||||
"""Update detail dimension gizmos (nosing, tread depth, riser height)."""
|
||||
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=False)
|
||||
|
||||
self.set_dimension_gizmo_position("nosing_length", mw, V_(0, props.width / 2, riser_height), (-1, 0, 0))
|
||||
self.set_dimension_gizmo_position("tread_depth", mw, V_(0, y_pos, riser_height), (0, 0, -1))
|
||||
self.set_dimension_gizmo_position("riser_height", mw, V_(props.tread_run, y_pos, 0), (0, 0, 1))
|
||||
self.set_dimension_gizmo_position("nosing_depth", mw, V_(-props.nosing_length, props.width / 2, riser_height), (0, 0, -1))
|
||||
|
||||
def _update_lock_gizmo_position(
|
||||
self, mw: Matrix, props, viewing_from_negative_y: bool, billboard_rot: Matrix, total_run: float
|
||||
) -> None:
|
||||
"""Update lock gizmo position based on Y view direction."""
|
||||
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=True)
|
||||
self.set_icon_gizmo_position(
|
||||
"lock_gizmo", mw, total_run + 0.5, y_pos, -self.GIZMO_OFFSET, billboard_rot, scale=self.EDITING_ICON_SCALE
|
||||
)
|
||||
self.cycle_gizmo.matrix_basis = mw @ local_transform
|
||||
|
||||
def draw_prepare(self, context):
|
||||
"""Called before drawing - updates gizmos to face camera."""
|
||||
# Call base class implementation
|
||||
super().draw_prepare(context)
|
||||
def _update_editing_icon_positions(self, mw, props, viewing_from_negative_y, billboard_rot):
|
||||
"""Update editing icon positions, flipping Y based on viewing angle."""
|
||||
if not props.is_editing:
|
||||
return
|
||||
|
||||
# Also update the secondary width gizmo to face camera
|
||||
if hasattr(self, "gizmo_width_top") and not self.gizmo_width_top.hide:
|
||||
self.gizmo_width_top.draw_prepare(context)
|
||||
icon_z = props.height + 0.5
|
||||
y_pos = -self.GIZMO_OFFSET if viewing_from_negative_y else props.width + self.GIZMO_OFFSET
|
||||
|
||||
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=0.3)
|
||||
self.set_icon_gizmo_position(
|
||||
"tread_lock_gizmo", mw, self.ICON_TREAD_LOCK_X, 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
|
||||
)
|
||||
self.set_icon_gizmo_position(
|
||||
"minus_gizmo", mw, self.ICON_MINUS_X, y_pos, icon_z, billboard_rot, scale=self.ICON_PLUS_MINUS_SCALE
|
||||
)
|
||||
|
||||
@@ -28,8 +28,8 @@ import ifcopenshell.api.pset
|
||||
import bonsai.tool as tool
|
||||
import bonsai.core.root
|
||||
import bonsai.core.geometry
|
||||
from bonsai.bim import gizmo
|
||||
from bonsai.bim.gizmo import GizmoPropConfig
|
||||
from bonsai.bim.module.drawing import gizmos as gizmo
|
||||
from bonsai.bim.module.drawing.gizmos import DimensionGizmoConfig
|
||||
from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS
|
||||
import ifcopenshell.api
|
||||
import ifcopenshell.api.material
|
||||
@@ -43,6 +43,44 @@ from typing import get_args
|
||||
|
||||
V_ = tool.Blender.V_
|
||||
|
||||
# Window type visibility helpers for dimension gizmos
|
||||
_MULLION_TYPES = frozenset((
|
||||
"DOUBLE_PANEL_VERTICAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
"TRIPLE_PANEL_LEFT",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_VERTICAL",
|
||||
))
|
||||
_TRANSOM_TYPES = frozenset((
|
||||
"DOUBLE_PANEL_HORIZONTAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
"TRIPLE_PANEL_LEFT",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_HORIZONTAL",
|
||||
))
|
||||
|
||||
|
||||
def _has_mullion(props) -> bool:
|
||||
"""Check if the window type uses mullions (vertical dividers)."""
|
||||
return props.window_type in _MULLION_TYPES
|
||||
|
||||
|
||||
def _has_second_mullion(props) -> bool:
|
||||
"""Check if the window type uses a second mullion."""
|
||||
return props.window_type == "TRIPLE_PANEL_VERTICAL"
|
||||
|
||||
|
||||
def _has_transom(props) -> bool:
|
||||
"""Check if the window type uses transoms (horizontal dividers)."""
|
||||
return props.window_type in _TRANSOM_TYPES
|
||||
|
||||
|
||||
def _has_second_transom(props) -> bool:
|
||||
"""Check if the window type uses a second transom."""
|
||||
return props.window_type == "TRIPLE_PANEL_HORIZONTAL"
|
||||
|
||||
|
||||
def update_window_modifier_representation(context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
@@ -481,6 +519,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.cancel_editing_window"
|
||||
bl_label = "Cancel Editing Window"
|
||||
bl_description = "Cancel editing and revert window parameters to their previous values"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -509,6 +548,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.finish_editing_window"
|
||||
bl_label = "Finish Editing Window"
|
||||
bl_description = "Apply changes and finish editing window parameters"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -541,6 +581,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class EnableEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_idname = "bim.enable_editing_window"
|
||||
bl_label = "Enable Editing Window"
|
||||
bl_description = "Enter edit mode to modify window parameters interactively"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
@@ -605,6 +646,26 @@ class CycleWindowType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def _compute_frame_depth(props) -> float:
|
||||
"""Get the first panel's frame depth value."""
|
||||
return props.frame_depth[0]
|
||||
|
||||
|
||||
def _apply_frame_depth(props, value: float) -> None:
|
||||
"""Apply a new frame depth value to the first panel, preserving other panels."""
|
||||
props.frame_depth = (max(0.0, value),) + tuple(props.frame_depth[1:])
|
||||
|
||||
|
||||
def _compute_frame_thickness(props) -> float:
|
||||
"""Get the first panel's frame thickness value."""
|
||||
return props.frame_thickness[0]
|
||||
|
||||
|
||||
def _apply_frame_thickness(props, value: float) -> None:
|
||||
"""Apply a new frame thickness value to the first panel, preserving other panels."""
|
||||
props.frame_thickness = (max(0.0, value),) + tuple(props.frame_thickness[1:])
|
||||
|
||||
|
||||
class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
bl_idname = "OBJECT_GGT_bim_window_edition"
|
||||
bl_label = "Window Editing Gizmo"
|
||||
@@ -617,190 +678,180 @@ class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
cancel_editing_operator = "bim.cancel_editing_window"
|
||||
cycle_type_operator = "bim.cycle_window_type"
|
||||
|
||||
gizmo_props = [
|
||||
GizmoPropConfig("overall_height", (0, 0, 1)),
|
||||
GizmoPropConfig("overall_width", (1, 0, 0)),
|
||||
GizmoPropConfig("lining_depth", (0, 1, 0)),
|
||||
GizmoPropConfig("lining_thickness", (1, 0, 0)),
|
||||
GizmoPropConfig("lining_to_panel_offset_x", (1, 0, 0)),
|
||||
GizmoPropConfig("lining_to_panel_offset_y", (0, 1, 0)),
|
||||
GizmoPropConfig("mullion_thickness", (1, 0, 0), delta_scale=2.0),
|
||||
GizmoPropConfig("first_mullion_offset", (1, 0, 0)),
|
||||
GizmoPropConfig("second_mullion_offset", (1, 0, 0)),
|
||||
GizmoPropConfig("transom_thickness", (0, 0, 1), delta_scale=2.0),
|
||||
GizmoPropConfig("first_transom_offset", (0, 0, 1)),
|
||||
GizmoPropConfig("second_transom_offset", (0, 0, 1)),
|
||||
dimension_gizmo_props = [
|
||||
DimensionGizmoConfig(attr_name="overall_width", axis=(1, 0, 0), min_value=0.01, text_offset_sign=-1),
|
||||
DimensionGizmoConfig(attr_name="overall_height", axis=(0, 0, 1), min_value=0.01, text_alignment="start"),
|
||||
DimensionGizmoConfig(attr_name="lining_offset", axis=(0, 1, 0)),
|
||||
DimensionGizmoConfig(attr_name="lining_depth", axis=(0, 1, 0)),
|
||||
DimensionGizmoConfig(attr_name="lining_thickness", axis=(1, 0, 0)),
|
||||
DimensionGizmoConfig(attr_name="lining_to_panel_offset_x", axis=(1, 0, 0)),
|
||||
DimensionGizmoConfig(attr_name="lining_to_panel_offset_y", axis=(0, 1, 0), min_value=-10.0),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="frame_depth",
|
||||
axis=(0, -1, 0),
|
||||
compute_value=_compute_frame_depth,
|
||||
apply_value=_apply_frame_depth,
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="frame_thickness",
|
||||
axis=(1, 0, 0),
|
||||
compute_value=_compute_frame_thickness,
|
||||
apply_value=_apply_frame_thickness,
|
||||
),
|
||||
DimensionGizmoConfig(attr_name="mullion_thickness", axis=(1, 0, 0), delta_scale=2.0, visibility_condition=_has_mullion),
|
||||
DimensionGizmoConfig(attr_name="first_mullion_offset", axis=(1, 0, 0), visibility_condition=_has_mullion),
|
||||
DimensionGizmoConfig(attr_name="second_mullion_offset", axis=(1, 0, 0), visibility_condition=_has_second_mullion),
|
||||
DimensionGizmoConfig(attr_name="transom_thickness", axis=(0, 0, 1), delta_scale=2.0, visibility_condition=_has_transom),
|
||||
DimensionGizmoConfig(attr_name="first_transom_offset", axis=(0, 0, 1), visibility_condition=_has_transom),
|
||||
DimensionGizmoConfig(attr_name="second_transom_offset", axis=(0, 0, 1), visibility_condition=_has_second_transom),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
return tool.Blender.Modifier.is_window(element)
|
||||
|
||||
def get_props(self, obj):
|
||||
def get_props(self, obj: bpy.types.Object):
|
||||
return tool.Model.get_window_props(obj)
|
||||
|
||||
def get_gizmo_prefs(self):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return prefs.gizmos.window
|
||||
|
||||
def should_hide_gizmo(self, attr_name, props):
|
||||
"""Window-specific visibility rules for gizmos."""
|
||||
if not props.is_editing:
|
||||
return True
|
||||
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float:
|
||||
"""Position icons beyond the furthest geometry extent based on view direction."""
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return self.ICON_Y_OFFSET
|
||||
props = self.get_props(obj)
|
||||
|
||||
has_mullion = self._has_mullion(props)
|
||||
has_second_mullion = self._has_second_mullion(props)
|
||||
has_transom = self._has_transom(props)
|
||||
has_second_transom = self._has_second_transom(props)
|
||||
|
||||
if attr_name == "mullion_thickness" and not has_mullion:
|
||||
return True
|
||||
if attr_name == "first_mullion_offset" and not has_mullion:
|
||||
return True
|
||||
if attr_name == "second_mullion_offset" and not has_second_mullion:
|
||||
return True
|
||||
if attr_name == "transom_thickness" and not has_transom:
|
||||
return True
|
||||
if attr_name == "first_transom_offset" and not has_transom:
|
||||
return True
|
||||
if attr_name == "second_transom_offset" and not has_second_transom:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_gizmo_matrix_overall_height(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_overall_width(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_lining_depth(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
|
||||
furthest_positive_y = (
|
||||
max(0, props.lining_offset)
|
||||
+ props.lining_depth
|
||||
+ props.lining_to_panel_offset_y
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
furthest_negative_y = min(0, props.lining_offset)
|
||||
|
||||
def get_gizmo_matrix_lining_thickness(self, props):
|
||||
translation = Matrix.Translation(
|
||||
V_(props.lining_thickness, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2)
|
||||
viewing_from_negative_y, _ = self.get_local_view_direction(context, mw)
|
||||
if viewing_from_negative_y:
|
||||
return furthest_negative_y - 2 * self.GIZMO_OFFSET
|
||||
return furthest_positive_y + 2 * self.GIZMO_OFFSET
|
||||
|
||||
def get_dimension_matrix_lining_offset(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(V_(0, 0, 0), (0, 1, 0))
|
||||
|
||||
def get_dimension_matrix_lining_depth(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.overall_height), (0, 1, 0)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_lining_thickness(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(0, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2), (1, 0, 0)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
@staticmethod
|
||||
def _get_lining_to_panel_offset_y_full(props) -> float:
|
||||
"""Get the full Y offset for lining-to-panel positioning."""
|
||||
return (props.lining_depth - props.frame_depth[0]) + props.lining_to_panel_offset_y
|
||||
|
||||
def get_gizmo_matrix_lining_to_panel_offset_x(self, props):
|
||||
def get_dimension_matrix_lining_to_panel_offset_x(self, props) -> Matrix:
|
||||
y_full = self._get_lining_to_panel_offset_y_full(props)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.lining_to_panel_offset_x, y_full + props.lining_offset, props.lining_thickness)
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(0, y_full + props.frame_depth[0] + props.lining_offset, props.lining_to_panel_offset_x), (1, 0, 0)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_lining_to_panel_offset_y(self, props):
|
||||
def get_dimension_matrix_lining_to_panel_offset_y(self, props) -> Matrix:
|
||||
y_start = props.lining_depth + props.lining_offset
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width - props.lining_to_panel_offset_x, y_start, props.lining_to_panel_offset_x),
|
||||
(0, 1, 0),
|
||||
)
|
||||
|
||||
def get_dimension_matrix_frame_depth(self, props) -> Matrix:
|
||||
y_full = self._get_lining_to_panel_offset_y_full(props)
|
||||
translation = Matrix.Translation(
|
||||
V_(
|
||||
props.lining_to_panel_offset_x,
|
||||
y_full + props.frame_depth[0] + props.lining_offset,
|
||||
props.lining_thickness,
|
||||
)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 1, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_mullion_thickness(self, props):
|
||||
# Position at the right edge of the mullion (offset + thickness/2) so gizmo movement matches property 1:1
|
||||
translation = Matrix.Translation(
|
||||
V_(props.first_mullion_offset + props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_first_mullion_offset(self, props):
|
||||
# Position at the left edge of the mullion (offset - thickness/2)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.first_mullion_offset - props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_second_mullion_offset(self, props):
|
||||
translation = Matrix.Translation(V_(props.second_mullion_offset, props.lining_offset, props.overall_height / 2))
|
||||
rotation = self.get_axis_rotation_matrix((1, 0, 0))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_transom_thickness(self, props):
|
||||
# Position at the top edge of the transom (offset + thickness/2) so gizmo movement matches property 1:1
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset + props.transom_thickness / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_first_transom_offset(self, props):
|
||||
# Position at the bottom edge of the transom (offset - thickness/2)
|
||||
translation = Matrix.Translation(
|
||||
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset - props.transom_thickness / 2)
|
||||
)
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def get_gizmo_matrix_second_transom_offset(self, props):
|
||||
translation = Matrix.Translation(V_(props.overall_width / 2, props.lining_offset, props.second_transom_offset))
|
||||
rotation = self.get_axis_rotation_matrix((0, 0, 1))
|
||||
return translation @ rotation
|
||||
|
||||
def _has_mullion(self, props):
|
||||
"""Check if the window type uses mullions (vertical dividers)."""
|
||||
window_type = props.window_type
|
||||
return window_type in (
|
||||
"DOUBLE_PANEL_VERTICAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
"TRIPLE_PANEL_LEFT",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_VERTICAL",
|
||||
y_start = y_full + props.frame_depth[0] + props.lining_offset
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.lining_to_panel_offset_x, y_start, props.lining_to_panel_offset_x), (0, -1, 0)
|
||||
)
|
||||
|
||||
def _has_second_mullion(self, props):
|
||||
"""Check if the window type uses a second mullion."""
|
||||
return props.window_type == "TRIPLE_PANEL_VERTICAL"
|
||||
|
||||
def _has_transom(self, props):
|
||||
"""Check if the window type uses transoms (horizontal dividers)."""
|
||||
window_type = props.window_type
|
||||
return window_type in (
|
||||
"DOUBLE_PANEL_HORIZONTAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
"TRIPLE_PANEL_LEFT",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_HORIZONTAL",
|
||||
def get_dimension_matrix_frame_thickness(self, props) -> Matrix:
|
||||
y_full = self._get_lining_to_panel_offset_y_full(props)
|
||||
y_pos = y_full + props.frame_depth[0] + props.lining_offset
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.lining_to_panel_offset_x, y_pos, props.overall_height / 2), (1, 0, 0)
|
||||
)
|
||||
|
||||
def _has_second_transom(self, props):
|
||||
"""Check if the window type uses a second transom."""
|
||||
return props.window_type == "TRIPLE_PANEL_HORIZONTAL"
|
||||
def get_dimension_matrix_mullion_thickness(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.first_mullion_offset - props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2),
|
||||
(1, 0, 0),
|
||||
)
|
||||
|
||||
def setup(self, context):
|
||||
# Use base class methods for common gizmos
|
||||
self.setup_property_gizmos(context)
|
||||
def get_dimension_matrix_first_mullion_offset(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(0, props.lining_offset, props.overall_height / 2), (1, 0, 0)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_second_mullion_offset(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(0, props.lining_offset, props.overall_height / 2 + 0.1), (1, 0, 0)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_transom_thickness(self, props) -> Matrix:
|
||||
# Offset X when panels are horizontal to avoid overlap with mullion gizmo
|
||||
x_pos = props.overall_width / 2 - 0.1 if _has_transom(props) else props.overall_width / 2
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(x_pos, props.lining_offset, props.first_transom_offset - props.transom_thickness / 2),
|
||||
(0, 0, 1),
|
||||
)
|
||||
|
||||
def get_dimension_matrix_first_transom_offset(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width / 2, props.lining_offset, 0), (0, 0, 1)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_second_transom_offset(self, props) -> Matrix:
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width / 2 + 0.1, props.lining_offset, 0), (0, 0, 1)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_overall_width(self, props) -> Matrix:
|
||||
"""Position width dimension below the window."""
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(0, props.lining_offset - self.GIZMO_OFFSET, -self.GIZMO_OFFSET), (1, 0, 0)
|
||||
)
|
||||
|
||||
def get_dimension_matrix_overall_height(self, props) -> Matrix:
|
||||
"""Position height dimension to the side of the window."""
|
||||
return self.compose_gizmo_matrix(
|
||||
V_(props.overall_width + self.GIZMO_OFFSET, props.lining_offset - self.GIZMO_OFFSET, 0), (0, 0, 1)
|
||||
)
|
||||
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
self.setup_editing_gizmos(context)
|
||||
self.setup_dimension_gizmos(context)
|
||||
|
||||
def refresh(self, context):
|
||||
def refresh(self, context: bpy.types.Context) -> None:
|
||||
if not self.is_setup_complete():
|
||||
return
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
|
||||
props = self.get_props(obj)
|
||||
mw = obj.matrix_world
|
||||
self.update_property_gizmos(mw, props)
|
||||
self.update_editing_gizmos(context, mw, props)
|
||||
self.update_dimension_gizmos(mw, props)
|
||||
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: Matrix, props) -> None:
|
||||
"""Update dimension gizmo positions based on camera view direction."""
|
||||
viewing_from_negative_y, viewing_from_negative_x = self.get_local_view_direction(context, mw)
|
||||
y_pos = self.get_lining_y_position_for_view(props, viewing_from_negative_y)
|
||||
|
||||
self.set_dimension_gizmo_position("overall_width", mw, V_(0, y_pos, -self.GIZMO_OFFSET), (1, 0, 0))
|
||||
|
||||
if viewing_from_negative_x:
|
||||
x_pos = -self.GIZMO_OFFSET
|
||||
else:
|
||||
x_pos = props.overall_width + self.GIZMO_OFFSET
|
||||
self.set_dimension_gizmo_position("overall_height", mw, V_(x_pos, y_pos, 0), (0, 0, 1))
|
||||
|
||||
@@ -249,6 +249,8 @@ class GizmoPreferencesDoor(bpy.types.PropertyGroup):
|
||||
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)
|
||||
@@ -263,6 +265,8 @@ class GizmoPreferencesDoor(bpy.types.PropertyGroup):
|
||||
overall_width: bool
|
||||
threshold_thickness: bool
|
||||
threshold_depth: bool
|
||||
threshold_offset: bool
|
||||
lining_offset: bool
|
||||
lining_depth: bool
|
||||
lining_thickness: bool
|
||||
transom_offset: bool
|
||||
@@ -278,10 +282,13 @@ class GizmoPreferencesWindow(bpy.types.PropertyGroup):
|
||||
|
||||
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)
|
||||
@@ -292,10 +299,13 @@ class GizmoPreferencesWindow(bpy.types.PropertyGroup):
|
||||
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
|
||||
@@ -311,6 +321,7 @@ class GizmoPreferencesStair(bpy.types.PropertyGroup):
|
||||
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)
|
||||
@@ -326,6 +337,7 @@ class GizmoPreferencesStair(bpy.types.PropertyGroup):
|
||||
height: bool
|
||||
tread_run: bool
|
||||
tread_depth: bool
|
||||
riser_height: bool
|
||||
nosing_length: bool
|
||||
nosing_depth: bool
|
||||
total_length_target: bool
|
||||
@@ -763,7 +775,8 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
from bonsai.bim.module.model.door import GizmoDoorEdition
|
||||
|
||||
door_gizmos = self.gizmos.door
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoDoorEdition.gizmo_props}
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoDoorEdition.dimension_gizmo_props}
|
||||
# Add special gizmos not in dimension_gizmo_props
|
||||
gizmo_prop_names.update(("swing_arc", "flip_arc"))
|
||||
for prop in door_gizmos.__annotations__:
|
||||
if prop in gizmo_prop_names:
|
||||
@@ -773,7 +786,7 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
from bonsai.bim.module.model.window import GizmoWindowEdition
|
||||
|
||||
window_gizmos = self.gizmos.window
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoWindowEdition.gizmo_props}
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoWindowEdition.dimension_gizmo_props}
|
||||
for prop in window_gizmos.__annotations__:
|
||||
if prop in gizmo_prop_names:
|
||||
layout.prop(window_gizmos, prop)
|
||||
@@ -782,8 +795,9 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
|
||||
from bonsai.bim.module.model.stair import GizmoStairEdition
|
||||
|
||||
stair_gizmos = self.gizmos.stair
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoStairEdition.gizmo_props}
|
||||
special_gizmo_names = {"lock", "plus", "minus"}
|
||||
gizmo_prop_names = {p.attr_name for p in GizmoStairEdition.dimension_gizmo_props}
|
||||
# Add special gizmos not in dimension_gizmo_props
|
||||
special_gizmo_names = {"lock", "plus", "minus", "cycle"}
|
||||
for prop in stair_gizmos.__annotations__:
|
||||
if prop in gizmo_prop_names or prop in special_gizmo_names:
|
||||
layout.prop(stair_gizmos, prop)
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# Bonsai - OpenBIM Blender Add-on
|
||||
# Copyright (C) 2021 Dion Moult <dion@thinkmoult.com>
|
||||
#
|
||||
# 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/>.
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Literal, Protocol
|
||||
|
||||
ModalResult = Literal["RUNNING_MODAL", "FINISHED", "CANCELLED"]
|
||||
|
||||
|
||||
class IntegerInputOperator(Protocol):
|
||||
"""Protocol for operators using IntegerInputState."""
|
||||
|
||||
_input: "IntegerInputState"
|
||||
|
||||
def _apply_value(self, context) -> None: ...
|
||||
def _restore_value(self, context) -> None: ...
|
||||
def _format_header(self) -> str: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
class IntegerInputState:
|
||||
"""State for keyboard integer input during modal operations."""
|
||||
|
||||
characters: list[str] = field(default_factory=list)
|
||||
is_valid: bool = True
|
||||
min_value: int = 1
|
||||
_user_started_typing: bool = False
|
||||
|
||||
@classmethod
|
||||
def from_value(cls, value: int, min_value: int = 1) -> "IntegerInputState":
|
||||
"""Create state initialized with an existing value."""
|
||||
return cls(
|
||||
characters=list(str(value)),
|
||||
is_valid=True,
|
||||
min_value=min_value,
|
||||
_user_started_typing=False,
|
||||
)
|
||||
|
||||
def get_input_string(self) -> str:
|
||||
return "".join(self.characters)
|
||||
|
||||
def get_value(self) -> int | None:
|
||||
"""Return parsed integer value, or None if invalid."""
|
||||
if not self.characters:
|
||||
return None
|
||||
try:
|
||||
value = int(self.get_input_string())
|
||||
return value if value >= self.min_value else None
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def handle_digit(self, digit: str) -> None:
|
||||
"""Handle a digit key press."""
|
||||
if not self._user_started_typing:
|
||||
self.characters.clear()
|
||||
self._user_started_typing = True
|
||||
self.characters.append(digit)
|
||||
self._update_validity()
|
||||
|
||||
def handle_backspace(self) -> None:
|
||||
"""Handle backspace key press."""
|
||||
self._user_started_typing = True
|
||||
if self.characters:
|
||||
self.characters.pop()
|
||||
self._update_validity()
|
||||
|
||||
def _update_validity(self) -> None:
|
||||
"""Update is_valid based on current input."""
|
||||
self.is_valid = self.get_value() is not None
|
||||
|
||||
|
||||
def handle_numeric_event(state: IntegerInputState, event) -> tuple[bool, ModalResult | None]:
|
||||
"""
|
||||
Process a Blender event for numeric input.
|
||||
|
||||
Returns:
|
||||
(handled, result):
|
||||
- handled: True if the event was consumed
|
||||
- result: "FINISHED", "CANCELLED", or None if still running
|
||||
"""
|
||||
if event.value != "PRESS":
|
||||
return False, None
|
||||
|
||||
# Digit input
|
||||
if event.ascii and event.ascii in "0123456789":
|
||||
state.handle_digit(event.ascii)
|
||||
return True, None
|
||||
|
||||
# Backspace
|
||||
if event.type == "BACK_SPACE":
|
||||
state.handle_backspace()
|
||||
return True, None
|
||||
|
||||
# Confirm
|
||||
if event.type in {"RET", "NUMPAD_ENTER"}:
|
||||
if state.is_valid:
|
||||
return True, "FINISHED"
|
||||
return True, None # Invalid - don't confirm yet
|
||||
|
||||
# Click confirm
|
||||
if event.type == "LEFTMOUSE":
|
||||
if state.characters and state.is_valid:
|
||||
return True, "FINISHED"
|
||||
return True, "FINISHED" # Empty or invalid - still finish
|
||||
|
||||
# Cancel
|
||||
if event.type in {"ESC", "RIGHTMOUSE"}:
|
||||
return True, "CANCELLED"
|
||||
|
||||
return False, None
|
||||
|
||||
|
||||
def run_integer_input_modal(op: IntegerInputOperator, context, event) -> set[str]:
|
||||
"""
|
||||
Run the modal loop for an integer input operator.
|
||||
|
||||
The operator must implement:
|
||||
- _input: IntegerInputState
|
||||
- _apply_value(context): Apply the current input value
|
||||
- _restore_value(context): Restore original value on cancel
|
||||
- _format_header(): Return the header string to display
|
||||
|
||||
Returns the Blender modal return set (e.g., {"RUNNING_MODAL"}).
|
||||
"""
|
||||
handled, result = handle_numeric_event(op._input, event)
|
||||
|
||||
if handled:
|
||||
if result == "FINISHED":
|
||||
op._apply_value(context)
|
||||
cleanup_header(context)
|
||||
return {"FINISHED"}
|
||||
elif result == "CANCELLED":
|
||||
op._restore_value(context)
|
||||
cleanup_header(context)
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
op._apply_value(context)
|
||||
update_header(context, op._format_header())
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
|
||||
def update_header(context, text: str) -> None:
|
||||
"""Update the area header text."""
|
||||
if context.area:
|
||||
context.area.header_text_set(text)
|
||||
|
||||
|
||||
def cleanup_header(context) -> None:
|
||||
"""Clear the area header text."""
|
||||
if context.area:
|
||||
context.area.header_text_set(None)
|
||||
Reference in New Issue
Block a user