import bpy, mathutils, math, os from .. import __package__ as base_package from ..functions.draw import ( carver_overlay, ) from ..functions.object import ( add_boolean_modifier, set_cutter_properties, delete_cutter, set_object_origin, ) from ..functions.mesh import ( create_cutter_shape, extrude, shade_smooth_by_angle, ) from ..functions.select import ( cursor_snap, selection_fallback, ) #### ------------------------------ /tool_shelf_draw/ ------------------------------ #### class CarverToolshelf(): def draw_settings(context, layout, tool): props = tool.operator_properties("object.carve") if context.object: mode = "OBJECT" if context.object.mode == 'OBJECT' else "EDIT_MESH" active_tool = context.workspace.tools.from_space_view3d_mode(mode, create=False).idname layout.prop(props, "mode", text="") layout.prop(props, "depth", text="") row = layout.row() row.prop(props, "solver", expand=True) if context.object: layout.popover("TOPBAR_PT_carver_shape", text="Shape") layout.popover("TOPBAR_PT_carver_array", text="Array") layout.popover("TOPBAR_PT_carver_cutter", text="Cutter") class TOPBAR_PT_carver_shape(bpy.types.Panel): bl_label = "Carver Shape" bl_idname = "TOPBAR_PT_carver_shape" bl_region_type = 'HEADER' bl_space_type = 'TOPBAR' bl_category = 'Tool' def draw(self, context): layout = self.layout layout.use_property_split = True prefs = context.preferences.addons[base_package].preferences mode = "OBJECT" if context.object.mode == 'OBJECT' else "EDIT_MESH" tool = context.workspace.tools.from_space_view3d_mode(mode, create=False) op = tool.operator_properties("object.carve") if tool.idname == "object.carve_polyline": layout.prop(op, "closed") else: if tool.idname == "object.carve_circle": layout.prop(op, "subdivision", text="Vertices") layout.prop(op, "rotation") layout.prop(op, "aspect", expand=True) layout.prop(op, "origin", expand=True) if tool.idname == 'object.carve_box': layout.separator() layout.prop(op, "use_bevel", text="Bevel") col = layout.column(align=True) row = col.row(align=True) if prefs.experimental: row.prop(op, "bevel_profile", text="Profile", expand=True) col.prop(op, "bevel_segments", text="Segments") col.prop(op, "bevel_radius", text="Radius") if op.use_bevel == False: col.enabled = False class TOPBAR_PT_carver_array(bpy.types.Panel): bl_label = "Carver Array" bl_idname = "TOPBAR_PT_carver_array" bl_region_type = 'HEADER' bl_space_type = 'TOPBAR' bl_category = 'Tool' def draw(self, context): layout = self.layout layout.use_property_split = True mode = "OBJECT" if context.object.mode == 'OBJECT' else "EDIT_MESH" tool = context.workspace.tools.from_space_view3d_mode(mode, create=False) op = tool.operator_properties("object.carve") col = layout.column(align=True) col.prop(op, "rows") row = col.row(align=True) row.prop(op, "rows_direction", text="Direction", expand=True) col.prop(op, "rows_gap", text="Gap") layout.separator() col = layout.column(align=True) col.prop(op, "columns") row = col.row(align=True) row.prop(op, "columns_direction", text="Direction", expand=True) col.prop(op, "columns_gap", text="Gap") class TOPBAR_PT_carver_cutter(bpy.types.Panel): bl_label = "Carver Cutter" bl_idname = "TOPBAR_PT_carver_cutter" bl_region_type = 'HEADER' bl_space_type = 'TOPBAR' bl_category = 'Tool' def draw(self, context): layout = self.layout layout.use_property_split = True mode = "OBJECT" if context.object.mode == 'OBJECT' else "EDIT_MESH" tool = context.workspace.tools.from_space_view3d_mode(mode, create=False) op = tool.operator_properties("object.carve") col = layout.column() col.prop(op, "pin", text="Pin Modifier") col.prop(op, "parent") if op.mode == 'MODIFIER': col.prop(op, "hide") # auto_smooth layout.separator() col = layout.column(align=True) col.prop(op, "auto_smooth", text="Auto Smooth") col.prop(op, "sharp_angle") #### ------------------------------ TOOLS ------------------------------ #### class OBJECT_WT_carve_box(bpy.types.WorkSpaceTool, CarverToolshelf): bl_idname = "object.carve_box" bl_label = "Box Carve" bl_description = ("Boolean cut rectangular shapes into mesh objects") bl_space_type = 'VIEW_3D' bl_context_mode = 'OBJECT' bl_icon = os.path.join(os.path.join(os.path.dirname(os.path.dirname(__file__)), "icons") , "ops.object.carver_box") # bl_widget = 'VIEW3D_GGT_placement' bl_keymap = ( ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("shape", 'BOX')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("shape", 'BOX')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": [("shape", 'BOX')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": [("shape", 'BOX')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("shape", 'BOX')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": [("shape", 'BOX')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": [("shape", 'BOX')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": [("shape", 'BOX')]}), ) class MESH_WT_carve_box(OBJECT_WT_carve_box): bl_context_mode = 'EDIT_MESH' class OBJECT_WT_carve_circle(bpy.types.WorkSpaceTool, CarverToolshelf): bl_idname = "object.carve_circle" bl_label = "Circle Carve" bl_description = ("Boolean cut circlular shapes into mesh objects") bl_space_type = 'VIEW_3D' bl_context_mode = 'OBJECT' bl_icon = os.path.join(os.path.join(os.path.dirname(os.path.dirname(__file__)), "icons") , "ops.object.carver_circle") # bl_widget = 'VIEW3D_GGT_placement' bl_keymap = ( ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("shape", 'CIRCLE')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("shape", 'CIRCLE')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": [("shape", 'CIRCLE')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("shape", 'CIRCLE')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": [("shape", 'CIRCLE')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}), ) class MESH_WT_carve_circle(OBJECT_WT_carve_circle): bl_context_mode = 'EDIT_MESH' class OBJECT_WT_carve_polyline(bpy.types.WorkSpaceTool, CarverToolshelf): bl_idname = "object.carve_polyline" bl_label = "Polyline Carve" bl_description = ("Boolean cut custom polygonal shapes into mesh objects") bl_space_type = 'VIEW_3D' bl_context_mode = 'OBJECT' bl_icon = os.path.join(os.path.join(os.path.dirname(os.path.dirname(__file__)), "icons") , "ops.object.carver_polyline") # bl_widget = 'VIEW3D_GGT_placement' bl_keymap = ( ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK'}, {"properties": [("shape", 'POLYLINE')]}), ("object.carve", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True}, {"properties": [("shape", 'POLYLINE')]}), # select ("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, None), ("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("mode", 'ADD')]}), ("view3d.select_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("mode", 'SUB')]}), ) class MESH_WT_carve_polyline(OBJECT_WT_carve_polyline): bl_context_mode = 'EDIT_MESH' #### ------------------------------ OPERATORS ------------------------------ #### class OBJECT_OT_carve(bpy.types.Operator): bl_idname = "object.carve" bl_label = "Carve" bl_description = "Boolean cut square shapes into mesh objects" bl_options = {'REGISTER', 'UNDO', 'DEPENDS_ON_CURSOR'} bl_cursor_pending = 'PICK_AREA' # OPERATOR-properties shape: bpy.props.EnumProperty( name = "Shape", items = (('BOX', "Box", ""), ('CIRCLE', "Circle", ""), ('POLYLINE', "Polyline", "")), default = 'BOX', ) mode: bpy.props.EnumProperty( name = "Mode", items = (('DESTRUCTIVE', "Destructive", "Boolean cutters are immediatelly applied and removed after the cut", 'MESH_DATA', 0), ('MODIFIER', "Modifier", "Cuts are stored as boolean modifiers and cutters placed inside the collection", 'MODIFIER_DATA', 1)), default = 'DESTRUCTIVE', ) # orientation: bpy.props.EnumProperty( # name = "Orientation", # items = (('SURFACE', "Surface", "Surface normal of the mesh under the cursor"), # ('VIEW', "View", "View-aligned orientation")), # default = 'SURFACE', # ) depth: bpy.props.EnumProperty( name = "Depth", items = (('VIEW', "View", "Depth is automatically calculated from view orientation", 'VIEW_CAMERA_UNSELECTED', 0), ('CURSOR', "Cursor", "Depth is automatically set at 3D cursor location", 'PIVOT_CURSOR', 1)), default = 'VIEW', ) # SHAPE-properties aspect: bpy.props.EnumProperty( name = "Aspect", items = (('FREE', "Free", "Use an unconstrained aspect"), ('FIXED', "Fixed", "Use a fixed 1:1 aspect")), default = 'FREE', ) origin: bpy.props.EnumProperty( name = "Origin", description = "The initial position for placement", items = (('EDGE', "Edge", ""), ('CENTER', "Center", "")), default = 'EDGE', ) rotation: bpy.props.FloatProperty( name = "Rotation", subtype = "ANGLE", soft_min = -360, soft_max = 360, default = 0, ) subdivision: bpy.props.IntProperty( name = "Circle Subdivisions", description = "Number of vertices that will make up the circular shape that will be extruded into a cylinder", min = 3, soft_max = 128, default = 16, ) closed: bpy.props.BoolProperty( name = "Closed Polygon", description = "When enabled, mouse position at the moment of execution will be registered as last point of the polygon", default = True, ) # CUTTER-properties hide: bpy.props.BoolProperty( name = "Hide Cutter", description = ("Hide cutter objects in the viewport after they're created.\n" "NOTE: They are hidden in render regardless of this property"), default = True, ) parent: bpy.props.BoolProperty( name = "Parent to Canvas", description = ("Cutters will be parented to active object being cut, even if cutting multiple objects.\n" "If there is no active object in selection cutters parent might be chosen seemingly randomly"), default = True, ) auto_smooth: bpy.props.BoolProperty( name = "Shade Auto Smooth", description = ("Cutter object will be shaded smooth with sharp edges (above 30 degrees) marked as sharp\n" "NOTE: This is one time operator. 'Smooth by Angle' modifier will not be added on object"), default = True, ) sharp_angle: bpy.props.FloatProperty( name = "Angle", description = "Maximum face angle for sharp edges", subtype = "ANGLE", min = 0, max = math.pi, default = 0.523599, ) # ARRAY-properties rows: bpy.props.IntProperty( name = "Rows", description = "Number of times shape is duplicated on X axis", min = 1, soft_max = 16, default = 1, ) rows_gap: bpy.props.FloatProperty( name = "Gap between Rows", min = 0, soft_max = 250, default = 50, ) rows_direction: bpy.props.EnumProperty( name = "Direction of Rows", items = (('LEFT', "Left", ""), ('RIGHT', "Right", "")), default = 'RIGHT', ) columns: bpy.props.IntProperty( name = "Columns", description = "Number of times shape is duplicated on Y axis", min = 1, soft_max = 16, default = 1, ) columns_direction: bpy.props.EnumProperty( name = "Direction of Rows", items = (('UP', "Up", ""), ('DOWN', "Down", "")), default = 'DOWN', ) columns_gap: bpy.props.FloatProperty( name = "Gap between Columns", min = 0, soft_max = 250, default = 50, ) # BEVEL-properties use_bevel: bpy.props.BoolProperty( name = "Bevel Cutter", description = "Bevel each side edge of the cutter", default = False, ) bevel_profile: bpy.props.EnumProperty( name = "Bevel Profile", items = (('CONVEX', "Convex", "Outside bevel (rounded corners)"), ('CONCAVE', "Concave", "Inside bevel")), default = 'CONVEX', ) bevel_segments: bpy.props.IntProperty( name = "Bevel Segments", description = "Segments for curved edge", min = 2, soft_max = 32, default = 8, ) bevel_radius: bpy.props.FloatProperty( name = "Bevel Radius", description = "Amout of the bevel (in screen-space units)", min = 0.01, soft_max = 5, default = 1, ) # MODIFIER-properties solver: bpy.props.EnumProperty( name = "Solver", items = [('FAST', "Fast", ""), ('EXACT', "Exact", "")], default = 'FAST', ) pin: bpy.props.BoolProperty( name = "Pin Boolean Modifier", description = ("When enabled boolean modifier will be moved above every other modifier on the object (if there are any).\n" "Order of modifiers can drastically affect the result (especially in destructive mode)"), default = True, ) @classmethod def poll(cls, context): return context.mode in ('OBJECT', 'EDIT_MESH') and context.area.type == 'VIEW_3D' def invoke(self, context, event): self.selected_objects = context.selected_objects self.initial_selection = context.selected_objects self.mouse_path = [(event.mouse_region_x, event.mouse_region_y), (event.mouse_region_x, event.mouse_region_y)] # initialize_empty_values self.verts = [] self.cutter = None self.duplicates = [] self.view_depth = mathutils.Vector() self.cached_mouse_position = () # modifier_keys self.initial_origin = self.origin self.initial_aspect = self.aspect self.snap = False self.move = False self.rotate = False self.gap = False self.bevel = False # overlay_position self.position_x = 0 self.position_y = 0 self.initial_position = False self.center_origin = [] self.distance_from_first = 0 # Add Draw Handler self._handle = bpy.types.SpaceView3D.draw_handler_add(carver_overlay, (self, bpy.context), 'WINDOW', 'POST_PIXEL') context.window.cursor_set("MUTE") context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} def modal(self, context, event): snap_text = ", [MOUSEWHEEL]: Change Snapping Increment" if self.snap else "" if self.shape == 'POLYLINE': shape_text = "[BACKSPACE]: Remove Last Point, [ENTER]: Confirm" else: shape_text = "[SHIFT]: Aspect, [ALT]: Origin, [R]: Rotate, [ARROWS]: Array" array_text = ", [A]: Gap" if (self.rows > 1 or self.columns > 1) else "" bevel_text = ", [B]: Bevel" if self.shape == 'BOX' else "" context.workspace.status_text_set("[CTRL]: Snap Invert, [SPACEBAR]: Move, " + shape_text + bevel_text + array_text + snap_text) # find_the_limit_of_the_3d_viewport_region region_types = {'WINDOW', 'UI'} for area in context.window.screen.areas: if area.type == 'VIEW_3D': for region in area.regions: if not region_types or region.type in region_types: region.tag_redraw() # SNAP # change_the_snap_increment_value_using_the_wheel_mouse if (self.move is False) and (self.rotate is False): for i, a in enumerate(context.screen.areas): if a.type == 'VIEW_3D': space = context.screen.areas[i].spaces.active if event.type == 'WHEELUPMOUSE': space.overlay.grid_subdivisions -= 1 elif event.type == 'WHEELDOWNMOUSE': space.overlay.grid_subdivisions += 1 self.snap = context.scene.tool_settings.use_snap if event.ctrl and (self.move is False) and (self.rotate is False): self.snap = not self.snap # ASPECT if event.shift and (self.shape != 'POLYLINE'): if self.initial_aspect == 'FREE': self.aspect = 'FIXED' elif self.initial_aspect == 'FIXED': self.aspect = 'FREE' else: self.aspect = self.initial_aspect # ORIGIN if event.alt and (self.shape != 'POLYLINE'): if self.initial_origin == 'EDGE': self.origin = 'CENTER' elif self.initial_origin == 'CENTER': self.origin = 'EDGE' else: self.origin = self.initial_origin # ROTATE if event.type == 'R' and (self.shape != 'POLYLINE'): if event.value == 'PRESS': self.cached_mouse_position = (self.mouse_path[1][0], self.mouse_path[1][1]) context.window.cursor_set("NONE") self.rotate = True elif event.value == 'RELEASE': context.window.cursor_set("MUTE") context.window.cursor_warp(int(self.cached_mouse_position[0]), int(self.cached_mouse_position[1])) self.rotate = False # BEVEL if event.type == 'B' and (self.shape == 'BOX'): if event.value == 'PRESS': self.use_bevel = True self.cached_mouse_position = (self.mouse_path[1][0], self.mouse_path[1][1]) context.window.cursor_set("NONE") self.bevel = True elif event.value == 'RELEASE': context.window.cursor_set("MUTE") context.window.cursor_warp(int(self.cached_mouse_position[0]), int(self.cached_mouse_position[1])) self.bevel = False if self.bevel: if event.type == 'WHEELUPMOUSE': self.bevel_segments += 1 elif event.type == 'WHEELDOWNMOUSE': self.bevel_segments -= 1 # ARRAY if event.type == 'LEFT_ARROW' and event.value == 'PRESS': self.rows -= 1 if event.type == 'RIGHT_ARROW' and event.value == 'PRESS': self.rows += 1 if event.type == 'DOWN_ARROW' and event.value == 'PRESS': self.columns -= 1 if event.type == 'UP_ARROW' and event.value == 'PRESS': self.columns += 1 if (self.rows > 1 or self.columns > 1) and (event.type == 'A'): if event.value == 'PRESS': self.cached_mouse_position = (self.mouse_path[1][0], self.mouse_path[1][1]) context.window.cursor_set("NONE") self.gap = True elif event.value == 'RELEASE': context.window.cursor_set("MUTE") context.window.cursor_warp(self.cached_mouse_position[0], self.cached_mouse_position[1]) self.gap = False # MOVE if event.type == 'SPACE': if event.value == 'PRESS': self.move = True elif event.value == 'RELEASE': self.move = False if self.move: # initial_position_variable_before_moving_the_brush if self.initial_position is False: self.position_x = 0 self.position_y = 0 self.last_mouse_region_x = event.mouse_region_x self.last_mouse_region_y = event.mouse_region_y self.initial_position = True self.move = True # update_the_coordinates if self.initial_position and self.move is False: for i in range(0, len(self.mouse_path)): l = list(self.mouse_path[i]) l[0] += self.position_x l[1] += self.position_y self.mouse_path[i] = tuple(l) self.position_x = self.position_y = 0 self.initial_position = False # Remove Point (Polyline) if event.type == 'BACK_SPACE' and event.value == 'PRESS': if len(self.mouse_path) > 2: context.window.cursor_warp(self.mouse_path[-2][0], self.mouse_path[-2][1]) self.mouse_path = self.mouse_path[:-2] if event.type in {'MIDDLEMOUSE', 'N', 'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4', 'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9'}: return {'PASS_THROUGH'} if self.bevel == False and event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}: return {'PASS_THROUGH'} # mouse_move if event.type == 'MOUSEMOVE': if self.rotate: self.rotation = event.mouse_region_x * 0.01 elif self.move: # MOVE self.position_x += (event.mouse_region_x - self.last_mouse_region_x) self.position_y += (event.mouse_region_y - self.last_mouse_region_y) self.last_mouse_region_x = event.mouse_region_x self.last_mouse_region_y = event.mouse_region_y elif self.gap: self.rows_gap = event.mouse_region_x * 0.1 self.columns_gap = event.mouse_region_y * 0.1 elif self.bevel: self.bevel_radius = event.mouse_region_x * 0.002 else: if len(self.mouse_path) > 0: # ASPECT if self.aspect == 'FIXED': side = max(abs(event.mouse_region_x - self.mouse_path[0][0]), abs(event.mouse_region_y - self.mouse_path[0][1])) self.mouse_path[len(self.mouse_path) - 1] = \ (self.mouse_path[0][0] + (side if event.mouse_region_x >= self.mouse_path[0][0] else -side), self.mouse_path[0][1] + (side if event.mouse_region_y >= self.mouse_path[0][1] else -side)) elif self.aspect == 'FREE': self.mouse_path[len(self.mouse_path) - 1] = (event.mouse_region_x, event.mouse_region_y) # SNAP (find_the_closest_position_on_the_overlay_grid_and_snap_the_shape_to_it) if self.snap: cursor_snap(self, context, event, self.mouse_path) if self.shape == 'POLYLINE': # get_distance_from_first_point distance = math.sqrt((self.mouse_path[-1][0] - self.mouse_path[0][0]) ** 2 + (self.mouse_path[-1][1] - self.mouse_path[0][1]) ** 2) min_radius = 0 max_radius = 30 self.distance_from_first = max(max_radius - distance, min_radius) # Confirm elif (event.type == 'LEFTMOUSE' and event.value == 'RELEASE') or (event.type == 'RET' and event.value == 'PRESS'): # selection_fallback if self.shape != 'POLYLINE': if len(self.selected_objects) == 0: self.selected_objects = selection_fallback(self, context, context.view_layer.objects) for obj in self.selected_objects: obj.select_set(True) if len(self.selected_objects) == 0: self.cancel(context) return {'FINISHED'} else: empty = self.selection_fallback(context) if empty: return {'FINISHED'} else: if len(self.initial_selection) == 0: # expand_selection_fallback_on_every_polyline_click self.selected_objects = selection_fallback(self, context, context.view_layer.objects) for obj in self.selected_objects: obj.select_set(True) # Polyline if self.shape == 'POLYLINE': if not (event.type == 'RET' and event.value == 'PRESS') and (self.distance_from_first < 15): self.mouse_path.append((event.mouse_region_x, event.mouse_region_y)) if self.closed == False: # NOTE: Additional vert is needed for open loop. self.mouse_path.append((event.mouse_region_x, event.mouse_region_y)) else: # Confirm Cut (Polyline) if self.closed == False: self.verts.pop() # dont_add_current_mouse_position_as_vert if self.distance_from_first > 15: self.verts[-1] = self.verts[0] if len(self.verts) / 2 <= 1: self.report({'INFO'}, "At least two points are required to make polygonal shape") self.cancel(context) return {'FINISHED'} if self.closed and self.mouse_path[-1] == self.mouse_path[-2]: context.window.cursor_warp(event.mouse_region_x - 1, event.mouse_region_y) # NOTE: Polyline needs separate selection fallback, because it needs to calculate selection bounding box... # NOTE: after all points are already drawn, i.e. before execution. empty = self.selection_fallback(context) if empty: return {'FINISHED'} self.confirm(context) return {'FINISHED'} # Confirm Cut (Box, Circle) else: # protection_against_returning_no_rectangle_by_clicking delta_x = abs(event.mouse_region_x - self.mouse_path[0][0]) delta_y = abs(event.mouse_region_y - self.mouse_path[0][1]) min_distance = 5 if delta_x > min_distance or delta_y > min_distance: self.confirm(context) return {'FINISHED'} # Cancel elif event.type in {'RIGHTMOUSE', 'ESC'}: self.cancel(context) return {'FINISHED'} return {'RUNNING_MODAL'} def confirm(self, context): create_cutter_shape(self, context) extrude(self, self.cutter.data) set_object_origin(self.cutter) if self.auto_smooth: shade_smooth_by_angle(self.cutter, angle=math.degrees(self.sharp_angle)) self.Cut(context) self.cancel(context) def cancel(self, context): bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') context.workspace.status_text_set(None) context.window.cursor_set('DEFAULT' if context.object.mode == 'OBJECT' else 'CROSSHAIR') def selection_fallback(self, context): # filter_out_objects_not_inside_the_selection_bounding_box self.selected_objects = selection_fallback(self, context, self.selected_objects, include_cutters=True) # silently_fail_if_no_objects_inside_selection_bounding_box empty = False if len(self.selected_objects) == 0: self.cancel(context) empty = True return empty def Cut(self, context): # ensure_active_object if not context.active_object: context.view_layer.objects.active = self.selected_objects[0] # Add Modifier for obj in self.selected_objects: if self.mode == 'DESTRUCTIVE': add_boolean_modifier(self, context, obj, self.cutter, "DIFFERENCE", self.solver, apply=True, pin=self.pin, redo=False) elif self.mode == 'MODIFIER': add_boolean_modifier(self, context, obj, self.cutter, "DIFFERENCE", self.solver, pin=self.pin, redo=False) obj.booleans.canvas = True if self.mode == 'DESTRUCTIVE': # Remove Cutter delete_cutter(self.cutter) elif self.mode == 'MODIFIER': # Set Cutter Properties canvas = None if context.active_object and context.active_object in self.selected_objects: canvas = context.active_object else: canvas = self.selected_objects[0] set_cutter_properties(context, canvas, self.cutter, "Difference", parent=self.parent, hide=self.hide) #### ------------------------------ REGISTRATION ------------------------------ #### classes = [ OBJECT_OT_carve, TOPBAR_PT_carver_shape, TOPBAR_PT_carver_array, TOPBAR_PT_carver_cutter, ] main_tools = [ OBJECT_WT_carve_box, MESH_WT_carve_box, ] secondary_tools = [ OBJECT_WT_carve_circle, OBJECT_WT_carve_polyline, MESH_WT_carve_circle, MESH_WT_carve_polyline, ] def register(): for cls in classes: bpy.utils.register_class(cls) for tool in main_tools: bpy.utils.register_tool(tool, separator=False, after="builtin.primitive_cube_add", group=True) for tool in secondary_tools: bpy.utils.register_tool(tool, separator=False, after="object.carve_box", group=False) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) for tool in main_tools: bpy.utils.unregister_tool(tool) for tool in secondary_tools: bpy.utils.unregister_tool(tool)