Refactor parametric element gizmos

- Add gizmos for windows and stairs
- Add BaseParametricGizmoGroup mixin for doors, windows, and stairs
- Improve snapping with screen-space distance calculation
- Add GizmoPlus, GizmoMinus, GizmoCycle icon gizmos
- Add GizmoPropConfig dataclass for cleaner gizmo configuration
This commit is contained in:
Gorgious
2025-11-27 22:56:27 +01:00
parent fcf5614004
commit c828c1b08d
8 changed files with 1464 additions and 330 deletions
+5
View File
@@ -149,6 +149,8 @@ classes = [
ui.BIM_UL_generic, ui.BIM_UL_generic,
ui.DocPreferences, ui.DocPreferences,
ui.GizmoPreferencesDoor, # Register before GizmoPreferences ui.GizmoPreferencesDoor, # Register before GizmoPreferences
ui.GizmoPreferencesWindow, # Register before GizmoPreferences
ui.GizmoPreferencesStair, # Register before GizmoPreferences
ui.GizmoPreferences, ui.GizmoPreferences,
# ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below) # ui.DefaultParameters and ui.BIM_ADDON_preferences are registered separately after modules (see late_classes below)
# Tabs panel # Tabs panel
@@ -212,6 +214,9 @@ classes = [
gizmo.GizmoPen, gizmo.GizmoPen,
gizmo.GizmoValidate, gizmo.GizmoValidate,
gizmo.GizmoCancel, gizmo.GizmoCancel,
gizmo.GizmoPlus,
gizmo.GizmoMinus,
gizmo.GizmoCycle,
] ]
for mod in modules.values(): for mod in modules.values():
File diff suppressed because it is too large Load Diff
@@ -161,6 +161,10 @@ classes = (
stair.FinishEditingStair, stair.FinishEditingStair,
stair.EnableEditingStair, stair.EnableEditingStair,
stair.RemoveStair, stair.RemoveStair,
stair.ToggleStairTotalLengthLock,
stair.AdjustStairTreads,
stair.CycleStairType,
stair.GizmoStairEdition,
sverchok_modifier.CreateNewSverchokGraph, sverchok_modifier.CreateNewSverchokGraph,
sverchok_modifier.UpdateDataFromSverchok, sverchok_modifier.UpdateDataFromSverchok,
sverchok_modifier.DeleteSverchokGraph, sverchok_modifier.DeleteSverchokGraph,
@@ -172,6 +176,7 @@ classes = (
window.FinishEditingWindow, window.FinishEditingWindow,
window.EnableEditingWindow, window.EnableEditingWindow,
window.RemoveWindow, window.RemoveWindow,
window.GizmoWindowEdition,
door.BIM_OT_add_door, door.BIM_OT_add_door,
door.AddDoor, door.AddDoor,
door.CancelEditingDoor, door.CancelEditingDoor,
+124 -193
View File
@@ -33,10 +33,10 @@ import bonsai.core.geometry
import bonsai.core.geometry as core import bonsai.core.geometry as core
import bonsai.core.root import bonsai.core.root
from bonsai.bim.module.model.window import create_bm_window, create_bm_box from bonsai.bim.module.model.window import create_bm_window, create_bm_box
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
import math
from mathutils import Vector, Matrix from mathutils import Vector, Matrix
from typing import Union, Any, Optional
import json import json
import collections import collections
@@ -193,8 +193,8 @@ def bm_mirror(
def create_bm_extruded_profile( def create_bm_extruded_profile(
bm: bmesh.types.BMesh, bm: bmesh.types.BMesh,
points: list[Vector], points: list[Vector],
edges: Optional[list[tuple[int, int]]] = None, edges: list[tuple[int, int]] | None = None,
faces: Optional[list[list[int]]] = None, faces: list[list[int]] | None = None,
position: Vector = V_(0, 0, 0).freeze(), position: Vector = V_(0, 0, 0).freeze(),
magnitude: float = 1.0, magnitude: float = 1.0,
extrusion_vector: Vector = V_(0, 0, 1).freeze(), extrusion_vector: Vector = V_(0, 0, 1).freeze(),
@@ -473,6 +473,7 @@ def update_door_modifier_bmesh(context: bpy.types.Context) -> None:
lining_offset_verts = lining_verts + door_verts + window_lining_verts + frame_verts + glass_verts lining_offset_verts = lining_verts + door_verts + window_lining_verts + frame_verts + glass_verts
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=lining_offset_verts) bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=lining_offset_verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if bpy.context.active_object.mode == "EDIT": if bpy.context.active_object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data) bmesh.update_edit_mesh(obj.data)
@@ -684,104 +685,118 @@ class RemoveDoor(bpy.types.Operator, tool.Ifc.Operator):
class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator): class ToggleDoorSwing(bpy.types.Operator, tool.Ifc.Operator):
"""Toggle door swing direction between LEFT and RIGHT""" """Toggle door swing direction and optionally flip door geometry.
Shift+Click (when flip_geometry=True): Flip geometry only without changing door direction"""
bl_idname = "bim.toggle_door_swing" bl_idname = "bim.toggle_door_swing"
bl_label = "Toggle Door Swing" bl_label = "Toggle Door Swing"
bl_options = {"REGISTER", "UNDO"} bl_options = {"REGISTER", "UNDO"}
flip_geometry: bpy.props.BoolProperty(name="Flip Geometry", default=False)
flip_local_axes: bpy.props.EnumProperty(
name="Flip Local Axes", items=(("XY", "XY", ""), ("YZ", "YZ", ""), ("XZ", "XZ", "")), default="XY"
)
skip_direction_change: bpy.props.BoolProperty(
name="Skip Direction Change", default=False, options={"HIDDEN", "SKIP_SAVE"}
)
def invoke(self, context, event):
self.skip_direction_change = event.shift
return self.execute(context)
def _toggle_swing_direction(self, obj: bpy.types.Object) -> bool:
"""Toggle door swing direction between LEFT and RIGHT."""
props = tool.Model.get_door_props(obj)
current_type = props.door_type
if "LEFT" in current_type:
props.door_type = current_type.replace("LEFT", "RIGHT")
return True
elif "RIGHT" in current_type:
props.door_type = current_type.replace("RIGHT", "LEFT")
return True
return False
def _execute(self, context): def _execute(self, context):
obj = tool.Blender.get_active_object() obj = tool.Blender.get_active_object()
if not obj: if not obj:
return {"CANCELLED"} return {"CANCELLED"}
element = tool.Ifc.get_entity(obj) element = tool.Ifc.get_entity(obj)
if not element or not tool.Blender.Modifier.is_door(element): if not element:
return {"CANCELLED"} return {"CANCELLED"}
props = tool.Model.get_door_props(obj) is_door = tool.Blender.Modifier.is_door(element)
current_type = props.door_type
if "LEFT" in current_type: if self.flip_geometry:
props.door_type = current_type.replace("LEFT", "RIGHT") tool.Geometry.flip_object(obj, self.flip_local_axes)
elif "RIGHT" in current_type: if not self.skip_direction_change and is_door:
props.door_type = current_type.replace("RIGHT", "LEFT") self._toggle_swing_direction(obj)
elif is_door:
self._toggle_swing_direction(obj)
else:
return {"CANCELLED"}
return {"FINISHED"} return {"FINISHED"}
class GizmoDoorEdition(bpy.types.GizmoGroup): class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_door_edition" bl_idname = "OBJECT_GGT_bim_door_edition"
bl_label = "Door Editing Gizmo" bl_label = "Door Editing Gizmo"
bl_space_type = "VIEW_3D" bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW" bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"} bl_options = {"3D", "PERSISTENT"}
COLOR_RED = (1.0, 0.2, 0.2) enable_editing_operator = "bim.enable_editing_door"
COLOR_GREEN = (0.1, 0.8, 0.1) finish_editing_operator = "bim.finish_editing_door"
COLOR_BLUE = (0.2, 0.2, 1.0) cancel_editing_operator = "bim.cancel_editing_door"
ARROW_SCALE = 0.25
@classmethod
def get_arrow_color_from_axis(cls, axis):
"""Get arrow color based on axis direction (X=red, Y=green, Z=blue)."""
if axis[0] != 0:
return cls.COLOR_RED
elif axis[1] != 0:
return cls.COLOR_GREEN
else:
return cls.COLOR_BLUE
gizmo_props = [ gizmo_props = [
{"attr_name": "overall_height", "axis": (0, 0, 1)}, GizmoPropConfig("overall_height", (0, 0, 1)),
{"attr_name": "overall_width", "axis": (1, 0, 0)}, GizmoPropConfig("overall_width", (1, 0, 0)),
{"attr_name": "threshold_thickness", "axis": (0, 0, 1)}, GizmoPropConfig("threshold_thickness", (0, 0, 1)),
{"attr_name": "threshold_depth", "axis": (0, 1, 0)}, GizmoPropConfig("threshold_depth", (0, 1, 0)),
{"attr_name": "lining_depth", "axis": (0, 1, 0)}, GizmoPropConfig("lining_depth", (0, 1, 0)),
{"attr_name": "lining_thickness", "axis": (1, 0, 0)}, GizmoPropConfig("lining_thickness", (-1, 0, 0)),
{"attr_name": "transom_offset", "axis": (0, 0, 1)}, GizmoPropConfig("transom_offset", (0, 0, 1)),
{"attr_name": "transom_thickness", "axis": (0, 0, 1)}, GizmoPropConfig("transom_thickness", (0, 0, 1)),
GizmoPropConfig("casing_thickness", (-1, 0, 0)),
GizmoPropConfig("casing_depth", (0, 1, 0)),
] ]
@classmethod @classmethod
def poll(cls, context): def is_element_type(cls, element) -> bool:
"""Show gizmo only when a single door object is selected and active.""" return tool.Blender.Modifier.is_door(element)
def get_props(self, obj):
return tool.Model.get_door_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences() prefs = tool.Blender.get_addon_preferences()
if not prefs.gizmos.draw_gizmos_in_3d_viewport: return prefs.gizmos.door
return False
obj = tool.Blender.get_active_object(is_selected=True) def should_hide_gizmo(self, attr_name, props):
if not obj: """Door-specific visibility rules for gizmos."""
return False if not props.is_editing:
return True
if len(tool.Blender.get_selected_objects()) != 1: if attr_name == "threshold_depth" and props.threshold_thickness == 0.0:
return False return True
if attr_name == "transom_offset" and props.transom_thickness == 0.0:
element = tool.Ifc.get_entity(obj) return True
if not element or not tool.Blender.Modifier.is_door(element): if attr_name == "casing_thickness" and props.lining_offset != 0.0:
return False return True
return True if attr_name == "casing_depth" and (props.lining_offset != 0.0 or props.casing_thickness == 0.0):
return True
def get_axis_rotation_matrix(self, axis): return False
"""Get rotation matrix to align arrow (default +X direction) with the given axis.
Args:
axis: Tuple of (x, y, z) representing the target axis direction
Returns:
Rotation matrix to align arrow with the axis
"""
axis_vec = V_(*axis).normalized()
default_dir = V_(1, 0, 0)
return default_dir.rotation_difference(axis_vec).to_matrix().to_4x4()
def get_gizmo_matrix_overall_height(self, props): def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, 0.0, props.overall_height)) translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
rotation = self.get_axis_rotation_matrix((0, 0, 1)) rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation return translation @ rotation
def get_gizmo_matrix_overall_width(self, props): def get_gizmo_matrix_overall_width(self, props):
translation = Matrix.Translation(V_(props.overall_width, 0.0, props.overall_height - 0.05)) translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
rotation = self.get_axis_rotation_matrix((1, 0, 0)) rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation return translation @ rotation
@@ -789,7 +804,7 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
translation = Matrix.Translation( translation = Matrix.Translation(
V_( V_(
props.overall_width / 2, props.overall_width / 2,
props.threshold_offset + props.threshold_depth / 2, props.threshold_offset + props.threshold_depth / 2 + props.lining_offset,
props.threshold_thickness, props.threshold_thickness,
) )
) )
@@ -798,7 +813,7 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
def get_gizmo_matrix_threshold_depth(self, props): def get_gizmo_matrix_threshold_depth(self, props):
translation = Matrix.Translation( translation = Matrix.Translation(
V_(props.overall_width / 2, props.threshold_depth, props.threshold_thickness / 2) V_(props.overall_width / 2, props.threshold_depth + props.lining_offset, props.threshold_thickness / 2)
) )
rotation = self.get_axis_rotation_matrix((0, 1, 0)) rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation return translation @ rotation
@@ -812,146 +827,86 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
def get_gizmo_matrix_lining_thickness(self, props): def get_gizmo_matrix_lining_thickness(self, props):
translation = Matrix.Translation( translation = Matrix.Translation(
V_(props.lining_thickness, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2) V_(props.overall_width - props.lining_thickness, props.lining_depth / 2, props.overall_height / 2)
) )
rotation = self.get_axis_rotation_matrix((1, 0, 0)) rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation return translation @ rotation
def get_gizmo_matrix_transom_offset(self, props): def get_gizmo_matrix_transom_offset(self, props):
translation = Matrix.Translation(V_(props.overall_width / 2, 0.0, props.transom_offset)) translation = Matrix.Translation(V_(props.overall_width / 2, props.lining_offset, props.transom_offset))
rotation = self.get_axis_rotation_matrix((0, 0, 1)) rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props): def get_gizmo_matrix_transom_thickness(self, props):
translation = Matrix.Translation( translation = Matrix.Translation(
V_(props.overall_width / 2, 0.0, props.transom_offset + props.transom_thickness) V_(props.overall_width / 2, props.lining_offset, props.transom_offset + props.transom_thickness)
) )
rotation = self.get_axis_rotation_matrix((0, 0, 1)) rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation return translation @ rotation
@staticmethod
def _get_casing_gizmo_base_position(props) -> tuple[float, float, float]:
"""Get common base position for casing gizmos."""
x = -props.casing_thickness + props.lining_thickness
y_base = props.lining_depth + props.lining_offset
z = props.overall_height / 2
return x, y_base, z
def get_gizmo_matrix_casing_thickness(self, props):
x, y_base, z = self._get_casing_gizmo_base_position(props)
translation = Matrix.Translation(V_(x, y_base + props.casing_depth / 2, z))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_casing_depth(self, props):
x, y_base, z = self._get_casing_gizmo_base_position(props)
translation = Matrix.Translation(V_(x, y_base + props.casing_depth, z))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def setup(self, context): def setup(self, context):
# Use base class methods for common gizmos
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
# Door-specific: swing arc gizmos
prefs = tool.Blender.get_addon_preferences() prefs = tool.Blender.get_addon_preferences()
default_color = prefs.decorations_colour[:3]
highlight_color = prefs.decorator_color_selected[:3] highlight_color = prefs.decorator_color_selected[:3]
inactive_color = prefs.decorator_color_background[:3] inactive_color = prefs.decorator_color_background[:3]
special_color = prefs.decorator_color_special[:3] special_color = prefs.decorator_color_special[:3]
def add_gizmo_prop(prop_config):
attr_name = prop_config["attr_name"]
gizmo = self.gizmos.new("BIM_GT_gizmo_cone")
def move_get():
obj = bpy.context.active_object
if not obj:
return 0.0
props = tool.Model.get_door_props(obj)
return getattr(props, attr_name)
def move_set(value):
obj = bpy.context.active_object
if not obj:
return
props = tool.Model.get_door_props(obj)
setattr(props, attr_name, max(0.0, value))
# Set callbacks on the gizmo instance
gizmo.move_get_cb = move_get
gizmo.move_set_cb = move_set
gizmo.axis = V_(*prop_config["axis"])
gizmo.local_axis = V_(*prop_config["axis"])
gizmo.color = self.get_arrow_color_from_axis(prop_config["axis"])
gizmo.color_highlight = highlight_color
gizmo.alpha = 0.99
gizmo.use_draw_modal = True
gizmo.use_draw_scale = True
setattr(self, f"gizmo_{attr_name}", gizmo)
for prop_config in self.gizmo_props:
add_gizmo_prop(prop_config)
self.gizmo_door_type = self.gizmos.new("VIEW3D_GT_arc") self.gizmo_door_type = self.gizmos.new("VIEW3D_GT_arc")
self.gizmo_door_type.use_draw_scale = False self.gizmo_door_type.use_draw_scale = False
self.gizmo_door_type.color = special_color self.gizmo_door_type.color = special_color
self.gizmo_door_type.alpha = 0.5 self.gizmo_door_type.alpha = 0.5
self.gizmo_door_type.color_highlight = highlight_color self.gizmo_door_type.color_highlight = highlight_color
self.gizmo_door_type.prop_path = "BIMDoorProperties.door_type" self.gizmo_door_type.prop_path = "BIMDoorProperties.door_type"
self.gizmo_door_type.target_set_operator("bim.toggle_door_swing") op = self.gizmo_door_type.target_set_operator("bim.toggle_door_swing")
op.flip_geometry = False
# Flip arc gizmo - mirrors the swing arc along X axis, flips door when clicked # Flip arc gizmo - mirrors the swing arc along X axis, flips door and toggles direction when clicked
self.gizmo_flip_arc = self.gizmos.new("VIEW3D_GT_arc") self.gizmo_flip_arc = self.gizmos.new("VIEW3D_GT_arc")
self.gizmo_flip_arc.use_draw_scale = False self.gizmo_flip_arc.use_draw_scale = False
self.gizmo_flip_arc.color = inactive_color self.gizmo_flip_arc.color = inactive_color
self.gizmo_flip_arc.alpha = 0.5 self.gizmo_flip_arc.alpha = 0.5
self.gizmo_flip_arc.color_highlight = highlight_color self.gizmo_flip_arc.color_highlight = highlight_color
self.gizmo_flip_arc.prop_path = "BIMDoorProperties.door_type" self.gizmo_flip_arc.prop_path = "BIMDoorProperties.door_type"
op = self.gizmo_flip_arc.target_set_operator("bim.flip_object") op = self.gizmo_flip_arc.target_set_operator("bim.toggle_door_swing")
op.flip_geometry = True
op.flip_local_axes = "XY" op.flip_local_axes = "XY"
self.pen_gizmo = self.gizmos.new("VIEW3D_GT_pen")
self.pen_gizmo.use_draw_scale = False
self.pen_gizmo.color = default_color
self.pen_gizmo.color_highlight = highlight_color
self.pen_gizmo.alpha = 0.8
self.pen_gizmo.target_set_operator("bim.enable_editing_door")
self.validate_gizmo = self.gizmos.new("VIEW3D_GT_validate")
self.validate_gizmo.use_draw_scale = False
self.validate_gizmo.color = self.COLOR_GREEN
self.validate_gizmo.color_highlight = highlight_color
self.validate_gizmo.alpha = 0.8
self.validate_gizmo.target_set_operator("bim.finish_editing_door")
self.cancel_gizmo = self.gizmos.new("VIEW3D_GT_cancel")
self.cancel_gizmo.use_draw_scale = False
self.cancel_gizmo.color = self.COLOR_RED
self.cancel_gizmo.color_highlight = highlight_color
self.cancel_gizmo.alpha = 0.8
self.cancel_gizmo.target_set_operator("bim.cancel_editing_door")
def refresh(self, context): def refresh(self, context):
obj = context.active_object obj = context.active_object
if not obj: if not obj:
return return
props = tool.Model.get_door_props(obj) props = self.get_props(obj)
mw = obj.matrix_world mw = obj.matrix_world
self.update_arrows(mw, props) self.update_property_gizmos(mw, props)
self.update_swing_gizmo(mw, props) self.update_swing_gizmos(mw, props)
self.update_editing_gizmos(mw, props) self.update_editing_gizmos(mw, props)
def update_arrows(self, mw, props): def update_swing_gizmos(self, mw, props):
"""Update arrow gizmos position and visibility based on editing state."""
prefs = tool.Blender.get_addon_preferences()
door_gizmo_prefs = prefs.gizmos.door
for prop_config in self.gizmo_props:
attr_name = prop_config["attr_name"]
gizmo = getattr(self, f"gizmo_{attr_name}", None)
if gizmo is None:
continue
if not getattr(door_gizmo_prefs, attr_name, True):
gizmo.hide = True
continue
if (
not props.is_editing
or (attr_name == "threshold_depth" and props.threshold_thickness == 0.0)
or (attr_name == "transom_offset" and props.transom_thickness == 0.0)
):
gizmo.hide = True
continue
# Update position and show arrow when editing
gizmo.hide = False
matrix_method = getattr(self, f"get_gizmo_matrix_{attr_name}", None)
if matrix_method:
gizmo.matrix_basis = mw @ matrix_method(props)
gizmo.matrix_offset = Matrix.Scale(self.ARROW_SCALE, 4)
def update_swing_gizmo(self, mw, props):
"""Update swing gizmo position and color based on editing state.""" """Update swing gizmo position and color based on editing state."""
prefs = tool.Blender.get_addon_preferences() prefs = tool.Blender.get_addon_preferences()
door_gizmo_prefs = prefs.gizmos.door door_gizmo_prefs = prefs.gizmos.door
@@ -965,37 +920,13 @@ class GizmoDoorEdition(bpy.types.GizmoGroup):
# Calculate base swing transformation (common to both arcs) # Calculate base swing transformation (common to both arcs)
swing_x_offset = props.overall_width if "RIGHT" in props.door_type else 0.0 swing_x_offset = props.overall_width if "RIGHT" in props.door_type else 0.0
base_swing_transform = Matrix.Translation(V_(swing_x_offset, 0, 0)) @ Matrix.Scale(props.overall_width, 4) base_swing_transform = Matrix.Translation(V_(swing_x_offset, props.lining_offset, 0)) @ Matrix.Scale(props.overall_width, 4)
if not self.gizmo_door_type.hide: if not self.gizmo_door_type.hide:
self.gizmo_door_type.matrix_basis = mw @ base_swing_transform self.gizmo_door_type.matrix_basis = mw @ base_swing_transform
self.gizmo_door_type.color = prefs.decorations_colour[:3] self.gizmo_door_type.color = prefs.decorations_colour[:3]
if not self.gizmo_flip_arc.hide: if not self.gizmo_flip_arc.hide:
mirror_x = Matrix.Scale(-1, 4, (0, 1, 0)) # Mirror the flip arc along Y axis to show the opposite swing direction
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_x mirror_y = Matrix.Scale(-1, 4, (0, 1, 0))
self.gizmo_flip_arc.matrix_basis = mw @ base_swing_transform @ mirror_y
def update_editing_gizmos(self, mw, props):
"""Update editing control gizmos (pen/validate/cancel) visibility and position."""
icon_z = props.overall_height + 0.5
local_transform = (
Matrix.Translation(V_(0, 0.0, icon_z))
@ Matrix.Rotation(math.radians(90), 4, (1.0, 0.0, 0.0))
@ Matrix.Scale(0.5, 4)
)
icon_matrix_base = mw @ local_transform
if props.is_editing:
self.pen_gizmo.hide = True
self.validate_gizmo.hide = False
self.validate_gizmo.matrix_basis = icon_matrix_base
self.cancel_gizmo.hide = False
cancel_local = Matrix.Translation(V_(0.5, 0.0, 0.0)) @ local_transform
self.cancel_gizmo.matrix_basis = mw @ cancel_local
else:
self.pen_gizmo.hide = False
self.pen_gizmo.matrix_basis = icon_matrix_base
self.validate_gizmo.hide = True
self.cancel_gizmo.hide = True
+18 -6
View File
@@ -214,10 +214,17 @@ class BIMModelProperties(PropertyGroup):
# Used for things like windows, other hosted furniture, and MEP # Used for things like windows, other hosted furniture, and MEP
rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows") rl2: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for windows")
# Used for plan calculation points such as in room generation # Used for plan calculation points such as in room generation
rl3: bpy.props.FloatProperty(name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation") rl3: bpy.props.FloatProperty(
name="RL", default=1, subtype="DISTANCE", description="Z offset for space calculation"
)
type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page) type_page: bpy.props.IntProperty(name="Type Page", default=1, min=1, update=update_type_page)
x_angle: bpy.props.FloatProperty( x_angle: bpy.props.FloatProperty(
name="X Angle", default=0, subtype="ANGLE", min=math.radians(-180), max=math.radians(180), update=update_x_angle name="X Angle",
default=0,
subtype="ANGLE",
min=math.radians(-180),
max=math.radians(180),
update=update_x_angle,
) )
type_name: bpy.props.StringProperty(name="Name", default="TYPEX") type_name: bpy.props.StringProperty(name="Name", default="TYPEX")
boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class") boundary_class: bpy.props.EnumProperty(items=get_boundary_class, name="Boundary Class")
@@ -238,7 +245,9 @@ class BIMModelProperties(PropertyGroup):
default="TOP", default="TOP",
description="Offset convention to reference line", description="Offset convention to reference line",
) )
offset: bpy.props.FloatProperty(name="Offset", default=0.0, description="Material usage offset from reference line") offset: bpy.props.FloatProperty(
name="Offset", default=0.0, description="Material usage offset from reference line"
)
show_wall_axis: bpy.props.BoolProperty( show_wall_axis: bpy.props.BoolProperty(
name="Show Wall Axis", name="Show Wall Axis",
default=False, default=False,
@@ -717,9 +726,11 @@ class BIMWindowProperties(PropertyGroup):
overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6, subtype="DISTANCE", update=update_window) overall_width: bpy.props.FloatProperty(name="Overall Width", default=0.6, subtype="DISTANCE", update=update_window)
# lining properties # lining properties
lining_depth: bpy.props.FloatProperty(name="Lining Depth", default=0.050, subtype="DISTANCE", update=update_window) lining_depth: bpy.props.FloatProperty(
name="Lining Depth", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
)
lining_thickness: bpy.props.FloatProperty( lining_thickness: bpy.props.FloatProperty(
name="Lining Thickness", default=0.050, subtype="DISTANCE", update=update_window name="Lining Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
) )
lining_offset: bpy.props.FloatProperty( lining_offset: bpy.props.FloatProperty(
name="Lining Offset", default=0.050, subtype="DISTANCE", update=update_window name="Lining Offset", default=0.050, subtype="DISTANCE", update=update_window
@@ -748,12 +759,13 @@ class BIMWindowProperties(PropertyGroup):
update=update_window, update=update_window,
) )
transom_thickness: bpy.props.FloatProperty( transom_thickness: bpy.props.FloatProperty(
name="Transom Thickness", default=0.050, subtype="DISTANCE", update=update_window name="Transom Thickness", default=0.050, min=0.001, subtype="DISTANCE", update=update_window
) )
first_transom_offset: bpy.props.FloatProperty( first_transom_offset: bpy.props.FloatProperty(
name="First Transom Offset", name="First Transom Offset",
description="Distance from the first lining to the first transom center", description="Distance from the first lining to the first transom center",
default=0.3, default=0.3,
min=0.001,
subtype="DISTANCE", subtype="DISTANCE",
update=update_window, update=update_window,
) )
+310 -1
View File
@@ -19,6 +19,7 @@
import bpy import bpy
import json import json
import bmesh import bmesh
import math
import ifcopenshell import ifcopenshell
import ifcopenshell.api.pset import ifcopenshell.api.pset
import ifcopenshell.util.element import ifcopenshell.util.element
@@ -26,7 +27,9 @@ import ifcopenshell.util.representation
import ifcopenshell.util.unit import ifcopenshell.util.unit
import bonsai.core.root import bonsai.core.root
import bonsai.tool as tool import bonsai.tool as tool
from mathutils import Vector from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from mathutils import Vector, Matrix
from bmesh.types import BMVert from bmesh.types import BMVert
from bpy.types import Operator from bpy.types import Operator
from bpy.props import FloatProperty, IntProperty from bpy.props import FloatProperty, IntProperty
@@ -293,3 +296,309 @@ class RemoveStair(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset) ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"} return {"FINISHED"}
class ToggleStairTotalLengthLock(bpy.types.Operator):
"""Toggle the total length lock for stair editing"""
bl_idname = "bim.toggle_stair_total_length_lock"
bl_label = "Toggle Stair Total Length 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.total_length_lock = not props.total_length_lock
return {"FINISHED"}
class AdjustStairTreads(bpy.types.Operator):
"""Adjust the number of treads"""
bl_idname = "bim.adjust_stair_treads"
bl_label = "Adjust Stair Treads"
bl_options = {"REGISTER", "UNDO"}
increment: IntProperty(name="Increment", default=1)
def execute(self, context):
obj = context.active_object
if not obj:
return {"CANCELLED"}
props = tool.Model.get_stair_props(obj)
new_value = props.number_of_treads + self.increment
if new_value >= 1:
props.number_of_treads = new_value
return {"FINISHED"}
class CycleStairType(bpy.types.Operator):
"""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"})
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)
types = ["CONCRETE", "WOOD/STEEL", "GENERIC"]
try:
current_idx = types.index(props.stair_type)
except ValueError:
current_idx = 0
direction = -1 if self.reverse else 1
props.stair_type = types[(current_idx + direction) % len(types)]
return {"FINISHED"}
class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_stair_edition"
bl_label = "Stair Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
enable_editing_operator = "bim.enable_editing_stair"
finish_editing_operator = "bim.finish_editing_stair"
cancel_editing_operator = "bim.cancel_editing_stair"
gizmo_props = [
GizmoPropConfig("width", (0, 1, 0)),
GizmoPropConfig("height", (0, 0, 1)),
GizmoPropConfig("tread_run", (1, 0, 0)),
GizmoPropConfig("tread_depth", (0, 0, -1)),
GizmoPropConfig("nosing_length", (-1, 0, 0)),
GizmoPropConfig("nosing_depth", (0, 0, -1)),
GizmoPropConfig("total_length_target", (1, 0, 0)),
GizmoPropConfig("base_slab_depth", (0, 0, -1)),
GizmoPropConfig("top_slab_depth", (0, 0, -1)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_stair(element)
def get_props(self, obj):
return tool.Model.get_stair_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.stair
def should_hide_gizmo(self, attr_name, props):
"""Stair-specific visibility rules for gizmos."""
if not props.is_editing:
return True
# Hide concrete-specific gizmos for non-concrete stairs
if attr_name in ("base_slab_depth", "top_slab_depth") and props.stair_type != "CONCRETE":
return True
# Hide tread_depth for generic stairs (has no tread geometry)
if attr_name == "tread_depth" and props.stair_type == "GENERIC":
return True
# Hide nosing_depth when nosing_length is 0 or for Wood/Steel stair types
if attr_name == "nosing_depth" and (props.nosing_length == 0.0 or props.stair_type == "WOOD/STEEL"):
return True
return False
@staticmethod
def _get_stair_total_run(props) -> float:
"""Calculate the total horizontal run of the stair."""
return props.tread_run * props.number_of_treads
@staticmethod
def _get_first_riser_height(props) -> float:
"""Calculate the height of the first riser."""
return props.height / (props.number_of_treads + 1)
def get_gizmo_matrix_width(self, props):
translation = Matrix.Translation(Vector((0, props.width, 0)))
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_height(self, props):
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height)))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_tread_run(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((props.tread_run, props.width / 2, riser_height)))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_tread_depth(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((props.tread_run / 2, props.width / 2, riser_height - props.tread_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_nosing_length(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((-props.nosing_length, props.width / 2, riser_height)))
rotation = self.get_axis_rotation_matrix((-1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_nosing_depth(self, props):
riser_height = self._get_first_riser_height(props)
translation = Matrix.Translation(Vector((-props.nosing_length, props.width / 2, riser_height - props.nosing_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_total_length_target(self, props):
translation = Matrix.Translation(Vector((props.total_length_target, props.width / 2, props.height)))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_base_slab_depth(self, props):
translation = Matrix.Translation(Vector((0, props.width / 2, -props.base_slab_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def get_gizmo_matrix_top_slab_depth(self, props):
total_run = self._get_stair_total_run(props)
translation = Matrix.Translation(Vector((total_run, props.width / 2, props.height - props.top_slab_depth)))
rotation = self.get_axis_rotation_matrix((0, 0, -1))
return translation @ rotation
def setup(self, context):
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
# Stair-specific gizmos
prefs = tool.Blender.get_addon_preferences()
highlight_color = prefs.decorator_color_selected[:3]
# Total length lock gizmo
self.lock_gizmo = self.gizmos.new("VIEW3D_GT_lock")
self.lock_gizmo.use_draw_scale = False
self.lock_gizmo.color = self.COLOR_BLUE
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")
# Plus gizmo for increasing treads
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
# Minus gizmo for decreasing treads
self.minus_gizmo = self.gizmos.new("VIEW3D_GT_minus")
self.minus_gizmo.use_draw_scale = False
self.minus_gizmo.color = self.COLOR_RED
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
# Cycle gizmo for stair type
default_color = prefs.decorations_colour[:3]
self.cycle_gizmo = self.gizmos.new("VIEW3D_GT_cycle")
self.cycle_gizmo.use_draw_scale = False
self.cycle_gizmo.color = default_color
self.cycle_gizmo.color_highlight = highlight_color
self.cycle_gizmo.alpha = 0.8
self.cycle_gizmo.target_set_operator("bim.cycle_stair_type")
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
self.update_property_gizmos(mw, props)
self.update_editing_gizmos(mw, props)
self.update_lock_gizmo(mw, props)
self.update_tread_count_gizmos(mw, props)
self.update_cycle_gizmo(mw, props)
def update_lock_gizmo(self, mw, props):
"""Update lock gizmo position and visibility."""
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
# Update color based on lock state: red when locked, green when unlocked
self.lock_gizmo.color = self.COLOR_RED if props.total_length_lock else self.COLOR_GREEN
total_run = self._get_stair_total_run(props)
lock_x = total_run + props.tread_run + 0.5
local_transform = (
Matrix.Translation(Vector((lock_x, props.width / 2, props.height)))
@ Matrix.Rotation(math.radians(90), 4, (1.0, 0.0, 0.0))
@ Matrix.Scale(0.2, 4)
)
self.lock_gizmo.matrix_basis = mw @ local_transform
def update_tread_count_gizmos(self, mw, props):
"""Update plus and minus gizmos for tread count adjustment."""
gizmo_prefs = self.get_gizmo_prefs()
# Plus gizmo - always visible when editing (if preference enabled)
self.plus_gizmo.hide = not props.is_editing or not gizmo_prefs.plus
# Minus gizmo - hidden when number_of_treads is 1 (if preference enabled)
self.minus_gizmo.hide = not props.is_editing or props.number_of_treads <= 1 or not gizmo_prefs.minus
if self.plus_gizmo.hide and self.minus_gizmo.hide:
return
total_run = self._get_stair_total_run(props)
base_x = total_run + props.tread_run + 0.5
if not self.plus_gizmo.hide:
plus_local_transform = (
Matrix.Translation(Vector((base_x + 0.25, props.width / 2, props.height + 0.15)))
@ Matrix.Rotation(math.radians(90), 4, (1.0, 0.0, 0.0))
@ Matrix.Scale(0.2, 4)
)
self.plus_gizmo.matrix_basis = mw @ plus_local_transform
if not self.minus_gizmo.hide:
minus_local_transform = (
Matrix.Translation(Vector((base_x + 0.5, props.width / 2, props.height + 0.15)))
@ Matrix.Rotation(math.radians(90), 4, (1.0, 0.0, 0.0))
@ Matrix.Scale(0.2, 4)
)
self.minus_gizmo.matrix_basis = mw @ minus_local_transform
def update_cycle_gizmo(self, mw, props):
"""Update cycle gizmo position - 0.5m above lock gizmo."""
gizmo_prefs = self.get_gizmo_prefs()
self.cycle_gizmo.hide = not props.is_editing or not gizmo_prefs.cycle
if self.cycle_gizmo.hide:
return
total_run = self._get_stair_total_run(props)
cycle_x = total_run + props.tread_run + 0.5 # Same X as lock
local_transform = (
Matrix.Translation(Vector((cycle_x, props.width / 2, props.height + 0.5))) # +0.5m above lock
@ Matrix.Rotation(math.radians(90), 4, (1.0, 0.0, 0.0))
@ Matrix.Scale(0.2, 4)
)
self.cycle_gizmo.matrix_basis = mw @ local_transform
+202 -4
View File
@@ -28,6 +28,8 @@ import ifcopenshell.api.pset
import bonsai.tool as tool import bonsai.tool as tool
import bonsai.core.root import bonsai.core.root
import bonsai.core.geometry import bonsai.core.geometry
from bonsai.bim import gizmo
from bonsai.bim.gizmo import GizmoPropConfig
from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS from ifcopenshell.api.geometry.add_window_representation import DEFAULT_PANEL_SCHEMAS
import ifcopenshell.api import ifcopenshell.api
import ifcopenshell.api.material import ifcopenshell.api.material
@@ -36,8 +38,7 @@ import ifcopenshell.util.representation
import ifcopenshell.util.shape_builder import ifcopenshell.util.shape_builder
import ifcopenshell.util.unit import ifcopenshell.util.unit
from bmesh.types import BMVert from bmesh.types import BMVert
from mathutils import Vector from mathutils import Vector, Matrix
from typing import Optional, Union
V_ = tool.Blender.V_ V_ = tool.Blender.V_
@@ -139,7 +140,7 @@ def update_window_modifier_representation(context: bpy.types.Context) -> None:
def create_bm_window_frame( def create_bm_window_frame(
bm: bmesh.types.BMesh, size: Vector, thickness: Union[float, list[float]], position: Vector = V_(0, 0, 0).freeze() bm: bmesh.types.BMesh, size: Vector, thickness: float | list[float], position: Vector = V_(0, 0, 0).freeze()
) -> list[bmesh.types.BMVert]: ) -> list[bmesh.types.BMVert]:
"""`thickness` of the profile is defined as list in the following order: """`thickness` of the profile is defined as list in the following order:
`(LEFT, TOP, RIGHT, BOTTOM)` `(LEFT, TOP, RIGHT, BOTTOM)`
@@ -226,7 +227,7 @@ def create_bm_window(
frame_thickness: float, frame_thickness: float,
glass_thickness: float, glass_thickness: float,
position: Vector, position: Vector,
x_offsets: Optional[list] = None, x_offsets: list | None = None,
) -> tuple[list[bmesh.types.BMVert], list[bmesh.types.BMVert], list[bmesh.types.BMVert]]: ) -> tuple[list[bmesh.types.BMVert], list[bmesh.types.BMVert], list[bmesh.types.BMVert]]:
"""`lining_thickness` and `x_offsets` are expected to be defined as a list, """`lining_thickness` and `x_offsets` are expected to be defined as a list,
similarly to `create_bm_window_frame` `thickness` argument""" similarly to `create_bm_window_frame` `thickness` argument"""
@@ -391,6 +392,7 @@ def update_window_modifier_bmesh(context: bpy.types.Context) -> None:
bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=bm.verts) bmesh.ops.translate(bm, vec=V_(0, lining_offset, 0), verts=bm.verts)
bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)
bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
if bpy.context.active_object.mode == "EDIT": if bpy.context.active_object.mode == "EDIT":
bmesh.update_edit_mesh(obj.data) bmesh.update_edit_mesh(obj.data)
@@ -575,3 +577,199 @@ class RemoveWindow(bpy.types.Operator, tool.Ifc.Operator):
ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset) ifcopenshell.api.pset.remove_pset(tool.Ifc.get(), product=element, pset=pset)
return {"FINISHED"} return {"FINISHED"}
class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup):
bl_idname = "OBJECT_GGT_bim_window_edition"
bl_label = "Window Editing Gizmo"
bl_space_type = "VIEW_3D"
bl_region_type = "WINDOW"
bl_options = {"3D", "PERSISTENT"}
enable_editing_operator = "bim.enable_editing_window"
finish_editing_operator = "bim.finish_editing_window"
cancel_editing_operator = "bim.cancel_editing_window"
gizmo_props = [
GizmoPropConfig("overall_height", (0, 0, 1)),
GizmoPropConfig("overall_width", (1, 0, 0)),
GizmoPropConfig("lining_depth", (0, 1, 0)),
GizmoPropConfig("lining_thickness", (1, 0, 0)),
GizmoPropConfig("lining_to_panel_offset_x", (1, 0, 0)),
GizmoPropConfig("lining_to_panel_offset_y", (0, 1, 0)),
GizmoPropConfig("mullion_thickness", (1, 0, 0)),
GizmoPropConfig("first_mullion_offset", (1, 0, 0)),
GizmoPropConfig("second_mullion_offset", (1, 0, 0)),
GizmoPropConfig("transom_thickness", (0, 0, 1)),
GizmoPropConfig("first_transom_offset", (0, 0, 1)),
GizmoPropConfig("second_transom_offset", (0, 0, 1)),
]
@classmethod
def is_element_type(cls, element) -> bool:
return tool.Blender.Modifier.is_window(element)
def get_props(self, obj):
return tool.Model.get_window_props(obj)
def get_gizmo_prefs(self):
prefs = tool.Blender.get_addon_preferences()
return prefs.gizmos.window
def should_hide_gizmo(self, attr_name, props):
"""Window-specific visibility rules for gizmos."""
if not props.is_editing:
return True
has_mullion = self._has_mullion(props)
has_second_mullion = self._has_second_mullion(props)
has_transom = self._has_transom(props)
has_second_transom = self._has_second_transom(props)
if attr_name == "mullion_thickness" and not has_mullion:
return True
if attr_name == "first_mullion_offset" and not has_mullion:
return True
if attr_name == "second_mullion_offset" and not has_second_mullion:
return True
if attr_name == "transom_thickness" and not has_transom:
return True
if attr_name == "first_transom_offset" and not has_transom:
return True
if attr_name == "second_transom_offset" and not has_second_transom:
return True
return False
def get_gizmo_matrix_overall_height(self, props):
translation = Matrix.Translation(V_(props.overall_width - 0.05, props.lining_offset, props.overall_height))
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_overall_width(self, props):
translation = Matrix.Translation(V_(props.overall_width, props.lining_offset, props.overall_height - 0.05))
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_lining_depth(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_depth + props.lining_offset, props.overall_height)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_lining_thickness(self, props):
translation = Matrix.Translation(
V_(props.lining_thickness, props.lining_depth / 2 + props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
@staticmethod
def _get_lining_to_panel_offset_y_full(props) -> float:
"""Get the full Y offset for lining-to-panel positioning."""
return (props.lining_depth - props.frame_depth[0]) + props.lining_to_panel_offset_y
def get_gizmo_matrix_lining_to_panel_offset_x(self, props):
y_full = self._get_lining_to_panel_offset_y_full(props)
translation = Matrix.Translation(
V_(props.lining_to_panel_offset_x, y_full + props.lining_offset, props.lining_thickness)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_lining_to_panel_offset_y(self, props):
y_full = self._get_lining_to_panel_offset_y_full(props)
translation = Matrix.Translation(
V_(props.lining_to_panel_offset_x, y_full + props.frame_depth[0] + props.lining_offset, props.lining_thickness)
)
rotation = self.get_axis_rotation_matrix((0, 1, 0))
return translation @ rotation
def get_gizmo_matrix_mullion_thickness(self, props):
translation = Matrix.Translation(
V_(props.first_mullion_offset + props.mullion_thickness / 2, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_first_mullion_offset(self, props):
translation = Matrix.Translation(
V_(props.first_mullion_offset, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_second_mullion_offset(self, props):
translation = Matrix.Translation(
V_(props.second_mullion_offset, props.lining_offset, props.overall_height / 2)
)
rotation = self.get_axis_rotation_matrix((1, 0, 0))
return translation @ rotation
def get_gizmo_matrix_transom_thickness(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset + props.transom_thickness / 2)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_first_transom_offset(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.first_transom_offset)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def get_gizmo_matrix_second_transom_offset(self, props):
translation = Matrix.Translation(
V_(props.overall_width / 2, props.lining_offset, props.second_transom_offset)
)
rotation = self.get_axis_rotation_matrix((0, 0, 1))
return translation @ rotation
def _has_mullion(self, props):
"""Check if the window type uses mullions (vertical dividers)."""
window_type = props.window_type
return window_type in (
"DOUBLE_PANEL_VERTICAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_VERTICAL",
)
def _has_second_mullion(self, props):
"""Check if the window type uses a second mullion."""
return props.window_type == "TRIPLE_PANEL_VERTICAL"
def _has_transom(self, props):
"""Check if the window type uses transoms (horizontal dividers)."""
window_type = props.window_type
return window_type in (
"DOUBLE_PANEL_HORIZONTAL",
"TRIPLE_PANEL_BOTTOM",
"TRIPLE_PANEL_TOP",
"TRIPLE_PANEL_LEFT",
"TRIPLE_PANEL_RIGHT",
"TRIPLE_PANEL_HORIZONTAL",
)
def _has_second_transom(self, props):
"""Check if the window type uses a second transom."""
return props.window_type == "TRIPLE_PANEL_HORIZONTAL"
def setup(self, context):
# Use base class methods for common gizmos
self.setup_property_gizmos(context)
self.setup_editing_gizmos(context)
def refresh(self, context):
obj = context.active_object
if not obj:
return
props = self.get_props(obj)
mw = obj.matrix_world
self.update_property_gizmos(mw, props)
self.update_editing_gizmos(mw, props)
+100 -1
View File
@@ -253,6 +253,8 @@ class GizmoPreferencesDoor(bpy.types.PropertyGroup):
lining_thickness: BoolProperty(name="Lining Thickness", default=True) lining_thickness: BoolProperty(name="Lining Thickness", default=True)
transom_offset: BoolProperty(name="Transom Offset", default=True) transom_offset: BoolProperty(name="Transom Offset", default=True)
transom_thickness: BoolProperty(name="Transom Thickness", default=True) transom_thickness: BoolProperty(name="Transom Thickness", default=True)
casing_thickness: BoolProperty(name="Casing Thickness", default=True)
casing_depth: BoolProperty(name="Casing Depth", default=True)
swing_arc: BoolProperty(name="Swing Arc", default=True, description="Show door swing direction arc") swing_arc: BoolProperty(name="Swing Arc", default=True, description="Show door swing direction arc")
flip_arc: BoolProperty(name="Flip Arc", default=True, description="Show flip door orientation arc") flip_arc: BoolProperty(name="Flip Arc", default=True, description="Show flip door orientation arc")
@@ -265,10 +267,76 @@ class GizmoPreferencesDoor(bpy.types.PropertyGroup):
lining_thickness: bool lining_thickness: bool
transom_offset: bool transom_offset: bool
transom_thickness: bool transom_thickness: bool
casing_thickness: bool
casing_depth: bool
swing_arc: bool swing_arc: bool
flip_arc: bool flip_arc: bool
class GizmoPreferencesWindow(bpy.types.PropertyGroup):
"""Property group for window gizmo visibility settings."""
overall_height: BoolProperty(name="Overall Height", default=True)
overall_width: BoolProperty(name="Overall Width", default=True)
lining_depth: BoolProperty(name="Lining Depth", default=True)
lining_thickness: BoolProperty(name="Lining Thickness", default=True)
lining_to_panel_offset_x: BoolProperty(name="Lining to Panel Offset X", default=True)
lining_to_panel_offset_y: BoolProperty(name="Lining to Panel Offset Y", default=True)
mullion_thickness: BoolProperty(name="Mullion Thickness", default=True)
first_mullion_offset: BoolProperty(name="First Mullion Offset", default=True)
second_mullion_offset: BoolProperty(name="Second Mullion Offset", default=True)
transom_thickness: BoolProperty(name="Transom Thickness", default=True)
first_transom_offset: BoolProperty(name="First Transom Offset", default=True)
second_transom_offset: BoolProperty(name="Second Transom Offset", default=True)
if TYPE_CHECKING:
overall_height: bool
overall_width: bool
lining_depth: bool
lining_thickness: bool
lining_to_panel_offset_x: bool
lining_to_panel_offset_y: bool
mullion_thickness: bool
first_mullion_offset: bool
second_mullion_offset: bool
transom_thickness: bool
first_transom_offset: bool
second_transom_offset: bool
class GizmoPreferencesStair(bpy.types.PropertyGroup):
"""Property group for stair gizmo visibility settings."""
width: BoolProperty(name="Width", default=True)
height: BoolProperty(name="Height", default=True)
tread_run: BoolProperty(name="Tread Run", default=True)
tread_depth: BoolProperty(name="Tread Depth", default=True)
nosing_length: BoolProperty(name="Nosing Length", default=True)
nosing_depth: BoolProperty(name="Nosing Depth", default=True)
total_length_target: BoolProperty(name="Total Length Target", default=True)
base_slab_depth: BoolProperty(name="Base Slab Depth", default=True)
top_slab_depth: BoolProperty(name="Top Slab Depth", default=True)
lock: BoolProperty(name="Total Length Lock", default=True)
plus: BoolProperty(name="Add Tread (+)", default=True)
minus: BoolProperty(name="Remove Tread (-)", default=True)
cycle: BoolProperty(name="Cycle Stair Type", default=True)
if TYPE_CHECKING:
width: bool
height: bool
tread_run: bool
tread_depth: bool
nosing_length: bool
nosing_depth: bool
total_length_target: bool
base_slab_depth: bool
top_slab_depth: bool
lock: bool
plus: bool
minus: bool
cycle: bool
class GizmoPreferences(bpy.types.PropertyGroup): class GizmoPreferences(bpy.types.PropertyGroup):
"""Property group for all gizmo visibility settings.""" """Property group for all gizmo visibility settings."""
@@ -278,10 +346,14 @@ class GizmoPreferences(bpy.types.PropertyGroup):
description="Show interactive gizmos in the 3D viewport for parametric elements", description="Show interactive gizmos in the 3D viewport for parametric elements",
) )
door: bpy.props.PointerProperty(type=GizmoPreferencesDoor) door: bpy.props.PointerProperty(type=GizmoPreferencesDoor)
window: bpy.props.PointerProperty(type=GizmoPreferencesWindow)
stair: bpy.props.PointerProperty(type=GizmoPreferencesStair)
if TYPE_CHECKING: if TYPE_CHECKING:
draw_gizmos_in_3d_viewport: bool draw_gizmos_in_3d_viewport: bool
door: GizmoPreferencesDoor door: GizmoPreferencesDoor
window: GizmoPreferencesWindow
stair: GizmoPreferencesStair
class DocPreferences(bpy.types.PropertyGroup): class DocPreferences(bpy.types.PropertyGroup):
@@ -681,13 +753,40 @@ class BIM_ADDON_preferences(bpy.types.AddonPreferences):
) )
def draw_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: def draw_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.label(text="Toggle visibility of gizmos in editing mode")
box = layout.box() box = layout.box()
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Door", self.draw_door_gizmo_parameters) bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Door", self.draw_door_gizmo_parameters)
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Window", self.draw_window_gizmo_parameters)
bonsai.bim.helper.draw_expandable_panel(box, context, "Parametric Stair", self.draw_stair_gizmo_parameters)
def draw_door_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: def draw_door_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.door import GizmoDoorEdition
door_gizmos = self.gizmos.door door_gizmos = self.gizmos.door
gizmo_prop_names = {p.attr_name for p in GizmoDoorEdition.gizmo_props}
gizmo_prop_names.update(("swing_arc", "flip_arc"))
for prop in door_gizmos.__annotations__: for prop in door_gizmos.__annotations__:
layout.prop(door_gizmos, prop) if prop in gizmo_prop_names:
layout.prop(door_gizmos, prop)
def draw_window_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.window import GizmoWindowEdition
window_gizmos = self.gizmos.window
gizmo_prop_names = {p.attr_name for p in GizmoWindowEdition.gizmo_props}
for prop in window_gizmos.__annotations__:
if prop in gizmo_prop_names:
layout.prop(window_gizmos, prop)
def draw_stair_gizmo_parameters(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
from bonsai.bim.module.model.stair import GizmoStairEdition
stair_gizmos = self.gizmos.stair
gizmo_prop_names = {p.attr_name for p in GizmoStairEdition.gizmo_props}
special_gizmo_names = {"lock", "plus", "minus"}
for prop in stair_gizmos.__annotations__:
if prop in gizmo_prop_names or prop in special_gizmo_names:
layout.prop(stair_gizmos, prop)
def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None: def draw_model_settings(self, layout: bpy.types.UILayout, context: bpy.types.Context) -> None:
layout.prop(self, "occurrence_name_style") layout.prop(self, "occurrence_name_style")