From dfb8298ecfb77c172efa36d030b535fa59eae154 Mon Sep 17 00:00:00 2001 From: Andrej730 Date: Tue, 2 Dec 2025 16:07:54 +0500 Subject: [PATCH] black . --- .../bonsai/bim/module/drawing/gizmos.py | 102 +++++++++--------- .../bonsai/bim/module/drawing/operator.py | 8 +- .../bonsai/bim/module/geometry/operator.py | 36 +++++-- src/bonsai/bonsai/bim/module/model/door.py | 74 ++++++++----- src/bonsai/bonsai/bim/module/model/opening.py | 4 +- src/bonsai/bonsai/bim/module/model/prop.py | 42 ++++++-- src/bonsai/bonsai/bim/module/model/stair.py | 84 +++++++++++---- src/bonsai/bonsai/bim/module/model/window.py | 89 +++++++++------ src/bonsai/bonsai/bim/module/project/ui.py | 3 - src/bonsai/bonsai/bim/module/type/operator.py | 26 +++-- src/bonsai/bonsai/tool/geometry.py | 53 +++++---- 11 files changed, 330 insertions(+), 191 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/drawing/gizmos.py b/src/bonsai/bonsai/bim/module/drawing/gizmos.py index e5aa3173f1..c402dde1e1 100644 --- a/src/bonsai/bonsai/bim/module/drawing/gizmos.py +++ b/src/bonsai/bonsai/bim/module/drawing/gizmos.py @@ -1558,9 +1558,7 @@ def snap_to_mesh( return _snap_manager.snap_to_mesh(location, context, active_obj, mouse_coords, include_active) -def build_snap_cache( - context: bpy.types.Context, active_obj: bpy.types.Object, include_active: bool = False -) -> None: +def build_snap_cache(context: bpy.types.Context, active_obj: bpy.types.Object, include_active: bool = False) -> None: _snap_manager.build_snap_cache(context, active_obj, include_active) @@ -2013,7 +2011,6 @@ class UglyDotGizmo(OffsetHandle, types.Gizmo): self.draw_custom_shape(self.custom_shape, select_id=select_id) - class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo): """Extrusion guides @@ -2047,7 +2044,6 @@ class ExtrusionGuidesGizmo(CustomGizmo, types.Gizmo): self.matrix_offset.col[2][2] = depth # z-scaled - class ExtrusionWidget(types.GizmoGroup): bl_idname = "bim.extrusion_widget" bl_label = "Extrusion Gizmos" @@ -2098,7 +2094,6 @@ class ExtrusionWidget(types.GizmoGroup): gz.target_set_prop("depth", prop, "value") gz.scale_value = scale_value - def refresh(self, context: bpy.types.Context) -> None: """updating gizmos""" target = context.active_object @@ -2146,10 +2141,12 @@ class ExtrusionWidget(types.GizmoGroup): scale_value /= si_conversions["thou"] return scale_value + # ============================================================================ # Core Gizmo Classes # ============================================================================ + class BIM_OT_gizmo_value_input(bpy.types.Operator): """Enter a numeric value for a gizmo property. Click or Enter to confirm, ESC to cancel.""" @@ -2462,10 +2459,7 @@ class GizmoMovable(bpy.types.Gizmo): self.keyboard_input.reset() should_invoke_keyboard = ( - not cancel - and hasattr(self, "_has_dragged") - and not self._has_dragged - and self.move_set_cb is not None + not cancel and hasattr(self, "_has_dragged") and not self._has_dragged and self.move_set_cb is not None ) if should_invoke_keyboard: @@ -2528,7 +2522,7 @@ class GizmoMovable(bpy.types.Gizmo): if not self._has_dragged and hasattr(self, "_start_mouse_pos"): dx = current_coord[0] - self._start_mouse_pos[0] dy = current_coord[1] - self._start_mouse_pos[1] - if (dx * dx + dy * dy) > (self.DRAG_THRESHOLD ** 2): + if (dx * dx + dy * dy) > (self.DRAG_THRESHOLD**2): self._has_dragged = True view_origin = region_2d_to_origin_3d(region, rv3d, current_coord) view_direction = region_2d_to_vector_3d(region, rv3d, current_coord) @@ -2576,9 +2570,7 @@ class GizmoMovable(bpy.types.Gizmo): return {"RUNNING_MODAL"} - def _handle_keyboard_input( - self, context: bpy.types.Context, event: bpy.types.Event - ) -> set[str] | None: + def _handle_keyboard_input(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str] | None: """Handle keyboard numeric input.""" kb = self.keyboard_input @@ -3266,10 +3258,16 @@ class GizmoArrow2D(GizmoMovable): return ( # Shaft - (0, -w, 0), (shaft, -w, 0), (0, w, 0), - (0, w, 0), (shaft, -w, 0), (shaft, w, 0), + (0, -w, 0), + (shaft, -w, 0), + (0, w, 0), + (0, w, 0), + (shaft, -w, 0), + (shaft, w, 0), # Head - (shaft, -hw, 0), (shaft + head, 0, 0), (shaft, hw, 0), + (shaft, -hw, 0), + (shaft + head, 0, 0), + (shaft, hw, 0), ) def setup(self) -> None: @@ -3434,14 +3432,30 @@ class GizmoDimension(GizmoMovable): """Generate a simple clickable bar shape (unit length along X).""" hw = self.HIT_WIDTH / 2 return ( - (0, -hw, -hw), (1, -hw, -hw), (0, hw, -hw), - (0, hw, -hw), (1, -hw, -hw), (1, hw, -hw), - (0, -hw, hw), (0, hw, hw), (1, -hw, hw), - (1, -hw, hw), (0, hw, hw), (1, hw, hw), - (0, -hw, -hw), (0, -hw, hw), (1, -hw, -hw), - (1, -hw, -hw), (0, -hw, hw), (1, -hw, hw), - (0, hw, -hw), (1, hw, -hw), (0, hw, hw), - (0, hw, hw), (1, hw, -hw), (1, hw, hw), + (0, -hw, -hw), + (1, -hw, -hw), + (0, hw, -hw), + (0, hw, -hw), + (1, -hw, -hw), + (1, hw, -hw), + (0, -hw, hw), + (0, hw, hw), + (1, -hw, hw), + (1, -hw, hw), + (0, hw, hw), + (1, hw, hw), + (0, -hw, -hw), + (0, -hw, hw), + (1, -hw, -hw), + (1, -hw, -hw), + (0, -hw, hw), + (1, -hw, hw), + (0, hw, -hw), + (1, hw, -hw), + (0, hw, hw), + (0, hw, hw), + (1, hw, -hw), + (1, hw, hw), ) def setup(self) -> None: @@ -3483,9 +3497,7 @@ class GizmoDimension(GizmoMovable): display_value=getattr(self, "_display_value", self._dimension_length), ) - def _calculate_screen_endpoints( - self, context: bpy.types.Context - ) -> tuple[Vector, Vector, Vector, float] | None: + def _calculate_screen_endpoints(self, context: bpy.types.Context) -> tuple[Vector, Vector, Vector, float] | None: """Calculate screen-space endpoints and direction for the dimension line. Returns: @@ -3726,7 +3738,7 @@ class GizmoDimension(GizmoMovable): if not self._has_dragged and hasattr(self, "_start_mouse_pos"): dx = current_coord[0] - self._start_mouse_pos[0] dy = current_coord[1] - self._start_mouse_pos[1] - if (dx * dx + dy * dy) > (self.DRAG_THRESHOLD ** 2): + if (dx * dx + dy * dy) > (self.DRAG_THRESHOLD**2): self._has_dragged = True view_origin = region_2d_to_origin_3d(region, rv3d, current_coord) view_direction = region_2d_to_vector_3d(region, rv3d, current_coord) @@ -3815,10 +3827,7 @@ class GizmoDimension(GizmoMovable): self.keyboard_input.reset() should_invoke_keyboard = ( - not cancel - and hasattr(self, "_has_dragged") - and not self._has_dragged - and self.move_set_cb is not None + not cancel and hasattr(self, "_has_dragged") and not self._has_dragged and self.move_set_cb is not None ) if should_invoke_keyboard: @@ -3989,7 +3998,7 @@ class BaseParametricGizmoGroup: GIZMO_CLAMP_MAX = 10000.0 # Maximum value for dimension clamping (meters) # Pre-computed flip matrix for negative value handling (180° rotation around Z) - FLIP_MATRIX = Matrix.Rotation(math.pi, 4, 'Z') + FLIP_MATRIX = Matrix.Rotation(math.pi, 4, "Z") # === Icon Gizmo Layout (meters) === # Icons are positioned in a horizontal row above the element: @@ -4073,9 +4082,7 @@ class BaseParametricGizmoGroup: from_neg_y, from_neg_x = self.get_local_view_direction(context, world_matrix) return ViewDirection(from_negative_y=from_neg_y, from_negative_x=from_neg_x) - def update_gizmo_visibility( - self, gizmo: bpy.types.Gizmo, is_editing: bool, pref_enabled: bool - ) -> bool: + def update_gizmo_visibility(self, gizmo: bpy.types.Gizmo, is_editing: bool, pref_enabled: bool) -> bool: """Update gizmo visibility based on modal state, editing state, and preference. Consolidates the common pattern: @@ -4154,9 +4161,7 @@ class BaseParametricGizmoGroup: """ return Matrix.Translation(translation) @ self.get_axis_rotation_matrix(axis) - def get_lining_y_position_for_view( - self, props, viewing_from_negative_y: bool, use_offset: bool = True - ) -> float: + def get_lining_y_position_for_view(self, props, viewing_from_negative_y: bool, use_offset: bool = True) -> float: """Get Y position for lining-based elements (doors, windows) based on view direction. For elements with lining_offset property, this calculates the Y position @@ -4207,9 +4212,7 @@ class BaseParametricGizmoGroup: Override in subclass if different positioning is needed. """ width = getattr(props, "overall_width", 0) - return self.compose_gizmo_matrix( - Vector((width + self.GIZMO_OFFSET, 0, -self.GIZMO_OFFSET)), (0, 1, 0) - ) + return self.compose_gizmo_matrix(Vector((width + self.GIZMO_OFFSET, 0, -self.GIZMO_OFFSET)), (0, 1, 0)) def get_casing_offset(self, props) -> float: """Get casing offset for view-dependent dimension positioning. @@ -4219,9 +4222,7 @@ class BaseParametricGizmoGroup: """ return 0.0 - def _update_view_dependent_dimensions( - self, context: bpy.types.Context, mw: Matrix, props - ) -> None: + def _update_view_dependent_dimensions(self, context: bpy.types.Context, mw: Matrix, props) -> None: """Update overall_width, overall_height, and lining_offset based on view direction. This base implementation handles the common pattern for door/window gizmos. @@ -4358,9 +4359,7 @@ class BaseParametricGizmoGroup: self.update_dimension_gizmos(mw, props) self._refresh_element_specific(context, mw, props) - def _refresh_element_specific( - self, context: bpy.types.Context, mw: "Matrix", props # noqa: ARG002 - ) -> None: + def _refresh_element_specific(self, context: bpy.types.Context, mw: "Matrix", props) -> None: # noqa: ARG002 """Override for element-specific refresh logic. Called after update_editing_gizmos and update_dimension_gizmos. @@ -4383,6 +4382,7 @@ class BaseParametricGizmoGroup: if self.props_getter: return getattr(tool.Model, self.props_getter)(obj) raise NotImplementedError("Subclass must define props_getter or override get_props()") + @staticmethod def get_addon_prefs(): """Get addon preferences (cached accessor).""" @@ -4803,7 +4803,9 @@ class BaseParametricGizmoGroup: self.cycle_gizmo.hide = self.is_gizmo_hidden_by_modal(self.cycle_gizmo) local_pos_cycle = Vector((self.ICON_VALIDATE_X + self.ICON_CYCLE_X, icon_y, icon_z)) world_pos_cycle = mw @ local_pos_cycle - self.cycle_gizmo.matrix_basis = Matrix.Translation(world_pos_cycle) @ billboard_rot @ Matrix.Scale(0.30, 4) + self.cycle_gizmo.matrix_basis = ( + Matrix.Translation(world_pos_cycle) @ billboard_rot @ Matrix.Scale(0.30, 4) + ) else: self.pen_gizmo.hide = self.is_gizmo_hidden_by_modal(self.pen_gizmo) self.pen_gizmo.matrix_basis = icon_matrix_base diff --git a/src/bonsai/bonsai/bim/module/drawing/operator.py b/src/bonsai/bonsai/bim/module/drawing/operator.py index db46b08e60..0648e58a1f 100644 --- a/src/bonsai/bonsai/bim/module/drawing/operator.py +++ b/src/bonsai/bonsai/bim/module/drawing/operator.py @@ -1007,7 +1007,7 @@ class CreateDrawing(bpy.types.Operator): for polygon in closed_polygons.geoms: # Less than 0.1mm2 is not worth styling on sheet - if polygon.area < .1: + if polygon.area < 0.1: continue centroid = polygon.centroid centroid = centroid if polygon.contains(centroid) else polygon.representative_point() @@ -3275,15 +3275,15 @@ class AssignSelectedObjectAsProduct(bpy.types.Operator): element1 = tool.Ifc.get_entity(obj1) element2 = tool.Ifc.get_entity(obj2) assert element1 and element2 - + # Check if at least one object is an IfcAnnotation is_annotation1 = element1.is_a("IfcAnnotation") is_annotation2 = element2.is_a("IfcAnnotation") - + if not (is_annotation1 or is_annotation2): self.report({"ERROR"}, "At least one of the selected objects must be IfcAnnotation.") return {"CANCELLED"} - + # If both are annotations, use the currently active object as relating product if is_annotation1 and is_annotation2: active_obj = context.active_object diff --git a/src/bonsai/bonsai/bim/module/geometry/operator.py b/src/bonsai/bonsai/bim/module/geometry/operator.py index ca54376c5f..174406a5da 100644 --- a/src/bonsai/bonsai/bim/module/geometry/operator.py +++ b/src/bonsai/bonsai/bim/module/geometry/operator.py @@ -2409,14 +2409,14 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): props = tool.Geometry.get_geometry_props() item = tool.Geometry.get_active_representation(obj) assert item - + # Fix vertex order for annotation items rep_obj = props.representation_obj if rep_obj: element = tool.Ifc.get_entity(rep_obj) if element and self._is_annotation_object(element): tool.Geometry.ensure_annotation_vertex_order(obj) - + if tool.Geometry.is_meshlike_item(item): if tool.Geometry.is_geometric_data(obj.data) and ( item.is_a("IfcVertex") or item.is_a("IfcEdge") or obj.data.polygons @@ -2560,12 +2560,12 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): """Check if element is an annotation object that needs vertex order preservation.""" if not element.is_a("IfcAnnotation"): return False - + # Object types that have semantic vertex order (arrow direction, text position) annotation_types_with_order = { "TEXT_LEADER", "DIMENSION", - "RADIUS", + "RADIUS", "DIAMETER", "ANGLE", "FALL", @@ -2578,7 +2578,7 @@ class OverrideModeSetObject(bpy.types.Operator, tool.Ifc.Operator): "SECTION", "ELEVATION", } - + return element.ObjectType in annotation_types_with_order @@ -3408,11 +3408,27 @@ class ImportRepresentationItems(bpy.types.Operator, tool.Ifc.Operator): tool.Root.reload_item_decorator() element = tool.Ifc.get_entity(obj) - if element and element.is_a("IfcAnnotation") and element.ObjectType in { - "TEXT_LEADER", "DIMENSION", "RADIUS", "DIAMETER", "ANGLE", - "FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT", - "STAIR_ARROW", "PLAN_LEVEL", "SECTION_LEVEL", "SECTION", "ELEVATION" - }: + if ( + element + and element.is_a("IfcAnnotation") + and element.ObjectType + in { + "TEXT_LEADER", + "DIMENSION", + "RADIUS", + "DIAMETER", + "ANGLE", + "FALL", + "SLOPE_ANGLE", + "SLOPE_FRACTION", + "SLOPE_PERCENT", + "STAIR_ARROW", + "PLAN_LEVEL", + "SECTION_LEVEL", + "SECTION", + "ELEVATION", + } + ): for item_obj_data in props.item_objs: item_obj = item_obj_data.obj if item_obj and isinstance(item_obj.data, bpy.types.Mesh) and item_obj.data.vertices: diff --git a/src/bonsai/bonsai/bim/module/model/door.py b/src/bonsai/bonsai/bim/module/model/door.py index ee9230b016..26d1a1a7fb 100644 --- a/src/bonsai/bonsai/bim/module/model/door.py +++ b/src/bonsai/bonsai/bim/module/model/door.py @@ -779,89 +779,105 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): # 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, + 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", + 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), + 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), + 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), + 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, + 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), + 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), + 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), + 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), + 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), + 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 + 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), + 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 + 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), + 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() + p.threshold_thickness + p.get_panel_center_z(), ), ), DimensionGizmoConfig( - attr_name="frame_thickness", axis=(-1, 0, 0), + 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() + p.get_transom_window_center_z(), ), ), DimensionGizmoConfig( - attr_name="frame_depth", axis=(0, 1, 0), + 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() + p.get_transom_window_center_z(), ), ), ] @@ -895,12 +911,14 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): special_color = prefs.decorator_color_special[:3] self.gizmo_door_type = self.create_arc_gizmo( - special_color, "bim.toggle_door_swing", + 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", + inactive_color, + "bim.toggle_door_swing", prop_path="BIMDoorProperties.door_type", flip_geometry=True, flip_local_axes="XY", @@ -916,7 +934,9 @@ class GizmoDoorEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): """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: + def _update_dimension_gizmo_positions( + self, context: bpy.types.Context, mw: Matrix, props: "BIMDoorProperties" + ) -> None: """Update dimension gizmo positions based on camera view direction.""" self._update_view_dependent_dimensions(context, mw, props) diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index 11c80041e6..958716954b 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -417,12 +417,14 @@ class RecalculateFill(bpy.types.Operator, tool.Ifc.Operator): representation=representation, ) - #Refresh cut decorator + # Refresh cut decorator DecoratorData.cut_cache.clear() DecoratorData.fill_cache.clear() DecoratorData.slice_cache.clear() return {"FINISHED"} + + class FlipFill(bpy.types.Operator, tool.Ifc.Operator): bl_idname = "bim.flip_fill" bl_label = "Flip Fill" diff --git a/src/bonsai/bonsai/bim/module/model/prop.py b/src/bonsai/bonsai/bim/module/model/prop.py index 0689d4a100..ce657c5925 100644 --- a/src/bonsai/bonsai/bim/module/model/prop.py +++ b/src/bonsai/bonsai/bim/module/model/prop.py @@ -171,6 +171,7 @@ def _get_updater(module_name: str, func_name: str) -> Callable: 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] @@ -476,12 +477,18 @@ 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", update=update_stair) + 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", update=update_stair) - top_slab_depth: bpy.props.FloatProperty(name="Top Slab Depth", default=0.25, min=0, subtype="DISTANCE", update=update_stair) + 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", @@ -515,7 +522,12 @@ 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", update=update_stair + name="Nosing Depth", + description="Depth of the tread's nosing", + min=0, + default=0, + unit="LENGTH", + update=update_stair, ) if TYPE_CHECKING: @@ -1087,7 +1099,7 @@ class BIMWindowProperties(PropertyGroup): 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:]) + new_value = tuple(current[:panel_index]) + (max(0.0, value),) + tuple(current[panel_index + 1 :]) setattr(self, attr_name, new_value) @@ -1401,7 +1413,10 @@ 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", update=update_railing + 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", update=update_railing) thickness: bpy.props.FloatProperty(name="Thickness", default=0.050, subtype="DISTANCE", update=update_railing) @@ -1423,7 +1438,9 @@ class BIMRailingProperties(PropertyGroup): subtype="DISTANCE", update=update_railing, ) - railing_diameter: bpy.props.FloatProperty(name="Railing Diameter", default=0.050, subtype="DISTANCE", update=update_railing) + 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, @@ -1529,10 +1546,17 @@ class BIMRoofProperties(PropertyGroup): 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", update=update_roof + 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", update=update_roof + 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", diff --git a/src/bonsai/bonsai/bim/module/model/stair.py b/src/bonsai/bonsai/bim/module/model/stair.py index bf41e8d643..91ddeccbc3 100644 --- a/src/bonsai/bonsai/bim/module/model/stair.py +++ b/src/bonsai/bonsai/bim/module/model/stair.py @@ -488,11 +488,16 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): 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", + 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, + attr_name="width", + axis=(0, 1, 0), + min_value=0.01, matrix_position=lambda p: V_(_G.GIZMO_OFFSET, 0, -_G.GIZMO_OFFSET), ), DimensionGizmoConfig( @@ -503,7 +508,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): 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 + p.get_riser_height() if p.custom_tread_lock else p.get_riser_height() * 2, ), ), DimensionGizmoConfig( @@ -524,12 +529,11 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): 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 - ), + 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), + attr_name="nosing_length", + axis=(-1, 0, 0), matrix_position=lambda p: V_(0, p.width / 2, p.get_riser_height()), ), DimensionGizmoConfig( @@ -578,12 +582,16 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): 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", + "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", + "VIEW3D_GT_lock", + (1.0, 1.0, 1.0), + "bim.toggle_stair_property", prop_path="BIMStairProperties.custom_tread_lock", property_name="custom_tread_lock", ) @@ -594,9 +602,7 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): "VIEW3D_GT_minus", self.COLOR_RED, "bim.adjust_stair_treads", increment=-1 ) - def _refresh_element_specific( - self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties" - ) -> None: + 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_lock_gizmo(mw, props, billboard_rot) @@ -637,7 +643,9 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): self.minus_gizmo, props.is_editing and props.number_of_treads > 1, gizmo_prefs.minus ) - def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: Matrix, props: "BIMStairProperties") -> 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) @@ -651,7 +659,12 @@ 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: "BIMStairProperties", 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).""" y_pos_offset = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=True) @@ -662,7 +675,12 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): 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: "BIMStairProperties", 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).""" y_pos = self.get_y_position_for_view(props, viewing_from_negative_y, use_offset=False) @@ -689,18 +707,33 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): self.set_dimension_gizmo_position("nosing_length", mw, V_(0, props.width / 2, riser_height), (-1, 0, 0)) self.set_dimension_gizmo_position("tread_depth", mw, V_(0, y_pos, riser_height), (0, 0, -1)) self.set_dimension_gizmo_position("riser_height", mw, V_(props.tread_run, y_pos, 0), (0, 0, 1)) - self.set_dimension_gizmo_position("nosing_depth", mw, V_(-props.nosing_length, props.width / 2, riser_height), (0, 0, -1)) + 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: "BIMStairProperties", 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 + self.ICON_Z_OFFSET, 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: Matrix, props: "BIMStairProperties", viewing_from_negative_y: bool, billboard_rot: Matrix) -> None: + 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 @@ -710,10 +743,17 @@ class GizmoStairEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): self.set_icon_gizmo_position("validate_gizmo", mw, 0, y_pos, icon_z, billboard_rot) self.set_icon_gizmo_position("cancel_gizmo", mw, self.ICON_CANCEL_X, y_pos, icon_z, billboard_rot) - self.set_icon_gizmo_position("cycle_gizmo", mw, self.ICON_CYCLE_X, y_pos, icon_z, billboard_rot, scale=self.ICON_CYCLE_SCALE) self.set_icon_gizmo_position( - "tread_lock_gizmo", mw, self.ICON_TREAD_LOCK_X, y_pos, - icon_z - self.EDITING_ICON_SCALE / 2, billboard_rot, scale=self.EDITING_ICON_SCALE + "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, ) self.set_icon_gizmo_position( "plus_gizmo", mw, self.ICON_PLUS_X, y_pos, icon_z, billboard_rot, scale=self.ICON_PLUS_MINUS_SCALE diff --git a/src/bonsai/bonsai/bim/module/model/window.py b/src/bonsai/bonsai/bim/module/model/window.py index 274d4288d9..206f65de08 100644 --- a/src/bonsai/bonsai/bim/module/model/window.py +++ b/src/bonsai/bonsai/bim/module/model/window.py @@ -605,9 +605,10 @@ class CycleWindowType(bpy.types.Operator, tool.Ifc.Operator, gizmo.CycleTypeMixi # 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]"]: +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: @@ -624,9 +625,7 @@ def _make_frame_accessors( _frame_accessors = { - (attr, idx): _make_frame_accessors(attr, idx) - for attr in ("frame_depth", "frame_thickness") - for idx in range(3) + (attr, idx): _make_frame_accessors(attr, idx) for attr in ("frame_depth", "frame_thickness") for idx in range(3) } @@ -645,112 +644,137 @@ class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): # 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, + 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="overall_height", axis=(0, 0, 1), min_value=0.01, text_alignment="start", + 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="lining_depth", axis=(0, 1, 0), + 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), + 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), + 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 + p.lining_to_panel_offset_x, ), ), DimensionGizmoConfig( - attr_name="lining_to_panel_offset_y", axis=(0, 1, 0), min_value=-10.0, + 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 + p.lining_to_panel_offset_x, ), ), DimensionGizmoConfig( - attr_name="frame_depth", axis=(0, -1, 0), + 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), + 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), + 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), + 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), + 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), + 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, + 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 + p.overall_height / 2 + 3 * _G.GIZMO_STACK_OFFSET, ), ), DimensionGizmoConfig( - attr_name="first_mullion_offset", axis=(1, 0, 0), + 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), + 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, + 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 + p.first_transom_offset - p.transom_thickness / 2, ), ), DimensionGizmoConfig( - attr_name="first_transom_offset", axis=(0, 0, 1), + 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), + 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), ), @@ -772,17 +796,16 @@ class GizmoWindowEdition(bpy.types.GizmoGroup, gizmo.BaseParametricGizmoGroup): 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 + max(0, props.lining_offset) + props.lining_depth + props.lining_to_panel_offset_y + 2 * self.GIZMO_OFFSET ) furthest_negative_y = abs(min(0, props.lining_offset)) + 2 * self.GIZMO_OFFSET return (furthest_positive_y, furthest_negative_y) # Window uses base class setup() and refresh() - no element-specific gizmos needed - def _update_dimension_gizmo_positions(self, context: bpy.types.Context, mw: Matrix, props: "BIMWindowProperties") -> 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.""" # Window uses base implementation with default casing_offset=0 self._update_view_dependent_dimensions(context, mw, props) diff --git a/src/bonsai/bonsai/bim/module/project/ui.py b/src/bonsai/bonsai/bim/module/project/ui.py index 505816b49b..358f64e681 100644 --- a/src/bonsai/bonsai/bim/module/project/ui.py +++ b/src/bonsai/bonsai/bim/module/project/ui.py @@ -673,6 +673,3 @@ class BIM_PT_purge(Panel): row.prop(ifc_parameter, "name", text="") row.prop(ifc_parameter, "value", text="") row.operator("bim.update_parametric_representation", icon="FILE_REFRESH", text="").index = index - - - diff --git a/src/bonsai/bonsai/bim/module/type/operator.py b/src/bonsai/bonsai/bim/module/type/operator.py index 3801923296..60a52fdb9b 100644 --- a/src/bonsai/bonsai/bim/module/type/operator.py +++ b/src/bonsai/bonsai/bim/module/type/operator.py @@ -58,7 +58,7 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator): else: related_objects = tool.Blender.get_selected_objects() prefs = tool.Blender.get_addon_preferences() - + # Get the active drawing's target view active_target_view = None drawing_props = context.scene.DocProperties @@ -66,13 +66,13 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator): active_drawing = tool.Ifc.get().by_id(drawing_props.active_drawing_id) if active_drawing: active_target_view = tool.Drawing.get_drawing_target_view(active_drawing) - + for obj in related_objects: element = tool.Ifc.get_entity(obj) if not element or not element.is_a("IfcObject"): continue core.assign_type(tool.Ifc, tool.Type, element=element, type=relating_type) - + # Switch to the drawing's target view if available if active_target_view and element.Representation: for rep in element.Representation.Representations: @@ -84,7 +84,7 @@ class AssignType(bpy.types.Operator, tool.Ifc.Operator): representation=rep, ) break - + if prefs.occurrence_name_style == "TYPE": obj.name = tool.Model.generate_occurrence_name(relating_type, element.is_a()) @@ -363,17 +363,17 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): if obj.data: new_obj.data = obj.data.copy() new = bonsai.core.root.copy_class(tool.Ifc, tool.Collector, tool.Geometry, tool.Root, obj=new_obj) - + # Apply the name and description from the dialog new.Name = self.name if self.description: new.Description = self.description - + # Update the Blender object name to match the IFC element name tool.Root.set_object_name(new_obj, new) - + bpy.ops.bim.load_type_thumbnails() - + # Assign selected objects to the new type if requested if self.assign_selected_objects: selected_objects = tool.Blender.get_selected_objects() @@ -384,7 +384,7 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): core.assign_type(tool.Ifc, tool.Type, element=selected_element, type=new) if prefs.occurrence_name_style == "TYPE": selected_obj.name = tool.Model.generate_occurrence_name(new, selected_element.is_a()) - + if obj in context.selectable_objects: tool.Blender.select_and_activate_single_object(context, new_obj) else: @@ -409,6 +409,10 @@ class DuplicateType(bpy.types.Operator, tool.Ifc.Operator): self.layout.prop(self, "name") self.layout.prop(self, "description") selected_objects = tool.Blender.get_selected_objects() - ifc_objects = [obj for obj in selected_objects if tool.Ifc.get_entity(obj) and tool.Ifc.get_entity(obj).is_a("IfcObject")] + ifc_objects = [ + obj for obj in selected_objects if tool.Ifc.get_entity(obj) and tool.Ifc.get_entity(obj).is_a("IfcObject") + ] if ifc_objects: - self.layout.prop(self, "assign_selected_objects", text=f"Assign {len(ifc_objects)} Selected Object(s) to New Type") + self.layout.prop( + self, "assign_selected_objects", text=f"Assign {len(ifc_objects)} Selected Object(s) to New Type" + ) diff --git a/src/bonsai/bonsai/tool/geometry.py b/src/bonsai/bonsai/tool/geometry.py index 5d77b6c323..e2955d3d28 100644 --- a/src/bonsai/bonsai/tool/geometry.py +++ b/src/bonsai/bonsai/tool/geometry.py @@ -1907,9 +1907,20 @@ class Geometry(bonsai.core.tool.Geometry): # ADD THIS AT THE END - Store initial vertex order for annotations if rep_obj and (element := tool.Ifc.get_entity(rep_obj)): if element.is_a("IfcAnnotation") and element.ObjectType in { - "TEXT_LEADER", "DIMENSION", "RADIUS", "DIAMETER", "ANGLE", - "FALL", "SLOPE_ANGLE", "SLOPE_FRACTION", "SLOPE_PERCENT", - "STAIR_ARROW", "PLAN_LEVEL", "SECTION_LEVEL", "SECTION", "ELEVATION" + "TEXT_LEADER", + "DIMENSION", + "RADIUS", + "DIAMETER", + "ANGLE", + "FALL", + "SLOPE_ANGLE", + "SLOPE_FRACTION", + "SLOPE_PERCENT", + "STAIR_ARROW", + "PLAN_LEVEL", + "SECTION_LEVEL", + "SECTION", + "ELEVATION", }: # Store the initial first vertex position if isinstance(obj.data, bpy.types.Mesh) and obj.data.vertices: @@ -2361,7 +2372,7 @@ class Geometry(bonsai.core.tool.Geometry): mesh = obj.data if not isinstance(mesh, bpy.types.Mesh): return - + # Get the original first vertex position from custom properties if "bonsai_first_vert_co" in mesh: original_first_co = Vector(mesh["bonsai_first_vert_co"]) @@ -2372,19 +2383,19 @@ class Geometry(bonsai.core.tool.Geometry): mesh["bonsai_first_vert_co"] = original_first_co[:] else: return - + bm = bmesh.new() bm.from_mesh(mesh) bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() - + if len(bm.verts) == 0: bm.free() return - + # Find endpoints (vertices with only one connected edge) endpoints = [v for v in bm.verts if len(v.link_edges) == 1] - + # Choose the endpoint closest to the original first vertex position if len(endpoints) == 0: # Closed loop - pick any vertex as start @@ -2395,12 +2406,12 @@ class Geometry(bonsai.core.tool.Geometry): else: # Choose endpoint closest to where the original first vertex was start_vert = min(endpoints, key=lambda v: (v.co - original_first_co).length) - + # Build ordered vertex list by following edges ordered_verts = [start_vert] current_vert = start_vert visited_edges = set() - + while True: # Find next unvisited edge next_edge = None @@ -2408,37 +2419,37 @@ class Geometry(bonsai.core.tool.Geometry): if edge not in visited_edges: next_edge = edge break - + if not next_edge: break - + visited_edges.add(next_edge) next_vert = next_edge.other_vert(current_vert) - + # Avoid going back on ourselves if next_vert not in ordered_verts: ordered_verts.append(next_vert) - + current_vert = next_vert - + # Store vertex coordinates in the correct order new_verts_co = [v.co.copy() for v in ordered_verts] - + # Update the stored first vertex position to the new first vertex mesh["bonsai_first_vert_co"] = new_verts_co[0][:] - + # Clear and rebuild mesh with correct vertex order bm.clear() - + # Create new vertices in order new_verts = [bm.verts.new(co) for co in new_verts_co] bm.verts.ensure_lookup_table() - + # Create edges connecting consecutive vertices for i in range(len(new_verts) - 1): bm.edges.new([new_verts[i], new_verts[i + 1]]) - + # Write back to mesh bm.to_mesh(mesh) bm.free() - mesh.update() \ No newline at end of file + mesh.update()