From 0d39a21bf45ad04f5914d5ca8beb0d1e9bd2ed49 Mon Sep 17 00:00:00 2001 From: Ryan Schultz Date: Fri, 12 Jun 2026 14:02:44 -0500 Subject: [PATCH] closes #6235 - Add copy toggle to CAD offset Add a "Copy" option to bim.cad_offset. When enabled (the default) it offsets a new copy of the selected edges as before; when disabled it moves the existing edges to the offset location instead. The toggle is exposed in the CAD tool's Offset panel and the operator redo panel. Generated with the assistance of an AI coding tool. --- src/bonsai/bonsai/bim/module/cad/operator.py | 31 ++++++++++++++++--- src/bonsai/bonsai/bim/module/cad/prop.py | 6 ++++ src/bonsai/bonsai/bim/module/cad/workspace.py | 4 ++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/bonsai/bonsai/bim/module/cad/operator.py b/src/bonsai/bonsai/bim/module/cad/operator.py index bef1d851af..5d74857822 100644 --- a/src/bonsai/bonsai/bim/module/cad/operator.py +++ b/src/bonsai/bonsai/bim/module/cad/operator.py @@ -345,9 +345,17 @@ class CadArcFrom3Points(bpy.types.Operator): class CadOffset(bpy.types.Operator): bl_idname = "bim.cad_offset" bl_label = "CAD Offset" - bl_description = "Copy selected mesh geometry at provided offset. Mesh copied based on the current viewport angle." + bl_description = ( + "Offset selected mesh geometry at provided distance, based on the current viewport angle. " + "Creates a copy by default, or moves the existing edges if Copy is disabled." + ) bl_options = {"REGISTER", "UNDO"} distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE") + copy: bpy.props.BoolProperty( + name="Copy", + description="Create a new offset copy of the geometry. If disabled, move the existing edges to the offset location", + default=True, + ) @classmethod def poll(cls, context): @@ -405,6 +413,11 @@ class CadOffset(bpy.types.Operator): rotation = Matrix.Rotation(pi / 2, 2, "Z") rotation_i = Matrix.Rotation(-pi / 2, 2, "Z") + # When not copying, the offset positions are gathered here and applied to + # the existing verts only after all loops are processed, so that the + # original coordinates are still available while computing offsets. + moved_verts = [] + # Create loops from edges loop_edges = set(edges) loops = [] @@ -517,12 +530,15 @@ class CadOffset(bpy.types.Operator): offset_length = self.distance / sqrt((1 + normals[0].dot(normals[1])) / 2) offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ (new_normal * offset_length).to_3d()) new_vert = v1.co + offset - new_verts.append(bm.verts.new(new_vert)) else: normal = (normals[0] * self.distance).to_3d() offset = mw.inverted().to_quaternion() @ (wp.to_quaternion() @ normal) new_vert = v1.co + offset + + if self.copy: new_verts.append(bm.verts.new(new_vert)) + else: + moved_verts.append((v1, new_vert)) processed_verts.add(v1.index) @@ -531,9 +547,14 @@ class CadOffset(bpy.types.Operator): v1 = v2 - [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] - if is_closed: - bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) + if self.copy: + [bm.edges.new((new_verts[i], new_verts[i + 1])) for i in range(len(new_verts) - 1)] + if is_closed: + bm.edges.new((new_verts[len(new_verts) - 1], new_verts[0])) + + # Move the existing edges to the offset location. + for vert, new_co in moved_verts: + vert.co = new_co bm.verts.index_update() bm.edges.index_update() diff --git a/src/bonsai/bonsai/bim/module/cad/prop.py b/src/bonsai/bonsai/bim/module/cad/prop.py index 7dab36df91..aee4b9d55f 100644 --- a/src/bonsai/bonsai/bim/module/cad/prop.py +++ b/src/bonsai/bonsai/bim/module/cad/prop.py @@ -27,6 +27,11 @@ class BIMCadProperties(PropertyGroup): resolution: bpy.props.IntProperty(name="Arc Resolution", min=1, default=1) radius: bpy.props.FloatProperty(name="Radius", default=0.1, subtype="DISTANCE") distance: bpy.props.FloatProperty(name="Distance", default=0.1, subtype="DISTANCE") + copy: bpy.props.BoolProperty( + name="Copy", + description="Create a new offset copy of the geometry. If disabled, move the existing edges to the offset location", + default=True, + ) x: bpy.props.FloatProperty(name="X", default=0.2, subtype="DISTANCE") y: bpy.props.FloatProperty(name="Y", default=0.1, subtype="DISTANCE") gable_roof_edge_angle: bpy.props.FloatProperty( @@ -37,6 +42,7 @@ class BIMCadProperties(PropertyGroup): resolution: int radius: float distance: float + copy: bool x: float y: float gable_roof_edge_angle: float diff --git a/src/bonsai/bonsai/bim/module/cad/workspace.py b/src/bonsai/bonsai/bim/module/cad/workspace.py index 24fb98ba3f..270c7f70b9 100644 --- a/src/bonsai/bonsai/bim/module/cad/workspace.py +++ b/src/bonsai/bonsai/bim/module/cad/workspace.py @@ -256,6 +256,8 @@ class CadHotkey(bpy.types.Operator): elif self.hotkey == "S_O": row = self.layout.row() row.prop(props, "distance") + row = self.layout.row() + row.prop(props, "copy") elif self.hotkey == "S_R": if tool.Geometry.is_profile_object_active(): @@ -291,7 +293,7 @@ class CadHotkey(bpy.types.Operator): bpy.ops.bim.cad_fillet(resolution=self.props.resolution, radius=self.props.radius) def hotkey_S_O(self): - bpy.ops.bim.cad_offset(distance=self.props.distance) + bpy.ops.bim.cad_offset(distance=self.props.distance, copy=self.props.copy) def hotkey_S_Q(self): obj = bpy.context.active_object