mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-14 11:24:19 +00:00
Update to gizmos system
Added more gizmos for multi-panel windows and for the door transom. Support negative dimension values (lining offset for door and window) Fix railing, stair, and roof being regenerated during UI panel draw instead of on property change Various code quality changes and DRY improvements
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -161,8 +161,7 @@ classes = (
|
||||
stair.FinishEditingStair,
|
||||
stair.EnableEditingStair,
|
||||
stair.RemoveStair,
|
||||
stair.ToggleStairTotalLengthLock,
|
||||
stair.ToggleStairCustomTreadLock,
|
||||
stair.ToggleStairProperty,
|
||||
stair.AdjustStairTreads,
|
||||
stair.SetStairTreads,
|
||||
stair.CycleStairType,
|
||||
|
||||
@@ -29,7 +29,6 @@ import ifcopenshell.util.representation
|
||||
import ifcopenshell.util.schema
|
||||
import ifcopenshell.util.unit
|
||||
import bonsai.tool as tool
|
||||
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
|
||||
@@ -41,10 +40,16 @@ from mathutils import Vector, Matrix
|
||||
import json
|
||||
import collections
|
||||
import collections.abc
|
||||
from typing import get_args
|
||||
from typing import get_args, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.model.prop import BIMDoorProperties
|
||||
|
||||
V_ = tool.Blender.V_
|
||||
|
||||
# Shorthand for gizmo offset constants used in DimensionGizmoConfig lambdas
|
||||
_G = gizmo.BaseParametricGizmoGroup
|
||||
|
||||
|
||||
def update_door_modifier_representation(obj: bpy.types.Object) -> None:
|
||||
props = tool.Model.get_door_props(obj)
|
||||
@@ -142,7 +147,7 @@ def update_door_modifier_representation(obj: bpy.types.Object) -> None:
|
||||
plan_representation = ifcopenshell.api.geometry.add_door_representation(ifc_file, **representation_data)
|
||||
tool.Model.replace_object_ifc_representation(plan_annotation, obj, plan_representation)
|
||||
|
||||
bonsai.core.geometry.switch_representation(
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
@@ -512,7 +517,7 @@ class BIM_OT_add_door(bpy.types.Operator, tool.Ifc.Operator):
|
||||
element = bonsai.core.root.assign_class(
|
||||
tool.Ifc, tool.Collector, tool.Root, obj=obj, ifc_class="IfcDoor", should_add_representation=False
|
||||
)
|
||||
bonsai.core.geometry.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
core.edit_object_placement(tool.Ifc, tool.Geometry, tool.Surveyor, obj=obj)
|
||||
if tool.Ifc.get_schema() != "IFC2X3":
|
||||
element.PredefinedType = "DOOR"
|
||||
|
||||
@@ -556,7 +561,7 @@ class AddDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
)
|
||||
update_door_modifier_representation(obj)
|
||||
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
if not tool.Blender.Modifier.is_eligible_for_door_modifier(obj):
|
||||
continue
|
||||
@@ -584,7 +589,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.set_props_kwargs_from_ifc_data(data)
|
||||
|
||||
body = ifcopenshell.util.representation.get_representation(element, "Model", "Body", "MODEL_VIEW")
|
||||
bonsai.core.geometry.switch_representation(
|
||||
core.switch_representation(
|
||||
tool.Ifc,
|
||||
tool.Geometry,
|
||||
obj=obj,
|
||||
@@ -593,7 +598,7 @@ class CancelEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
|
||||
props.is_editing = False
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.cancel_editing_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -605,7 +610,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Apply changes and finish editing door parameters"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def finish_editing_door_on_object(self, obj):
|
||||
def finish_editing_door_on_object(self, obj: bpy.types.Object) -> None:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
if not tool.Blender.Modifier.is_door(element):
|
||||
@@ -630,7 +635,7 @@ class FinishEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
door_data = tool.Ifc.get().createIfcText(json.dumps(door_data, default=list))
|
||||
ifcopenshell.api.pset.edit_pset(tool.Ifc.get(), pset=pset, properties={"Data": door_data})
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.finish_editing_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -642,7 +647,7 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Enter edit mode to modify door parameters interactively"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def edit_door_on_obj(self, obj):
|
||||
def edit_door_on_obj(self, obj: bpy.types.Object) -> None:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
if not tool.Blender.Modifier.is_door(element):
|
||||
@@ -657,7 +662,7 @@ class EnableEditingDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
props.set_props_kwargs_from_ifc_data(data)
|
||||
props.is_editing = True
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.edit_door_on_obj(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -668,7 +673,7 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Remove Door on Selected Objects"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def remove_door_on_object(self, obj):
|
||||
def remove_door_on_object(self, obj: bpy.types.Object) -> None:
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
assert element
|
||||
if not tool.Blender.Modifier.is_door(element):
|
||||
@@ -679,7 +684,7 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
|
||||
pset = tool.Pset.get_element_pset(element, "BBIM_Door")
|
||||
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
for obj in tool.Blender.get_selected_objects():
|
||||
self.remove_door_on_object(obj)
|
||||
return {"FINISHED"}
|
||||
@@ -702,7 +707,7 @@ class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
name="Skip Direction Change", default=False, options={"HIDDEN", "SKIP_SAVE"}
|
||||
)
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
self.skip_direction_change = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
@@ -719,7 +724,7 @@ class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
@@ -742,36 +747,20 @@ class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CycleDoorType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
class CycleDoorType(bpy.types.Operator, tool.Ifc.Operator, gizmo.CycleTypeMixin):
|
||||
"""Cycle through available door types. Shift+click to cycle in reverse."""
|
||||
|
||||
bl_idname = "bim.cycle_door_type"
|
||||
bl_label = "Cycle Door Type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
element_checker = "is_door"
|
||||
props_getter = "get_door_props"
|
||||
type_literal = tool.Model.DoorType
|
||||
type_attr = "door_type"
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.reverse = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def _execute(self, context):
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Blender.Modifier.is_door(element):
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_door_props(obj)
|
||||
door_types = get_args(tool.Model.DoorType)
|
||||
current_index = door_types.index(props.door_type) if props.door_type in door_types else 0
|
||||
direction = -1 if self.reverse else 1
|
||||
next_index = (current_index + direction) % len(door_types)
|
||||
props.door_type = door_types[next_index]
|
||||
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
return self._cycle_type(context)
|
||||
|
||||
|
||||
class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
@@ -786,56 +775,110 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
cancel_editing_operator = "bim.cancel_editing_door"
|
||||
cycle_type_operator = "bim.cycle_door_type"
|
||||
|
||||
# Declarative dimension gizmo configuration with visibility and position
|
||||
# matrix_position lambdas replace the get_dimension_matrix_* methods
|
||||
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)),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="overall_width", axis=(1, 0, 0), min_value=0.01, text_offset_sign=-1,
|
||||
# Position set dynamically in _update_dimension_gizmo_positions based on view
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="overall_height", axis=(0, 0, 1), min_value=0.01, text_alignment="start",
|
||||
# Position set dynamically in _update_dimension_gizmo_positions based on view
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="threshold_thickness", axis=(0, 0, 1),
|
||||
matrix_position=lambda p: V_(p.overall_width / 2, p.threshold_offset + p.threshold_depth, 0),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="threshold_depth", axis=(0, 1, 0),
|
||||
visibility_condition=lambda p: p.has_threshold_depth(),
|
||||
matrix_position=lambda p: V_(p.overall_width / 2, p.threshold_offset, p.threshold_thickness),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="threshold_offset", axis=(0, 1, 0),
|
||||
matrix_position=lambda p: V_(p.overall_width / 2 - _G.GIZMO_STACK_OFFSET, 0, p.threshold_thickness),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="lining_offset", axis=(0, 1, 0), min_value=-10.0,
|
||||
# Position set dynamically in _update_dimension_gizmo_positions based on view
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="lining_depth", axis=(0, 1, 0),
|
||||
matrix_position=lambda p: V_(p.overall_width, p.lining_offset, p.overall_height),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="lining_thickness", axis=(-1, 0, 0),
|
||||
matrix_position=lambda p: V_(p.overall_width, p.lining_depth / 2, p.overall_height / 2),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="transom_offset", axis=(0, 0, 1),
|
||||
visibility_condition=lambda p: p.has_transom(),
|
||||
matrix_position=lambda p: V_(p.overall_width / 2, p.lining_offset, 0),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="transom_thickness", axis=(0, 0, 1),
|
||||
matrix_position=lambda p: V_(p.overall_width / 2, p.lining_offset, p.transom_offset),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="casing_thickness", axis=(-1, 0, 0),
|
||||
visibility_condition=lambda p: p.has_casing(),
|
||||
matrix_position=lambda p: V_(
|
||||
p.lining_thickness,
|
||||
p.lining_depth + p.lining_offset + p.casing_depth / 2,
|
||||
p.overall_height / 2
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="casing_depth", axis=(0, 1, 0),
|
||||
visibility_condition=lambda p: p.has_casing_depth(),
|
||||
matrix_position=lambda p: V_(
|
||||
p.lining_thickness - p.casing_thickness,
|
||||
p.lining_depth + p.lining_offset,
|
||||
p.overall_height / 2
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="panel_depth", axis=(0, 1, 0),
|
||||
matrix_position=lambda p: V_(
|
||||
p.lining_to_panel_offset_x + p.overall_width * p.panel_width_ratio / 2,
|
||||
p.lining_offset + p.lining_to_panel_offset_y,
|
||||
p.threshold_thickness + p.get_panel_center_z()
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="frame_thickness", axis=(-1, 0, 0),
|
||||
visibility_condition=lambda p: p.has_transom(),
|
||||
matrix_position=lambda p: V_(
|
||||
p.overall_width,
|
||||
p.lining_offset + p.lining_to_panel_offset_y + p.frame_depth / 2,
|
||||
p.get_transom_window_center_z()
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="frame_depth", axis=(0, 1, 0),
|
||||
visibility_condition=lambda p: p.has_transom(),
|
||||
matrix_position=lambda p: V_(
|
||||
p.overall_width - p.frame_thickness,
|
||||
p.lining_offset + p.lining_to_panel_offset_y,
|
||||
p.get_transom_window_center_z()
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
props_getter = "get_door_props"
|
||||
gizmo_pref_name = "door"
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
def is_element_type(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||
return tool.Blender.Modifier.is_door(element)
|
||||
|
||||
def get_props(self, obj: bpy.types.Object):
|
||||
return tool.Model.get_door_props(obj)
|
||||
def get_icon_y_extent(self, props: "BIMDoorProperties") -> tuple[float, float]:
|
||||
"""Get Y extents for door icon positioning.
|
||||
|
||||
def get_gizmo_prefs(self):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return prefs.gizmos.door
|
||||
|
||||
def should_hide_gizmo(self, attr_name: str, props) -> bool:
|
||||
"""Door-specific visibility rules for gizmos."""
|
||||
if not props.is_editing:
|
||||
return True
|
||||
if attr_name == "threshold_depth" and props.threshold_thickness == 0.0:
|
||||
return True
|
||||
if attr_name == "transom_offset" and props.transom_thickness == 0.0:
|
||||
return True
|
||||
if attr_name == "casing_thickness" and props.lining_offset != 0.0:
|
||||
return True
|
||||
if attr_name == "casing_depth" and (props.lining_offset != 0.0 or props.casing_thickness == 0.0):
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_icon_y_offset(self, context: bpy.types.Context, mw: Matrix) -> float:
|
||||
"""Get Y offset for icons based on view direction.
|
||||
|
||||
Positions icons further than the furthest geometry:
|
||||
max(threshold_offset + threshold_depth, lining_offset + lining_depth) + 2 * GIZMO_OFFSET
|
||||
Door geometry extends in +Y direction from lining/threshold.
|
||||
Icons are positioned beyond max(threshold_offset + depth, lining_offset + depth).
|
||||
"""
|
||||
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,
|
||||
@@ -843,154 +886,53 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
)
|
||||
+ 2 * self.GIZMO_OFFSET
|
||||
)
|
||||
return (furthest_y, furthest_y)
|
||||
|
||||
viewing_from_negative_y, _ = self.get_local_view_direction(context, mw)
|
||||
if viewing_from_negative_y:
|
||||
return -furthest_y
|
||||
return furthest_y
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _get_casing_gizmo_base_position(props) -> tuple[float, float, float]:
|
||||
"""Get common base position for casing gizmos."""
|
||||
x = props.lining_thickness
|
||||
y_base = props.lining_depth + props.lining_offset
|
||||
z = props.overall_height / 2
|
||||
return x, y_base, z
|
||||
|
||||
def get_dimension_matrix_casing_thickness(self, props) -> Matrix:
|
||||
x, y_base, z = self._get_casing_gizmo_base_position(props)
|
||||
return self.compose_gizmo_matrix(V_(x, y_base + props.casing_depth / 2, z), (-1, 0, 0))
|
||||
|
||||
def get_dimension_matrix_casing_depth(self, props) -> Matrix:
|
||||
x, y_base, z = self._get_casing_gizmo_base_position(props)
|
||||
return self.compose_gizmo_matrix(V_(x - props.casing_thickness, y_base, z), (0, 1, 0))
|
||||
|
||||
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)
|
||||
self.setup_dimension_gizmos(context)
|
||||
def setup_element_specific_gizmos(self, context: bpy.types.Context) -> None:
|
||||
"""Create door-specific swing arc gizmos."""
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
highlight_color = prefs.decorator_color_selected[:3]
|
||||
inactive_color = prefs.decorator_color_background[:3]
|
||||
special_color = prefs.decorator_color_special[:3]
|
||||
|
||||
self.gizmo_door_type = self.gizmos.new("VIEW3D_GT_arc")
|
||||
self.gizmo_door_type.use_draw_scale = False
|
||||
self.gizmo_door_type.color = special_color
|
||||
self.gizmo_door_type.alpha = 0.5
|
||||
self.gizmo_door_type.color_highlight = highlight_color
|
||||
self.gizmo_door_type.prop_path = "BIMDoorProperties.door_type"
|
||||
op = self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
|
||||
op.flip_geometry = False
|
||||
self.gizmo_door_type = self.create_arc_gizmo(
|
||||
special_color, "bim.toggle_door_swing",
|
||||
prop_path="BIMDoorProperties.door_type",
|
||||
flip_geometry=False,
|
||||
)
|
||||
self.gizmo_flip_arc = self.create_arc_gizmo(
|
||||
inactive_color, "bim.toggle_door_swing",
|
||||
prop_path="BIMDoorProperties.door_type",
|
||||
flip_geometry=True,
|
||||
flip_local_axes="XY",
|
||||
)
|
||||
|
||||
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
|
||||
self.gizmo_flip_arc.alpha = 0.5
|
||||
self.gizmo_flip_arc.color_highlight = highlight_color
|
||||
self.gizmo_flip_arc.prop_path = "BIMDoorProperties.door_type"
|
||||
op = self.gizmo_flip_arc.target_set_operator("bim.toggle_door_swing")
|
||||
op.flip_geometry = True
|
||||
op.flip_local_axes = "XY"
|
||||
|
||||
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
|
||||
def _refresh_element_specific(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties" # noqa: ARG002
|
||||
) -> None:
|
||||
"""Update door-specific swing arc gizmos."""
|
||||
self.update_swing_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:
|
||||
def get_casing_offset(self, props: "BIMDoorProperties") -> float:
|
||||
"""Override to return casing_thickness when lining_offset is 0."""
|
||||
return props.get_casing_offset()
|
||||
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties") -> 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._update_view_dependent_dimensions(context, mw, props)
|
||||
|
||||
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:
|
||||
def update_swing_gizmos(self, mw: Matrix, props: "BIMDoorProperties") -> None:
|
||||
"""Update swing gizmo position and color based on editing state."""
|
||||
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)
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
door_gizmo_prefs = prefs.gizmos.door
|
||||
|
||||
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
|
||||
door_type_visible = self.update_gizmo_visibility(
|
||||
self.gizmo_door_type, props.is_editing, door_gizmo_prefs.swing_arc
|
||||
)
|
||||
flip_arc_visible = self.update_gizmo_visibility(
|
||||
self.gizmo_flip_arc, props.is_editing, door_gizmo_prefs.flip_arc
|
||||
)
|
||||
|
||||
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 self.gizmo_door_type.hide and self.gizmo_flip_arc.hide:
|
||||
if not door_type_visible and not flip_arc_visible:
|
||||
return
|
||||
|
||||
swing_x_offset = props.overall_width if "RIGHT" in props.door_type else 0.0
|
||||
@@ -998,11 +940,10 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
props.overall_width, 4
|
||||
)
|
||||
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
if not self.gizmo_door_type.hide:
|
||||
if door_type_visible:
|
||||
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:
|
||||
if flip_arc_visible:
|
||||
mirror_y = Matrix.Scale(-1, 4, (0, 1, 0))
|
||||
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_y
|
||||
|
||||
@@ -29,7 +29,8 @@ from bonsai.bim.module.model.decorator import WallAxisDecorator, SlabDirectionDe
|
||||
from bonsai.bim.module.model.door import update_door_modifier_bmesh
|
||||
from bonsai.bim.module.model.window import update_window_modifier_bmesh
|
||||
from bonsai.bim.module.drawing.decoration import CutDecorator
|
||||
from typing import TYPE_CHECKING, Literal, get_args, Union, get_args, Any, Optional
|
||||
from typing import TYPE_CHECKING, Literal, get_args, Union, Any, Optional, Callable
|
||||
from mathutils import Vector
|
||||
|
||||
|
||||
def get_ifc_class(self: "BIMModelProperties", context: bpy.types.Context) -> list[tuple[str, str, str]]:
|
||||
@@ -152,6 +153,53 @@ def update_window(self: "BIMWindowProperties", context: bpy.types.Context) -> No
|
||||
update_window_modifier_bmesh(context)
|
||||
|
||||
|
||||
# Lazy-loaded module references for parametric element updates
|
||||
# Using module-level lazy loading avoids circular imports and repeated import overhead
|
||||
_updater_cache: dict[str, Callable] = {}
|
||||
|
||||
|
||||
def _get_updater(module_name: str, func_name: str) -> Callable:
|
||||
"""Lazy-load an updater function to avoid circular imports.
|
||||
|
||||
Args:
|
||||
module_name: Module name within bonsai.bim.module.model (e.g., "stair")
|
||||
func_name: Function name to import (e.g., "regenerate_stair_mesh")
|
||||
|
||||
Returns:
|
||||
The imported function, cached for subsequent calls.
|
||||
"""
|
||||
cache_key = f"{module_name}.{func_name}"
|
||||
if cache_key not in _updater_cache:
|
||||
import importlib
|
||||
module = importlib.import_module(f"bonsai.bim.module.model.{module_name}")
|
||||
_updater_cache[cache_key] = getattr(module, func_name)
|
||||
return _updater_cache[cache_key]
|
||||
|
||||
|
||||
def update_stair(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Regenerate stair mesh when property changes."""
|
||||
obj = context.active_object
|
||||
if obj and self.is_editing:
|
||||
_get_updater("stair", "regenerate_stair_mesh")(obj)
|
||||
|
||||
|
||||
def update_railing(self: "BIMRailingProperties", context: bpy.types.Context) -> None:
|
||||
"""Regenerate railing mesh when property changes."""
|
||||
if self.is_editing:
|
||||
# Only FRAMELESS_PANEL can update live via bmesh.
|
||||
# WALL_MOUNTED_HANDRAIL geometry is generated from IFC representation,
|
||||
# so it only updates on "Finish Editing" to avoid modifying IFC during preview.
|
||||
if self.railing_type == "FRAMELESS_PANEL":
|
||||
_get_updater("railing", "update_railing_modifier_bmesh")(context)
|
||||
|
||||
|
||||
def update_roof(self: "BIMRoofProperties", context: bpy.types.Context) -> None:
|
||||
"""Regenerate roof mesh when property changes."""
|
||||
obj = context.active_object
|
||||
if obj and self.is_editing:
|
||||
_get_updater("roof", "update_roof_modifier_bmesh")(obj)
|
||||
|
||||
|
||||
class BIMModelProperties(PropertyGroup):
|
||||
ifc_class: bpy.props.EnumProperty(items=get_ifc_class, name="Construction Class", update=update_ifc_class)
|
||||
relating_type_id: bpy.props.EnumProperty(
|
||||
@@ -353,153 +401,65 @@ class BIMArrayProperties(PropertyGroup):
|
||||
|
||||
|
||||
def update_total_length_target(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update tread_run when total_length_target changes"""
|
||||
# Calculate available length for default treads
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1 # number of risers
|
||||
|
||||
# Subtract custom first tread if not locked and not zero
|
||||
if not self.custom_tread_lock and self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
|
||||
# Subtract custom last tread if not locked and not zero
|
||||
if not self.custom_tread_lock and self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
# Calculate tread_run for remaining treads
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
# All treads are custom, just use target length
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
"""Update tread_run when total_length_target changes."""
|
||||
self.update_tread_run_from_length()
|
||||
# Must call update_stair here because self["prop"] bypasses property update callbacks
|
||||
update_stair(self, context)
|
||||
|
||||
|
||||
def update_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update either number_of_treads or total_length_target when tread_run changes"""
|
||||
"""Update either number_of_treads or total_length_target when tread_run changes."""
|
||||
if self.total_length_lock:
|
||||
# Calculate how much length custom treads take up
|
||||
custom_length = 0
|
||||
n_custom_treads = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[0]
|
||||
n_custom_treads += 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[1]
|
||||
n_custom_treads += 1
|
||||
|
||||
# Calculate how many default treads fit in remaining space
|
||||
custom_length, custom_count = self.get_custom_tread_info()
|
||||
available_length = self.total_length_target - custom_length
|
||||
if self.tread_run > 0:
|
||||
n_default_treads = available_length / self.tread_run
|
||||
total_treads = n_default_treads + n_custom_treads
|
||||
# number_of_treads = number_of_risers - 1
|
||||
self["number_of_treads"] = int(total_treads - 1)
|
||||
self["number_of_treads"] = int(n_default_treads + custom_count - 1)
|
||||
else:
|
||||
# Calculate total length from current settings
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
self.update_total_length_from_treads()
|
||||
# Must call update_stair here because self["prop"] bypasses property update callbacks
|
||||
update_stair(self, context)
|
||||
|
||||
|
||||
def update_number_of_treads(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update either tread_run or total_length_target when number_of_treads changes"""
|
||||
"""Update either tread_run or total_length_target when number_of_treads changes."""
|
||||
if self.total_length_lock:
|
||||
# Calculate available length for default treads
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
self.update_tread_run_from_length()
|
||||
else:
|
||||
# Calculate total length from current settings
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
self.update_total_length_from_treads()
|
||||
# Must call update_stair here because self["prop"] bypasses property update callbacks
|
||||
update_stair(self, context)
|
||||
|
||||
|
||||
def update_custom_first_last_tread_run(self: "BIMStairProperties", context: bpy.types.Context) -> None:
|
||||
"""Update tread_run or total_length when custom treads change"""
|
||||
"""Update tread_run or total_length when custom treads change."""
|
||||
if self.total_length_lock:
|
||||
# Recalculate tread_run to maintain total length
|
||||
available_length = self.total_length_target
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
available_length -= self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
self.update_tread_run_from_length()
|
||||
else:
|
||||
# Recalculate total length
|
||||
n_default_treads = self.number_of_treads + 1
|
||||
total_length = 0
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
total_length += self.custom_first_last_tread_run[0]
|
||||
n_default_treads -= 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
total_length += self.custom_first_last_tread_run[1]
|
||||
n_default_treads -= 1
|
||||
|
||||
total_length += n_default_treads * self.tread_run
|
||||
self["total_length_target"] = total_length
|
||||
self.update_total_length_from_treads()
|
||||
# Must call update_stair here because self["prop"] bypasses property update callbacks
|
||||
update_stair(self, context)
|
||||
|
||||
|
||||
class BIMStairProperties(PropertyGroup):
|
||||
def validate_nosing_value(self, context: bpy.types.Context) -> None:
|
||||
if self.stair_type != "WOOD/STEEL" and self.nosing_length < 0:
|
||||
self["nosing_length"] = 0
|
||||
update_stair(self, context)
|
||||
|
||||
def update_custom_tread_lock(self, context: bpy.types.Context) -> None:
|
||||
"""When lock is enabled, sync custom treads with tread_run"""
|
||||
if self.custom_tread_lock:
|
||||
self["custom_first_last_tread_run"] = (self.tread_run, self.tread_run)
|
||||
update_stair(self, context)
|
||||
|
||||
non_si_units_props = ("is_editing", "number_of_treads", "has_top_nib", "stair_type", "custom_tread_lock")
|
||||
|
||||
is_editing: bpy.props.BoolProperty(default=False)
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, min=0.01, subtype="DISTANCE")
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, min=0.01, subtype="DISTANCE")
|
||||
width: bpy.props.FloatProperty(name="Width", default=1.2, min=0.01, subtype="DISTANCE", update=update_stair)
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, min=0.01, subtype="DISTANCE", update=update_stair)
|
||||
number_of_treads: bpy.props.IntProperty(
|
||||
name="Number of Treads", default=6, soft_min=1, min=0, update=update_number_of_treads
|
||||
)
|
||||
@@ -516,13 +476,13 @@ class BIMStairProperties(PropertyGroup):
|
||||
name="Lock Total Length",
|
||||
description="Lock Total Length when changing number of treads or tread run",
|
||||
)
|
||||
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, min=0.01, subtype="DISTANCE")
|
||||
tread_depth: bpy.props.FloatProperty(name="Tread Depth", default=0.25, min=0.01, subtype="DISTANCE", update=update_stair)
|
||||
tread_run: bpy.props.FloatProperty(
|
||||
name="Tread Run", default=0.3, min=0.01, subtype="DISTANCE", update=update_tread_run
|
||||
)
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, min=0, subtype="DISTANCE")
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, min=0, subtype="DISTANCE")
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True)
|
||||
base_slab_depth: bpy.props.FloatProperty(name="Base Slab Depth", default=0.25, min=0, subtype="DISTANCE", update=update_stair)
|
||||
top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, min=0, subtype="DISTANCE", update=update_stair)
|
||||
has_top_nib: bpy.props.BoolProperty(name="Has Top Nib", default=True, update=update_stair)
|
||||
stair_type: bpy.props.EnumProperty(
|
||||
name="Stair Type",
|
||||
items=[(i, i.replace("/", " / ").title(), "") for i in get_args(tool.Model.StairType)],
|
||||
@@ -555,7 +515,7 @@ class BIMStairProperties(PropertyGroup):
|
||||
update=validate_nosing_value,
|
||||
)
|
||||
nosing_depth: bpy.props.FloatProperty(
|
||||
name="Nosing Depth", description="Depth of the tread's nosing", min=0, default=0, unit="LENGTH"
|
||||
name="Nosing Depth", description="Depth of the tread's nosing", min=0, default=0, unit="LENGTH", update=update_stair
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -659,6 +619,100 @@ class BIMStairProperties(PropertyGroup):
|
||||
continue
|
||||
setattr(target_props, prop_name, prop_value)
|
||||
|
||||
def is_concrete_stair(self) -> bool:
|
||||
return self.stair_type == "CONCRETE"
|
||||
|
||||
def has_nosing(self) -> bool:
|
||||
return self.nosing_length != 0.0 and self.stair_type != "WOOD/STEEL"
|
||||
|
||||
def has_custom_treads(self) -> bool:
|
||||
return not self.custom_tread_lock
|
||||
|
||||
def has_tread_run_gizmo(self) -> bool:
|
||||
return self.custom_tread_lock or self.number_of_treads > 2
|
||||
|
||||
def has_tread_depth(self) -> bool:
|
||||
return self.stair_type != "GENERIC"
|
||||
|
||||
def get_riser_height(self) -> float:
|
||||
"""Compute the riser height from total height and number of treads."""
|
||||
return self.height / (self.number_of_treads + 1)
|
||||
|
||||
def set_riser_height(self, value: float) -> None:
|
||||
"""Apply riser height by adjusting the total stair height."""
|
||||
self.height = max(0.01, value) * (self.number_of_treads + 1)
|
||||
|
||||
def get_total_run(self) -> float:
|
||||
"""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 = self.number_of_treads + 1
|
||||
total_run = 0.0
|
||||
default_rises = number_of_rises
|
||||
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] is not None: # May be 0 though
|
||||
default_rises -= 1
|
||||
total_run += self.custom_first_last_tread_run[0]
|
||||
if self.custom_first_last_tread_run[1] is not None: # May be 0 though
|
||||
default_rises -= 1
|
||||
total_run += self.custom_first_last_tread_run[1]
|
||||
|
||||
total_run += self.tread_run * default_rises
|
||||
return total_run
|
||||
|
||||
def get_custom_tread_run(self, index: int) -> float:
|
||||
"""Get custom tread run value for first (0) or last (1) tread."""
|
||||
return self.custom_first_last_tread_run[index]
|
||||
|
||||
def set_custom_tread_run(self, index: int, value: float) -> None:
|
||||
"""Set custom tread run value for first (0) or last (1) tread."""
|
||||
current = self.custom_first_last_tread_run
|
||||
if index == 0:
|
||||
self.custom_first_last_tread_run = (max(0.01, value), current[1])
|
||||
else:
|
||||
self.custom_first_last_tread_run = (current[0], max(0.01, value))
|
||||
|
||||
def get_custom_tread_info(self) -> tuple[float, int]:
|
||||
"""Calculate total custom tread length and count.
|
||||
|
||||
Returns:
|
||||
Tuple of (custom_length, custom_count)
|
||||
"""
|
||||
custom_length = 0.0
|
||||
custom_count = 0
|
||||
if not self.custom_tread_lock:
|
||||
if self.custom_first_last_tread_run[0] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[0]
|
||||
custom_count += 1
|
||||
if self.custom_first_last_tread_run[1] != 0:
|
||||
custom_length += self.custom_first_last_tread_run[1]
|
||||
custom_count += 1
|
||||
return custom_length, custom_count
|
||||
|
||||
def calculate_total_length(self) -> float:
|
||||
"""Calculate total stair run length from current properties."""
|
||||
custom_length, custom_count = self.get_custom_tread_info()
|
||||
n_default_treads = self.number_of_treads + 1 - custom_count
|
||||
return custom_length + n_default_treads * self.tread_run
|
||||
|
||||
def update_tread_run_from_length(self) -> None:
|
||||
"""Recalculate tread_run to maintain total_length_target."""
|
||||
custom_length, custom_count = self.get_custom_tread_info()
|
||||
available_length = self.total_length_target - custom_length
|
||||
n_default_treads = self.number_of_treads + 1 - custom_count
|
||||
|
||||
if n_default_treads > 0:
|
||||
self["tread_run"] = available_length / n_default_treads
|
||||
else:
|
||||
# All treads are custom, use fallback
|
||||
self["tread_run"] = self.total_length_target / (self.number_of_treads + 1)
|
||||
|
||||
def update_total_length_from_treads(self) -> None:
|
||||
"""Recalculate total_length_target from current tread settings."""
|
||||
self["total_length_target"] = self.calculate_total_length()
|
||||
|
||||
|
||||
class BIMSverchokProperties(PropertyGroup):
|
||||
node_group: bpy.props.PointerProperty(name="Node Group", type=NodeTree)
|
||||
@@ -875,6 +929,167 @@ class BIMWindowProperties(PropertyGroup):
|
||||
if prop_name not in exclude_props:
|
||||
setattr(target_props, prop_name, prop_value)
|
||||
|
||||
# Window type feature mapping - centralized configuration for all window type checks
|
||||
# Each window type maps to its features: mullion, second_mullion, transom, second_transom, panels
|
||||
WINDOW_TYPE_FEATURES: dict[str, dict[str, bool | int]] = {
|
||||
"SINGLE_PANEL": {"panels": 1},
|
||||
"DOUBLE_PANEL_VERTICAL": {"mullion": True, "panels": 2},
|
||||
"DOUBLE_PANEL_HORIZONTAL": {"transom": True, "panels": 2},
|
||||
"TRIPLE_PANEL_BOTTOM": {"mullion": True, "transom": True, "panels": 3},
|
||||
"TRIPLE_PANEL_TOP": {"mullion": True, "transom": True, "panels": 3},
|
||||
"TRIPLE_PANEL_LEFT": {"mullion": True, "transom": True, "panels": 3},
|
||||
"TRIPLE_PANEL_RIGHT": {"mullion": True, "transom": True, "panels": 3},
|
||||
"TRIPLE_PANEL_HORIZONTAL": {"transom": True, "second_transom": True, "panels": 3},
|
||||
"TRIPLE_PANEL_VERTICAL": {"mullion": True, "second_mullion": True, "panels": 3},
|
||||
}
|
||||
|
||||
def _get_feature(self, feature: str, default: bool | int = False) -> bool | int:
|
||||
return self.WINDOW_TYPE_FEATURES.get(self.window_type, {}).get(feature, default)
|
||||
|
||||
def has_mullion(self) -> bool:
|
||||
return bool(self._get_feature("mullion"))
|
||||
|
||||
def has_second_mullion(self) -> bool:
|
||||
return bool(self._get_feature("second_mullion"))
|
||||
|
||||
def has_transom(self) -> bool:
|
||||
return bool(self._get_feature("transom"))
|
||||
|
||||
def has_second_transom(self) -> bool:
|
||||
return bool(self._get_feature("second_transom"))
|
||||
|
||||
def has_second_panel(self) -> bool:
|
||||
return int(self._get_feature("panels", 1)) >= 2
|
||||
|
||||
def has_third_panel(self) -> bool:
|
||||
return int(self._get_feature("panels", 1)) >= 3
|
||||
|
||||
def get_lining_to_panel_offset_y_full(self) -> float:
|
||||
"""Get the full Y offset for lining-to-panel positioning."""
|
||||
return (self.lining_depth - self.frame_depth[0]) + self.lining_to_panel_offset_y
|
||||
|
||||
def get_panel_geometry(self, panel_index: int) -> tuple[float, float, float, float]:
|
||||
"""Get panel geometry (x_offset, z_offset, height, center_z) for a given panel index.
|
||||
|
||||
Args:
|
||||
panel_index: 0 for first panel, 1 for second, 2 for third
|
||||
|
||||
Returns:
|
||||
Tuple of (x_offset, z_offset, height, center_z)
|
||||
"""
|
||||
window_type = self.window_type
|
||||
|
||||
if panel_index == 0:
|
||||
# First panel position and height
|
||||
if window_type == "DOUBLE_PANEL_HORIZONTAL":
|
||||
x, z = 0, self.first_transom_offset
|
||||
elif window_type == "TRIPLE_PANEL_HORIZONTAL":
|
||||
x, z = 0, self.second_transom_offset
|
||||
elif window_type in ("TRIPLE_PANEL_BOTTOM", "TRIPLE_PANEL_RIGHT", "TRIPLE_PANEL_TOP"):
|
||||
x, z = 0, self.first_transom_offset
|
||||
else:
|
||||
x, z = 0, 0
|
||||
|
||||
if window_type == "TRIPLE_PANEL_HORIZONTAL":
|
||||
height = self.overall_height - self.second_transom_offset
|
||||
elif window_type in (
|
||||
"DOUBLE_PANEL_HORIZONTAL",
|
||||
"TRIPLE_PANEL_BOTTOM",
|
||||
"TRIPLE_PANEL_RIGHT",
|
||||
"TRIPLE_PANEL_TOP",
|
||||
):
|
||||
height = self.overall_height - self.first_transom_offset
|
||||
else:
|
||||
height = self.overall_height
|
||||
|
||||
elif panel_index == 1:
|
||||
# Second panel position and height
|
||||
if window_type == "DOUBLE_PANEL_VERTICAL":
|
||||
x, z = self.first_mullion_offset, 0
|
||||
elif window_type == "DOUBLE_PANEL_HORIZONTAL":
|
||||
x, z = 0, 0
|
||||
elif window_type in ("TRIPLE_PANEL_BOTTOM", "TRIPLE_PANEL_LEFT", "TRIPLE_PANEL_RIGHT"):
|
||||
x, z = self.first_mullion_offset, self.first_transom_offset
|
||||
elif window_type == "TRIPLE_PANEL_TOP":
|
||||
x, z = 0, 0
|
||||
elif window_type == "TRIPLE_PANEL_HORIZONTAL":
|
||||
x, z = 0, self.first_transom_offset
|
||||
elif window_type == "TRIPLE_PANEL_VERTICAL":
|
||||
x, z = self.first_mullion_offset, 0
|
||||
else:
|
||||
x, z = 0, 0
|
||||
|
||||
if window_type == "DOUBLE_PANEL_HORIZONTAL":
|
||||
height = self.first_transom_offset
|
||||
elif window_type == "DOUBLE_PANEL_VERTICAL":
|
||||
height = self.overall_height
|
||||
elif window_type in ("TRIPLE_PANEL_BOTTOM", "TRIPLE_PANEL_LEFT", "TRIPLE_PANEL_RIGHT"):
|
||||
height = self.overall_height - self.first_transom_offset
|
||||
elif window_type == "TRIPLE_PANEL_TOP":
|
||||
height = self.first_transom_offset
|
||||
elif window_type == "TRIPLE_PANEL_HORIZONTAL":
|
||||
height = self.second_transom_offset - self.first_transom_offset
|
||||
elif window_type == "TRIPLE_PANEL_VERTICAL":
|
||||
height = self.overall_height
|
||||
else:
|
||||
height = self.overall_height
|
||||
|
||||
else: # panel_index == 2
|
||||
# Third panel position and height
|
||||
if window_type in ("TRIPLE_PANEL_BOTTOM", "TRIPLE_PANEL_RIGHT", "TRIPLE_PANEL_HORIZONTAL"):
|
||||
x, z = 0, 0
|
||||
elif window_type in ("TRIPLE_PANEL_TOP", "TRIPLE_PANEL_LEFT"):
|
||||
x, z = self.first_mullion_offset, 0
|
||||
elif window_type == "TRIPLE_PANEL_VERTICAL":
|
||||
x, z = self.second_mullion_offset, 0
|
||||
else:
|
||||
x, z = 0, 0
|
||||
|
||||
height = self.overall_height if window_type == "TRIPLE_PANEL_VERTICAL" else self.first_transom_offset
|
||||
|
||||
center_z = z + height / 2
|
||||
return x, z, height, center_z
|
||||
|
||||
def get_frame_position(self, panel_index: int, is_depth: bool) -> Vector:
|
||||
"""Get frame gizmo position for a given panel.
|
||||
|
||||
Args:
|
||||
panel_index: 0 for first panel, 1 for second, 2 for third
|
||||
is_depth: True for depth gizmo, False for thickness gizmo
|
||||
|
||||
Returns:
|
||||
Position vector for the gizmo
|
||||
"""
|
||||
x_offset, _, _, center_z = self.get_panel_geometry(panel_index)
|
||||
frame_depth = self.frame_depth[panel_index]
|
||||
y_full = (self.lining_depth - frame_depth) + self.lining_to_panel_offset_y
|
||||
y_pos = y_full + frame_depth + self.lining_offset
|
||||
return Vector((x_offset + self.lining_to_panel_offset_x, y_pos, center_z))
|
||||
|
||||
def get_frame_value(self, attr_name: str, panel_index: int) -> float:
|
||||
"""Get frame property value (frame_depth or frame_thickness) for a specific panel.
|
||||
|
||||
Args:
|
||||
attr_name: Property name ("frame_depth" or "frame_thickness")
|
||||
panel_index: Panel index (0, 1, or 2)
|
||||
|
||||
Returns:
|
||||
The value at the specified panel index
|
||||
"""
|
||||
return getattr(self, attr_name)[panel_index]
|
||||
|
||||
def set_frame_value(self, attr_name: str, panel_index: int, value: float) -> None:
|
||||
"""Set frame property value (frame_depth or frame_thickness) for a specific panel.
|
||||
|
||||
Args:
|
||||
attr_name: Property name ("frame_depth" or "frame_thickness")
|
||||
panel_index: Panel index (0, 1, or 2)
|
||||
value: New value (clamped to min 0.0)
|
||||
"""
|
||||
current = getattr(self, attr_name)
|
||||
new_value = tuple(current[:panel_index]) + (max(0.0, value),) + tuple(current[panel_index + 1:])
|
||||
setattr(self, attr_name, new_value)
|
||||
|
||||
|
||||
class BIMDoorProperties(PropertyGroup):
|
||||
non_si_units_props = (
|
||||
@@ -1138,6 +1353,36 @@ class BIMDoorProperties(PropertyGroup):
|
||||
if prop_name not in exclude_props:
|
||||
setattr(target_props, prop_name, prop_value)
|
||||
|
||||
def has_threshold_depth(self) -> bool:
|
||||
"""Check if threshold depth gizmo should be visible (has threshold)."""
|
||||
return self.threshold_thickness > 0.0
|
||||
|
||||
def has_transom(self) -> bool:
|
||||
"""Check if transom-related gizmos should be visible (has transom)."""
|
||||
return self.transom_thickness > 0.0
|
||||
|
||||
def has_casing(self) -> bool:
|
||||
"""Check if casing gizmos should be visible (no lining offset)."""
|
||||
return self.lining_offset == 0.0
|
||||
|
||||
def has_casing_depth(self) -> bool:
|
||||
"""Check if casing depth gizmo should be visible (has casing and casing thickness > 0)."""
|
||||
return self.lining_offset == 0.0 and self.casing_thickness > 0.0
|
||||
|
||||
def get_panel_center_z(self) -> float:
|
||||
"""Get the vertical center of the door panel for gizmo positioning."""
|
||||
if self.transom_thickness > 0:
|
||||
return (self.transom_offset - self.threshold_thickness) / 2
|
||||
return (self.overall_height - self.threshold_thickness) / 2
|
||||
|
||||
def get_transom_window_center_z(self) -> float:
|
||||
"""Get the vertical center of the transom window for frame gizmo positioning."""
|
||||
return (self.transom_offset + self.transom_thickness / 2 + self.overall_height - self.lining_thickness) / 2
|
||||
|
||||
def get_casing_offset(self) -> float:
|
||||
"""Get casing offset for gizmo positioning (casing_thickness when lining_offset is 0)."""
|
||||
return self.casing_thickness if self.lining_offset == 0.0 else 0.0
|
||||
|
||||
|
||||
RailingType = Literal["FRAMELESS_PANEL", "WALL_MOUNTED_HANDRAIL"]
|
||||
CapType = Literal["TO_END_POST_AND_FLOOR", "TO_END_POST", "TO_FLOOR", "TO_WALL", "180", "NONE"]
|
||||
@@ -1156,11 +1401,11 @@ class BIMRailingProperties(PropertyGroup):
|
||||
is_editing_path: bpy.props.BoolProperty(default=False)
|
||||
|
||||
railing_type: bpy.props.EnumProperty(
|
||||
name="Railing Type", items=[(i, i, "") for i in get_args(RailingType)], default="FRAMELESS_PANEL"
|
||||
name="Railing Type", items=[(i, i, "") for i in get_args(RailingType)], default="FRAMELESS_PANEL", update=update_railing
|
||||
)
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, subtype="DISTANCE")
|
||||
thickness: bpy.props.FloatProperty(name="Thickness", default=0.050, subtype="DISTANCE")
|
||||
spacing: bpy.props.FloatProperty(name="Spacing", default=0.050, subtype="DISTANCE")
|
||||
height: bpy.props.FloatProperty(name="Height", default=1.0, subtype="DISTANCE", update=update_railing)
|
||||
thickness: bpy.props.FloatProperty(name="Thickness", default=0.050, subtype="DISTANCE", update=update_railing)
|
||||
spacing: bpy.props.FloatProperty(name="Spacing", default=0.050, subtype="DISTANCE", update=update_railing)
|
||||
|
||||
# wall mounted handrail specific properties
|
||||
use_manual_supports: bpy.props.BoolProperty(
|
||||
@@ -1168,6 +1413,7 @@ class BIMRailingProperties(PropertyGroup):
|
||||
default=False,
|
||||
description="If enabled, supports are added on every vertex on the edges of the railing path.\n"
|
||||
"If disabled, supports are added automatically based on the support spacing",
|
||||
update=update_railing,
|
||||
)
|
||||
support_spacing: bpy.props.FloatProperty(
|
||||
name="Support Spacing",
|
||||
@@ -1175,16 +1421,18 @@ class BIMRailingProperties(PropertyGroup):
|
||||
min=0.01,
|
||||
description="Distance between supports if automatic supports are used",
|
||||
subtype="DISTANCE",
|
||||
update=update_railing,
|
||||
)
|
||||
railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050, subtype="DISTANCE")
|
||||
railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050, subtype="DISTANCE", update=update_railing)
|
||||
clear_width: bpy.props.FloatProperty(
|
||||
name="Clear Width",
|
||||
default=0.040,
|
||||
description="Clear width between the railing and the wall",
|
||||
subtype="DISTANCE",
|
||||
update=update_railing,
|
||||
)
|
||||
terminal_type: bpy.props.EnumProperty(
|
||||
name="Terminal Type", items=[(i, i, "") for i in get_args(CapType)], default="180"
|
||||
name="Terminal Type", items=[(i, i, "") for i in get_args(CapType)], default="180", update=update_railing
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -1258,9 +1506,11 @@ RoofGenerationMethod = Literal["HEIGHT", "ANGLE"]
|
||||
class BIMRoofProperties(PropertyGroup):
|
||||
def update_angle(self, context: bpy.types.Context) -> None:
|
||||
self["angle"] = to_angle(self.percentage)
|
||||
update_roof(self, context)
|
||||
|
||||
def update_percentage(self, context: bpy.types.Context) -> None:
|
||||
self["percentage"] = to_percentage(self.angle)
|
||||
update_roof(self, context)
|
||||
|
||||
non_si_units_props = (
|
||||
"is_editing",
|
||||
@@ -1276,13 +1526,13 @@ class BIMRoofProperties(PropertyGroup):
|
||||
is_editing_path: bpy.props.BoolProperty(default=False)
|
||||
|
||||
roof_type: bpy.props.EnumProperty(
|
||||
name="Roof Type", items=[(i, i, "") for i in get_args(RoofType)], default="HIP/GABLE ROOF"
|
||||
name="Roof Type", items=[(i, i, "") for i in get_args(RoofType)], default="HIP/GABLE ROOF", update=update_roof
|
||||
)
|
||||
generation_method: bpy.props.EnumProperty(
|
||||
name="Roof Generation Method", items=[(i, i, "") for i in get_args(RoofGenerationMethod)], default="ANGLE"
|
||||
name="Roof Generation Method", items=[(i, i, "") for i in get_args(RoofGenerationMethod)], default="ANGLE", update=update_roof
|
||||
)
|
||||
height: bpy.props.FloatProperty(
|
||||
name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE"
|
||||
name="Height", default=1.0, description="Maximum height of the roof to be generated.", subtype="DISTANCE", update=update_roof
|
||||
)
|
||||
angle: bpy.props.FloatProperty(
|
||||
name="Slope Angle",
|
||||
@@ -1304,9 +1554,9 @@ class BIMRoofProperties(PropertyGroup):
|
||||
soft_min=to_percentage(radians(5.0)),
|
||||
soft_max=to_percentage(radians(60.0)),
|
||||
)
|
||||
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE")
|
||||
roof_thickness: bpy.props.FloatProperty(name="Roof Thickness", default=0.1, subtype="DISTANCE", update=update_roof)
|
||||
rafter_edge_angle: bpy.props.FloatProperty(
|
||||
name="Rafter Edge Angle", min=0, max=pi / 2, default=pi / 2, subtype="ANGLE"
|
||||
name="Rafter Edge Angle", min=0, max=pi / 2, default=pi / 2, subtype="ANGLE", update=update_roof
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import bpy
|
||||
import json
|
||||
import bmesh
|
||||
import math
|
||||
import ifcopenshell
|
||||
import ifcopenshell.api.pset
|
||||
import ifcopenshell.util.element
|
||||
@@ -34,10 +33,11 @@ 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
|
||||
from bpy_extras.object_utils import AddObjectHelper, object_data_add
|
||||
from typing import get_args
|
||||
from bpy.props import IntProperty
|
||||
from typing import get_args, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.model.prop import BIMStairProperties
|
||||
|
||||
|
||||
def regenerate_stair_mesh(obj: bpy.types.Object) -> None:
|
||||
@@ -142,10 +142,10 @@ class BIM_OT_add_stair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
return tool.Ifc.get() and context.mode == "OBJECT"
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
ifc_file = tool.Ifc.get()
|
||||
if not ifc_file:
|
||||
self.report({"ERROR"}, "You need to start IFC project first to create a stair.")
|
||||
@@ -187,7 +187,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Add Bonsai parametric stair to the active IFC element"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -216,6 +216,7 @@ class AddStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
regenerate_stair_mesh(obj)
|
||||
update_ifc_stair_props(obj)
|
||||
tool.Model.add_body_representation(obj)
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
@@ -224,7 +225,7 @@ class CancelEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Cancel editing and revert stair parameters to their previous values"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -246,7 +247,7 @@ class FinishEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Apply changes and finish editing stair parameters"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -274,7 +275,7 @@ class EnableEditingStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Enter edit mode to modify stair parameters interactively"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
@@ -291,7 +292,7 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Remove Stair"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
@@ -305,40 +306,40 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class ToggleStairTotalLengthLock(bpy.types.Operator):
|
||||
"""Toggle the total length lock for stair editing"""
|
||||
class ToggleStairProperty(bpy.types.Operator):
|
||||
"""Toggle a boolean property on stair properties"""
|
||||
|
||||
bl_idname = "bim.toggle_stair_total_length_lock"
|
||||
bl_label = "Toggle Stair Total Length Lock"
|
||||
bl_idname = "bim.toggle_stair_property"
|
||||
bl_label = "Toggle Stair Property"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
property_name: bpy.props.StringProperty(
|
||||
name="Property Name",
|
||||
description="Name of the boolean property to toggle",
|
||||
options={"HIDDEN", "SKIP_SAVE"},
|
||||
)
|
||||
|
||||
# Map property names to their descriptions
|
||||
PROPERTY_DESCRIPTIONS: dict[str, str] = {
|
||||
"total_length_lock": "Lock/unlock total stair length. When locked, changing treads adjusts tread depth",
|
||||
"custom_tread_lock": "Lock/unlock first and last tread dimensions. When unlocked, they can differ from other treads",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def description(cls, context: bpy.types.Context, properties: bpy.types.OperatorProperties) -> str:
|
||||
prop_name = properties.property_name
|
||||
return cls.PROPERTY_DESCRIPTIONS.get(prop_name, "Toggle a boolean property on stair properties")
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
if not obj or not self.property_name:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
props.total_length_lock = not props.total_length_lock
|
||||
|
||||
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"}
|
||||
if hasattr(props, self.property_name):
|
||||
setattr(props, self.property_name, not getattr(props, self.property_name))
|
||||
return {"FINISHED"}
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class AdjustStairTreads(bpy.types.Operator):
|
||||
@@ -350,13 +351,13 @@ class AdjustStairTreads(bpy.types.Operator):
|
||||
|
||||
increment: IntProperty(name="Increment", default=1)
|
||||
|
||||
def invoke(self, context, event):
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
if event.shift:
|
||||
bpy.ops.bim.set_stair_treads("INVOKE_DEFAULT")
|
||||
return {"FINISHED"}
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
def execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
@@ -376,7 +377,7 @@ class SetStairTreads(bpy.types.Operator):
|
||||
bl_label = "Set Number of Treads"
|
||||
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||
|
||||
def invoke(self, context, event): # noqa: ARG002
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]: # noqa: ARG002
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
@@ -390,10 +391,10 @@ class SetStairTreads(bpy.types.Operator):
|
||||
update_header(context, self._format_header())
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def modal(self, context, event):
|
||||
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
return run_integer_input_modal(self, context, event)
|
||||
|
||||
def _apply_value(self, context) -> None:
|
||||
def _apply_value(self, context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return
|
||||
@@ -402,7 +403,7 @@ class SetStairTreads(bpy.types.Operator):
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
props.number_of_treads = value
|
||||
|
||||
def _restore_value(self, context) -> None:
|
||||
def _restore_value(self, context: bpy.types.Context) -> None:
|
||||
obj = context.active_object
|
||||
if obj:
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
@@ -414,51 +415,36 @@ class SetStairTreads(bpy.types.Operator):
|
||||
return f"Number of Treads: {input_str}_{validity} | Enter to confirm, Esc to cancel"
|
||||
|
||||
|
||||
class CycleStairType(bpy.types.Operator):
|
||||
class CycleStairType(bpy.types.Operator, gizmo.CycleTypeMixin):
|
||||
"""Cycle through stair types. Shift+click to cycle in reverse."""
|
||||
|
||||
bl_idname = "bim.cycle_stair_type"
|
||||
bl_label = "Cycle Stair Type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
reverse: bpy.props.BoolProperty(name="Reverse", default=False, options={"HIDDEN", "SKIP_SAVE"})
|
||||
props_getter = "get_stair_props"
|
||||
type_literal = tool.Model.StairType
|
||||
type_attr = "stair_type"
|
||||
skip_element_check = True
|
||||
|
||||
def invoke(self, context, event):
|
||||
self.reverse = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.active_object
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_stair_props(obj)
|
||||
stair_types = get_args(tool.Model.StairType)
|
||||
current_idx = stair_types.index(props.stair_type) if props.stair_type in stair_types else 0
|
||||
direction = -1 if self.reverse else 1
|
||||
props.stair_type = stair_types[(current_idx + direction) % len(stair_types)]
|
||||
|
||||
return {"FINISHED"}
|
||||
def execute(self, context: bpy.types.Context) -> set[str]:
|
||||
return self._cycle_type(context)
|
||||
|
||||
|
||||
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]
|
||||
# Tread run accessors - callbacks that delegate to BIMStairProperties methods
|
||||
_tread_run_accessors = {
|
||||
0: (
|
||||
lambda props: props.get_custom_tread_run(0),
|
||||
lambda props, value: props.set_custom_tread_run(0, value),
|
||||
),
|
||||
1: (
|
||||
lambda props: props.get_custom_tread_run(1),
|
||||
lambda props, value: props.set_custom_tread_run(1, value),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
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))
|
||||
# Shorthand for gizmo offset constants used in DimensionGizmoConfig lambdas
|
||||
_G = gizmo.BaseParametricGizmoGroup
|
||||
|
||||
|
||||
class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
@@ -475,28 +461,22 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
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)
|
||||
ICON_CYCLE_SCALE = 0.3 # Scale for cycle type icon
|
||||
ICON_Z_OFFSET = 0.5 # Z offset above geometry for editing icons
|
||||
|
||||
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"
|
||||
|
||||
def get_icon_y_offset(self, context, mw):
|
||||
"""Get Y offset for icons based on view direction.
|
||||
def get_icon_y_extent(self, props: "BIMStairProperties") -> tuple[float, float]:
|
||||
"""Get Y extents for stair icon positioning.
|
||||
|
||||
Positions icons further than the furthest geometry:
|
||||
stair_width + 2 * GIZMO_OFFSET
|
||||
Stair geometry extends from Y=0 to Y=width.
|
||||
Icons are positioned beyond the width on either side.
|
||||
"""
|
||||
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
|
||||
return (furthest_y, furthest_y)
|
||||
|
||||
dimension_gizmo_props = [
|
||||
DimensionGizmoConfig(
|
||||
@@ -505,267 +485,164 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
prop_name="Total Length",
|
||||
min_value=0.01,
|
||||
text_offset_sign=-1,
|
||||
matrix_position=lambda p: V_(0, -_G.GIZMO_OFFSET, -_G.GIZMO_OFFSET),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="height", axis=(0, 0, 1), min_value=0.01, text_alignment="start",
|
||||
matrix_position=lambda p: V_(p.get_total_run() + _G.GIZMO_OFFSET, -_G.GIZMO_OFFSET, 0),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="width", axis=(0, 1, 0), min_value=0.01,
|
||||
matrix_position=lambda p: V_(_G.GIZMO_OFFSET, 0, -_G.GIZMO_OFFSET),
|
||||
),
|
||||
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,
|
||||
visibility_condition=lambda p: p.has_tread_run_gizmo(),
|
||||
matrix_position=lambda p: V_(
|
||||
0 if p.custom_tread_lock else p.custom_first_last_tread_run[0],
|
||||
0,
|
||||
p.get_riser_height() if p.custom_tread_lock else p.get_riser_height() * 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,
|
||||
visibility_condition=lambda p: p.has_custom_treads(),
|
||||
compute_value=_tread_run_accessors[0][0],
|
||||
apply_value=_tread_run_accessors[0][1],
|
||||
matrix_position=lambda p: V_(0, 0, p.get_riser_height()),
|
||||
),
|
||||
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,
|
||||
visibility_condition=lambda p: p.has_custom_treads(),
|
||||
compute_value=_tread_run_accessors[1][0],
|
||||
apply_value=_tread_run_accessors[1][1],
|
||||
matrix_position=lambda p: V_(
|
||||
p.get_total_run() - p.custom_first_last_tread_run[1], 0, p.height
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="nosing_length", axis=(-1, 0, 0),
|
||||
matrix_position=lambda p: V_(0, p.width / 2, p.get_riser_height()),
|
||||
),
|
||||
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",
|
||||
visibility_condition=lambda p: p.has_tread_depth(),
|
||||
matrix_position=lambda p: V_(0, 0, p.get_riser_height()),
|
||||
),
|
||||
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)),
|
||||
compute_value=lambda p: p.get_riser_height(),
|
||||
apply_value=lambda p, v: p.set_riser_height(v),
|
||||
matrix_position=lambda p: V_(p.tread_run, p.width, 0),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="nosing_depth",
|
||||
axis=(0, 0, -1),
|
||||
visibility_condition=lambda props: props.nosing_length != 0.0 and props.stair_type != "WOOD/STEEL",
|
||||
visibility_condition=lambda p: p.has_nosing(),
|
||||
matrix_position=lambda p: V_(-p.nosing_length, p.width / 2, p.get_riser_height()),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="base_slab_depth",
|
||||
axis=(0, 0, -1),
|
||||
visibility_condition=lambda props: props.stair_type == "CONCRETE",
|
||||
visibility_condition=lambda p: p.is_concrete_stair(),
|
||||
matrix_position=lambda p: V_(0, p.width / 2, 0),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="top_slab_depth",
|
||||
axis=(0, 0, -1),
|
||||
visibility_condition=lambda props: props.stair_type == "CONCRETE",
|
||||
visibility_condition=lambda p: p.is_concrete_stair(),
|
||||
matrix_position=lambda p: V_(p.get_total_run(), p.width / 2, p.height),
|
||||
),
|
||||
]
|
||||
|
||||
# Metadata-driven dispatch for props and preferences
|
||||
props_getter = "get_stair_props"
|
||||
gizmo_pref_name = "stair"
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
def is_element_type(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||
return tool.Blender.Modifier.is_stair(element)
|
||||
|
||||
def get_props(self, obj: bpy.types.Object):
|
||||
return tool.Model.get_stair_props(obj)
|
||||
def setup_element_specific_gizmos(self, context: bpy.types.Context) -> None:
|
||||
"""Create stair-specific icon gizmos (lock, plus, minus)."""
|
||||
self.lock_gizmo = self.create_icon_gizmo(
|
||||
"VIEW3D_GT_lock", self.COLOR_BLUE, "bim.toggle_stair_property",
|
||||
prop_path="BIMStairProperties.total_length_lock",
|
||||
property_name="total_length_lock",
|
||||
)
|
||||
self.tread_lock_gizmo = self.create_icon_gizmo(
|
||||
"VIEW3D_GT_lock", (1.0, 1.0, 1.0), "bim.toggle_stair_property",
|
||||
prop_path="BIMStairProperties.custom_tread_lock",
|
||||
property_name="custom_tread_lock",
|
||||
)
|
||||
self.plus_gizmo = self.create_icon_gizmo(
|
||||
"VIEW3D_GT_plus", self.COLOR_GREEN, "bim.adjust_stair_treads", increment=1
|
||||
)
|
||||
self.minus_gizmo = self.create_icon_gizmo(
|
||||
"VIEW3D_GT_minus", self.COLOR_RED, "bim.adjust_stair_treads", increment=-1
|
||||
)
|
||||
|
||||
def get_gizmo_prefs(self):
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
return prefs.gizmos.stair
|
||||
|
||||
@staticmethod
|
||||
def _get_stair_total_run(props) -> float:
|
||||
"""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_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_dimension_matrix_height(self, props) -> Matrix:
|
||||
total_run = self._get_stair_total_run(props)
|
||||
return self.compose_gizmo_matrix(V_(total_run + self.GIZMO_OFFSET, -self.GIZMO_OFFSET, 0), (0, 0, 1))
|
||||
|
||||
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)
|
||||
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_dimension_matrix_nosing_length(self, props) -> Matrix:
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
return self.compose_gizmo_matrix(V_(0, props.width / 2, riser_height), (-1, 0, 0))
|
||||
|
||||
def get_dimension_matrix_tread_depth(self, props) -> Matrix:
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
return self.compose_gizmo_matrix(V_(0, 0, riser_height), (0, 0, -1))
|
||||
|
||||
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)
|
||||
return self.compose_gizmo_matrix(V_(-props.nosing_length, props.width / 2, riser_height), (0, 0, -1))
|
||||
|
||||
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_dimension_matrix_top_slab_depth(self, props) -> Matrix:
|
||||
total_run = self._get_stair_total_run(props)
|
||||
return self.compose_gizmo_matrix(V_(total_run, props.width / 2, props.height), (0, 0, -1))
|
||||
|
||||
def setup(self, context: bpy.types.Context) -> None:
|
||||
self.setup_editing_gizmos(context)
|
||||
self.setup_dimension_gizmos(context)
|
||||
|
||||
prefs = tool.Blender.get_addon_preferences()
|
||||
highlight_color = prefs.decorator_color_selected[:3]
|
||||
|
||||
self.lock_gizmo = self.gizmos.new("VIEW3D_GT_lock")
|
||||
self.lock_gizmo.use_draw_scale = False
|
||||
self.lock_gizmo.color = self.COLOR_BLUE
|
||||
self.lock_gizmo.color_highlight = highlight_color
|
||||
self.lock_gizmo.alpha = 0.8
|
||||
self.lock_gizmo.prop_path = "BIMStairProperties.total_length_lock"
|
||||
self.lock_gizmo.target_set_operator("bim.toggle_stair_total_length_lock")
|
||||
|
||||
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
|
||||
self.plus_gizmo.color_highlight = highlight_color
|
||||
self.plus_gizmo.alpha = 0.8
|
||||
op = self.plus_gizmo.target_set_operator("bim.adjust_stair_treads")
|
||||
op.increment = 1
|
||||
|
||||
self.minus_gizmo = self.gizmos.new("VIEW3D_GT_minus")
|
||||
self.minus_gizmo.use_draw_scale = False
|
||||
self.minus_gizmo.color = self.COLOR_RED
|
||||
self.minus_gizmo.color_highlight = highlight_color
|
||||
self.minus_gizmo.alpha = 0.8
|
||||
op = self.minus_gizmo.target_set_operator("bim.adjust_stair_treads")
|
||||
op.increment = -1
|
||||
|
||||
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
|
||||
def _refresh_element_specific(
|
||||
self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties"
|
||||
) -> None:
|
||||
"""Update stair-specific lock and tread count gizmos."""
|
||||
billboard_rot = gizmo.get_billboard_rotation(context)
|
||||
self.update_editing_gizmos(context, mw, props)
|
||||
self.update_lock_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_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
|
||||
|
||||
def update_lock_gizmo(self, mw: Matrix, props: "BIMStairProperties", billboard_rot: Matrix) -> None:
|
||||
"""Update lock gizmo visibility, color, and position."""
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.lock_gizmo.hide = not props.is_editing or not gizmo_prefs.lock
|
||||
|
||||
if self.lock_gizmo.hide:
|
||||
return
|
||||
if not self.update_gizmo_visibility(self.lock_gizmo, props.is_editing, gizmo_prefs.lock):
|
||||
return # Hidden, skip positioning
|
||||
|
||||
self.lock_gizmo.color = self.COLOR_RED if props.total_length_lock else self.COLOR_GREEN
|
||||
|
||||
total_run = self._get_stair_total_run(props)
|
||||
total_run = props.get_total_run()
|
||||
local_transform = (
|
||||
Matrix.Translation(Vector((total_run + 0.5, -self.GIZMO_OFFSET, -self.GIZMO_OFFSET)))
|
||||
Matrix.Translation(Vector((total_run + self.ICON_Z_OFFSET, -self.GIZMO_OFFSET, -self.GIZMO_OFFSET)))
|
||||
@ billboard_rot
|
||||
@ Matrix.Scale(self.EDITING_ICON_SCALE, 4)
|
||||
)
|
||||
self.lock_gizmo.matrix_basis = mw @ local_transform
|
||||
|
||||
def update_tread_lock_gizmo(self, props) -> None:
|
||||
def update_tread_lock_gizmo(self, props: "BIMStairProperties") -> None:
|
||||
"""Update visibility of tread lock gizmo. Positioning is handled in _update_editing_icon_positions."""
|
||||
if not hasattr(self, "tread_lock_gizmo"):
|
||||
return
|
||||
|
||||
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
|
||||
self.update_gizmo_visibility(self.tread_lock_gizmo, props.is_editing, gizmo_prefs.lock)
|
||||
|
||||
def update_tread_count_gizmos(self, props) -> None:
|
||||
def update_tread_count_gizmos(self, props: "BIMStairProperties") -> 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
|
||||
gizmo_prefs = self.get_gizmo_prefs()
|
||||
self.update_gizmo_visibility(self.plus_gizmo, props.is_editing, gizmo_prefs.plus)
|
||||
# Minus has additional condition: number_of_treads > 1
|
||||
self.update_gizmo_visibility(
|
||||
self.minus_gizmo, props.is_editing and props.number_of_treads > 1, gizmo_prefs.minus
|
||||
)
|
||||
|
||||
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:
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties") -> 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)
|
||||
riser_height = self._get_first_riser_height(props)
|
||||
total_run = props.get_total_run()
|
||||
riser_height = props.get_riser_height()
|
||||
|
||||
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)
|
||||
@@ -774,57 +651,37 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
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
|
||||
self, mw: Matrix, props: "BIMStairProperties", 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)
|
||||
)
|
||||
y_pos_offset = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=True)
|
||||
x_pos = total_run + self.GIZMO_OFFSET if viewing_from_negative_x else -self.GIZMO_OFFSET
|
||||
|
||||
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)
|
||||
)
|
||||
self.set_dimension_gizmo_position("total_length_target", mw, V_(0, y_pos_offset, -self.GIZMO_OFFSET), (1, 0, 0))
|
||||
self.set_dimension_gizmo_position("width", mw, V_(x_pos, 0, -self.GIZMO_OFFSET), (0, 1, 0))
|
||||
self.set_dimension_gizmo_position("height", mw, V_(total_run + self.GIZMO_OFFSET, y_pos_offset, 0), (0, 0, 1))
|
||||
|
||||
def _update_tread_dimension_gizmos(
|
||||
self, mw: Matrix, props, viewing_from_negative_y: bool, total_run: float, riser_height: float
|
||||
self, mw: Matrix, props: "BIMStairProperties", 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)
|
||||
)
|
||||
y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=False)
|
||||
|
||||
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)
|
||||
)
|
||||
# tread_run position depends on custom_tread_lock state
|
||||
if props.custom_tread_lock:
|
||||
tread_x, tread_z = 0, riser_height
|
||||
else:
|
||||
tread_x = props.custom_first_last_tread_run[0]
|
||||
tread_z = riser_height * 2
|
||||
self.set_dimension_gizmo_position("tread_run", mw, V_(tread_x, y_pos, tread_z), (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)
|
||||
)
|
||||
self.set_dimension_gizmo_position("custom_first_tread_run", mw, V_(0, y_pos, riser_height), (1, 0, 0))
|
||||
|
||||
last_x = total_run - props.custom_first_last_tread_run[1]
|
||||
self.set_dimension_gizmo_position("custom_last_tread_run", mw, V_(last_x, y_pos, props.height), (1, 0, 0))
|
||||
|
||||
def _update_detail_dimension_gizmos(
|
||||
self, mw: Matrix, props, viewing_from_negative_y: bool, riser_height: float
|
||||
self, mw: Matrix, props: "BIMStairProperties", 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)
|
||||
@@ -835,25 +692,25 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
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
|
||||
self, mw: Matrix, props: "BIMStairProperties", 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
|
||||
"lock_gizmo", mw, total_run + self.ICON_Z_OFFSET, y_pos, -self.GIZMO_OFFSET, billboard_rot, scale=self.EDITING_ICON_SCALE
|
||||
)
|
||||
|
||||
def _update_editing_icon_positions(self, mw, props, viewing_from_negative_y, billboard_rot):
|
||||
def _update_editing_icon_positions(self, mw: Matrix, props: "BIMStairProperties", viewing_from_negative_y: bool, billboard_rot: Matrix) -> None:
|
||||
"""Update editing icon positions, flipping Y based on viewing angle."""
|
||||
if not props.is_editing:
|
||||
return
|
||||
|
||||
icon_z = props.height + 0.5
|
||||
y_pos = -self.GIZMO_OFFSET if viewing_from_negative_y else props.width + self.GIZMO_OFFSET
|
||||
icon_z = props.height + self.ICON_Z_OFFSET
|
||||
y_pos = self.get_icon_y_for_view(props, viewing_from_negative_y)
|
||||
|
||||
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("cycle_gizmo", mw, self.ICON_CYCLE_X, y_pos, icon_z, billboard_rot, scale=self.ICON_CYCLE_SCALE)
|
||||
self.set_icon_gizmo_position(
|
||||
"tread_lock_gizmo", mw, self.ICON_TREAD_LOCK_X, y_pos,
|
||||
icon_z - self.EDITING_ICON_SCALE / 2, billboard_rot, scale=self.EDITING_ICON_SCALE
|
||||
|
||||
@@ -31,9 +31,6 @@ from bonsai.bim.module.model.data import (
|
||||
RailingData,
|
||||
RoofData,
|
||||
)
|
||||
from bonsai.bim.module.model.stair import regenerate_stair_mesh
|
||||
from bonsai.bim.module.model.railing import update_railing_modifier_bmesh
|
||||
from bonsai.bim.module.model.roof import update_roof_modifier_bmesh
|
||||
from collections.abc import Iterable
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
@@ -307,8 +304,6 @@ class BIM_PT_stair(bpy.types.Panel):
|
||||
row = self.layout.row(align=True)
|
||||
|
||||
draw_stair_properties(self.layout, props)
|
||||
|
||||
regenerate_stair_mesh(obj)
|
||||
else:
|
||||
calculated_params = StairData.data["calculated_params"]
|
||||
row.operator("bim.enable_editing_stair", icon="GREASEPENCIL", text="")
|
||||
@@ -565,9 +560,6 @@ class BIM_PT_railing(bpy.types.Panel):
|
||||
row.operator("bim.cancel_editing_railing", icon="CANCEL", text="")
|
||||
|
||||
draw_railing_properties(self.layout, props)
|
||||
|
||||
update_railing_modifier_bmesh(context)
|
||||
|
||||
elif props.is_editing_path:
|
||||
row.operator("bim.finish_editing_railing_path", icon="CHECKMARK", text="")
|
||||
row.operator("bim.cancel_editing_railing_path", icon="CANCEL", text="")
|
||||
@@ -623,8 +615,6 @@ class BIM_PT_roof(bpy.types.Panel):
|
||||
row.operator("bim.cancel_editing_roof", icon="CANCEL", text="")
|
||||
|
||||
draw_roof_properties(self.layout, props)
|
||||
|
||||
update_roof_modifier_bmesh(obj)
|
||||
elif props.is_editing_path:
|
||||
row.operator("bim.finish_editing_roof_path", icon="CHECKMARK", text="")
|
||||
row.operator("bim.cancel_editing_roof_path", icon="CANCEL", text="")
|
||||
|
||||
@@ -39,47 +39,14 @@ import ifcopenshell.util.shape_builder
|
||||
import ifcopenshell.util.unit
|
||||
from bmesh.types import BMVert
|
||||
from mathutils import Vector, Matrix
|
||||
from typing import get_args
|
||||
from typing import get_args, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bonsai.bim.module.model.prop import BIMWindowProperties
|
||||
|
||||
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"
|
||||
# Shorthand for gizmo offset constants used in DimensionGizmoConfig lambdas
|
||||
_G = gizmo.BaseParametricGizmoGroup
|
||||
|
||||
|
||||
def update_window_modifier_representation(context: bpy.types.Context) -> None:
|
||||
@@ -488,7 +455,7 @@ class AddWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Add Bonsai parametric window to the active IFC element"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -522,7 +489,7 @@ class CancelEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Cancel editing and revert window parameters to their previous values"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -551,7 +518,7 @@ class FinishEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Apply changes and finish editing window parameters"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -584,7 +551,7 @@ class EnableEditingWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_description = "Enter edit mode to modify window parameters interactively"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
props = tool.Model.get_window_props(obj)
|
||||
@@ -607,7 +574,7 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
bl_label = "Remove Window"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
def _execute(self, context):
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]: # noqa: ARG002
|
||||
obj = context.active_object
|
||||
assert obj
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
@@ -621,49 +588,46 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class CycleWindowType(bpy.types.Operator, tool.Ifc.Operator):
|
||||
"""Cycle through available window types."""
|
||||
class CycleWindowType(bpy.types.Operator, tool.Ifc.Operator, gizmo.CycleTypeMixin):
|
||||
"""Cycle through available window types. Shift+click to cycle in reverse."""
|
||||
|
||||
bl_idname = "bim.cycle_window_type"
|
||||
bl_label = "Cycle Window Type"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def _execute(self, context):
|
||||
obj = tool.Blender.get_active_object()
|
||||
if not obj:
|
||||
return {"CANCELLED"}
|
||||
element_checker = "is_window"
|
||||
props_getter = "get_window_props"
|
||||
type_literal = tool.Model.WindowType
|
||||
type_attr = "window_type"
|
||||
|
||||
element = tool.Ifc.get_entity(obj)
|
||||
if not element or not tool.Blender.Modifier.is_window(element):
|
||||
return {"CANCELLED"}
|
||||
|
||||
props = tool.Model.get_window_props(obj)
|
||||
window_types = list(get_args(tool.Model.WindowType))
|
||||
current_index = window_types.index(props.window_type) if props.window_type in window_types else 0
|
||||
next_index = (current_index + 1) % len(window_types)
|
||||
props.window_type = window_types[next_index]
|
||||
|
||||
return {"FINISHED"}
|
||||
def _execute(self, context: bpy.types.Context) -> set[str]:
|
||||
return self._cycle_type(context)
|
||||
|
||||
|
||||
def _compute_frame_depth(props) -> float:
|
||||
"""Get the first panel's frame depth value."""
|
||||
return props.frame_depth[0]
|
||||
# Frame accessor factory - creates callbacks that delegate to BIMWindowProperties methods
|
||||
def _make_frame_accessors(
|
||||
attr_name: str, panel_index: int
|
||||
) -> tuple["collections.abc.Callable[[BIMWindowProperties], float]", "collections.abc.Callable[[BIMWindowProperties, float], None]"]:
|
||||
"""Create compute/apply callbacks for frame properties at a specific panel index.
|
||||
|
||||
Args:
|
||||
attr_name: Property name ("frame_depth" or "frame_thickness")
|
||||
panel_index: Panel index (0, 1, or 2)
|
||||
|
||||
Returns:
|
||||
Tuple of (compute_fn, apply_fn) that delegate to BIMWindowProperties methods
|
||||
"""
|
||||
return (
|
||||
lambda props: props.get_frame_value(attr_name, panel_index),
|
||||
lambda props, value: props.set_frame_value(attr_name, panel_index, value),
|
||||
)
|
||||
|
||||
|
||||
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:])
|
||||
_frame_accessors = {
|
||||
(attr, idx): _make_frame_accessors(attr, idx)
|
||||
for attr in ("frame_depth", "frame_thickness")
|
||||
for idx in range(3)
|
||||
}
|
||||
|
||||
|
||||
class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
@@ -678,180 +642,147 @@ class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
|
||||
cancel_editing_operator = "bim.cancel_editing_window"
|
||||
cycle_type_operator = "bim.cycle_window_type"
|
||||
|
||||
# matrix_position lambdas replace the get_dimension_matrix_* methods
|
||||
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,
|
||||
attr_name="overall_width", axis=(1, 0, 0), min_value=0.01, text_offset_sign=-1,
|
||||
matrix_position=lambda p: V_(0, p.lining_offset - _G.GIZMO_OFFSET, -_G.GIZMO_OFFSET),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="frame_thickness",
|
||||
axis=(1, 0, 0),
|
||||
compute_value=_compute_frame_thickness,
|
||||
apply_value=_apply_frame_thickness,
|
||||
attr_name="overall_height", axis=(0, 0, 1), min_value=0.01, text_alignment="start",
|
||||
matrix_position=lambda p: V_(p.overall_width + _G.GIZMO_OFFSET, p.lining_offset - _G.GIZMO_OFFSET, 0),
|
||||
),
|
||||
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),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="lining_depth", axis=(0, 1, 0),
|
||||
matrix_position=lambda p: V_(p.overall_width / 2, p.lining_offset, p.overall_height),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="lining_thickness", axis=(1, 0, 0),
|
||||
matrix_position=lambda p: V_(0, p.lining_depth / 2 + p.lining_offset, p.overall_height / 2),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="lining_to_panel_offset_x", axis=(1, 0, 0),
|
||||
matrix_position=lambda p: V_(
|
||||
0,
|
||||
p.get_lining_to_panel_offset_y_full() + p.frame_depth[0] + p.lining_offset,
|
||||
p.lining_to_panel_offset_x
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="lining_to_panel_offset_y", axis=(0, 1, 0), min_value=-10.0,
|
||||
matrix_position=lambda p: V_(
|
||||
p.overall_width - p.lining_to_panel_offset_x,
|
||||
p.lining_depth + p.lining_offset,
|
||||
p.lining_to_panel_offset_x
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="frame_depth", axis=(0, -1, 0),
|
||||
compute_value=_frame_accessors[("frame_depth", 0)][0],
|
||||
apply_value=_frame_accessors[("frame_depth", 0)][1],
|
||||
matrix_position=lambda p: p.get_frame_position(0, is_depth=True),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="frame_thickness", axis=(1, 0, 0),
|
||||
compute_value=_frame_accessors[("frame_thickness", 0)][0],
|
||||
apply_value=_frame_accessors[("frame_thickness", 0)][1],
|
||||
matrix_position=lambda p: p.get_frame_position(0, is_depth=False),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="second_frame_depth", axis=(0, -1, 0),
|
||||
compute_value=_frame_accessors[("frame_depth", 1)][0],
|
||||
apply_value=_frame_accessors[("frame_depth", 1)][1],
|
||||
visibility_condition=lambda p: p.has_second_panel(),
|
||||
matrix_position=lambda p: p.get_frame_position(1, is_depth=True),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="second_frame_thickness", axis=(1, 0, 0),
|
||||
compute_value=_frame_accessors[("frame_thickness", 1)][0],
|
||||
apply_value=_frame_accessors[("frame_thickness", 1)][1],
|
||||
visibility_condition=lambda p: p.has_second_panel(),
|
||||
matrix_position=lambda p: p.get_frame_position(1, is_depth=False),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="third_frame_depth", axis=(0, -1, 0),
|
||||
compute_value=_frame_accessors[("frame_depth", 2)][0],
|
||||
apply_value=_frame_accessors[("frame_depth", 2)][1],
|
||||
visibility_condition=lambda p: p.has_third_panel(),
|
||||
matrix_position=lambda p: p.get_frame_position(2, is_depth=True),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="third_frame_thickness", axis=(1, 0, 0),
|
||||
compute_value=_frame_accessors[("frame_thickness", 2)][0],
|
||||
apply_value=_frame_accessors[("frame_thickness", 2)][1],
|
||||
visibility_condition=lambda p: p.has_third_panel(),
|
||||
matrix_position=lambda p: p.get_frame_position(2, is_depth=False),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="mullion_thickness", axis=(1, 0, 0), delta_scale=2.0,
|
||||
visibility_condition=lambda p: p.has_mullion(),
|
||||
matrix_position=lambda p: V_(
|
||||
p.first_mullion_offset - p.mullion_thickness / 2,
|
||||
p.lining_offset,
|
||||
p.overall_height / 2 + 3 * _G.GIZMO_STACK_OFFSET
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="first_mullion_offset", axis=(1, 0, 0),
|
||||
visibility_condition=lambda p: p.has_mullion(),
|
||||
matrix_position=lambda p: V_(0, p.lining_offset, p.overall_height / 2 + _G.GIZMO_STACK_OFFSET),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="second_mullion_offset", axis=(1, 0, 0),
|
||||
visibility_condition=lambda p: p.has_second_mullion(),
|
||||
matrix_position=lambda p: V_(0, p.lining_offset, p.overall_height / 2 + 2 * _G.GIZMO_STACK_OFFSET),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="transom_thickness", axis=(0, 0, 1), delta_scale=2.0,
|
||||
visibility_condition=lambda p: p.has_transom(),
|
||||
matrix_position=lambda p: V_(
|
||||
p.overall_width / 2 + 2 * _G.GIZMO_STACK_OFFSET,
|
||||
p.lining_offset,
|
||||
p.first_transom_offset - p.transom_thickness / 2
|
||||
),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="first_transom_offset", axis=(0, 0, 1),
|
||||
visibility_condition=lambda p: p.has_transom(),
|
||||
matrix_position=lambda p: V_(p.overall_width / 2, p.lining_offset, 0),
|
||||
),
|
||||
DimensionGizmoConfig(
|
||||
attr_name="second_transom_offset", axis=(0, 0, 1),
|
||||
visibility_condition=lambda p: p.has_second_transom(),
|
||||
matrix_position=lambda p: V_(p.overall_width / 2 + _G.GIZMO_STACK_OFFSET, p.lining_offset, 0),
|
||||
),
|
||||
# lining_offset is handled specially in _update_dimension_gizmo_positions due to negative value support
|
||||
DimensionGizmoConfig(attr_name="lining_offset", axis=(0, 1, 0), min_value=-10.0),
|
||||
]
|
||||
|
||||
props_getter = "get_window_props"
|
||||
gizmo_pref_name = "window"
|
||||
|
||||
@classmethod
|
||||
def is_element_type(cls, element) -> bool:
|
||||
def is_element_type(cls, element: ifcopenshell.entity_instance) -> bool:
|
||||
return tool.Blender.Modifier.is_window(element)
|
||||
|
||||
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 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)
|
||||
def get_icon_y_extent(self, props: "BIMWindowProperties") -> tuple[float, float]:
|
||||
"""Get Y extents for window icon positioning.
|
||||
|
||||
Window geometry can extend asymmetrically in +Y and -Y directions
|
||||
depending on lining_offset (which can be negative).
|
||||
"""
|
||||
furthest_positive_y = (
|
||||
max(0, props.lining_offset)
|
||||
+ props.lining_depth
|
||||
+ props.lining_to_panel_offset_y
|
||||
+ 2 * self.GIZMO_OFFSET
|
||||
)
|
||||
furthest_negative_y = min(0, props.lining_offset)
|
||||
furthest_negative_y = abs(min(0, props.lining_offset)) + 2 * self.GIZMO_OFFSET
|
||||
return (furthest_positive_y, furthest_negative_y)
|
||||
|
||||
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
|
||||
# Window uses base class setup() and refresh() - no element-specific gizmos needed
|
||||
|
||||
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)
|
||||
)
|
||||
|
||||
@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_dimension_matrix_lining_to_panel_offset_x(self, props) -> Matrix:
|
||||
y_full = self._get_lining_to_panel_offset_y_full(props)
|
||||
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)
|
||||
)
|
||||
|
||||
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)
|
||||
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 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 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 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: 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_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:
|
||||
def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: Matrix, props: "BIMWindowProperties") -> 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))
|
||||
# Window uses base implementation with default casing_offset=0
|
||||
self._update_view_dependent_dimensions(context, mw, props)
|
||||
|
||||
Reference in New Issue
Block a user