Files
blender-portable-repo/extensions/blender_org/bool_tool/tools/carver.py
T
2026-03-17 14:30:01 -06:00

799 lines
31 KiB
Python

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)