diff --git a/src/bonsai/bonsai/bim/module/model/opening.py b/src/bonsai/bonsai/bim/module/model/opening.py index cc69586191..c46ac25094 100644 --- a/src/bonsai/bonsai/bim/module/model/opening.py +++ b/src/bonsai/bonsai/bim/module/model/opening.py @@ -229,9 +229,15 @@ class FilledOpeningGenerator: filling_obj: bpy.types.Object, voided_obj: bpy.types.Object, target: Optional[Vector] = None, + preserve_placement: bool = False, ) -> Union[None, str]: """ :param target: Target opening position. If ommited, cursor position is used. + :param preserve_placement: If True, keep ``filling_obj.matrix_world`` as-is + and skip the snap-to-wall-axis / rl1-rl2 Z-default logic. The opening + is still created at the filling's current world position. Useful + when the caller (e.g. the SHIFT-add-opening gizmo flow) has + already positioned the filling intentionally. :return: None if there was no errors, otherwise returns a string with error message. """ props = tool.Model.get_model_props() @@ -253,7 +259,7 @@ class FilledOpeningGenerator: should_set_z_level = False # Sometimes, the voided_obj may be an aggregate, which won't have any representation. - if voided_obj.data: + if not preserve_placement and voided_obj.data: raycast = voided_obj.closest_point_on_mesh(voided_obj.matrix_world.inverted() @ target, distance=0.01) if not raycast[0]: target = filling_obj.matrix_world.translation.copy() diff --git a/src/bonsai/bonsai/bim/module/void/operator.py b/src/bonsai/bonsai/bim/module/void/operator.py index c27b8891f6..4ff021630d 100644 --- a/src/bonsai/bonsai/bim/module/void/operator.py +++ b/src/bonsai/bonsai/bim/module/void/operator.py @@ -36,9 +36,17 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): bl_description = ( "Apply opening objects to an Element.\n\n" "The Element and the openings to be applied should be selected. The order of selection is not important.\n" - "Opening can be just a Blender mesh object." + "Opening can be just a Blender mesh object.\n\n" + "Shift+click: keep the filling at its current matrix_world — skip the wall-axis snap " + "and the rl1/rl2 Z-elevation default that the regular click applies." ) + # Toggled by ``invoke`` when the user holds SHIFT during a gizmo / hotkey + # click. Forwards to ``FilledOpeningGenerator.generate`` which gates the + # snap-to-wall-axis block on it. HIDDEN + SKIP_SAVE so it doesn't surface + # in the F6 redo panel or persist into saved keymaps. + preserve_placement: bpy.props.BoolProperty(default=False, options={"HIDDEN", "SKIP_SAVE"}) + @classmethod def poll(cls, context): if len(context.selected_objects) < 2: @@ -46,6 +54,10 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): return False return True + def invoke(self, context, event): + self.preserve_placement = bool(event.shift) + return self.execute(context) + def _execute(self, context): selected_objects = context.selected_objects target_object = selected_objects[0] @@ -68,7 +80,12 @@ class AddOpening(bpy.types.Operator, tool.Ifc.Operator): elif not element1.is_a("IfcOpeningElement") and not element2.is_a("IfcOpeningElement"): if element1.is_a("IfcWindow") or element1.is_a("IfcDoor"): # Add a fill to an element. obj1, obj2 = obj2, obj1 - FilledOpeningGenerator().generate(obj2, obj1, target=obj2.matrix_world.translation) + FilledOpeningGenerator().generate( + obj2, + obj1, + target=obj2.matrix_world.translation, + preserve_placement=self.preserve_placement, + ) continue elif element1.is_a("IfcOpeningElement") or element2.is_a("IfcOpeningElement"): if element1.is_a("IfcOpeningElement"): # Reassign an opening to another element.