2025-12-01

This commit is contained in:
2026-03-17 14:58:51 -06:00
parent 183e865f8b
commit 4b82b57113
6846 changed files with 954887 additions and 162606 deletions
@@ -0,0 +1,239 @@
import bpy
import math
from ...functions.mesh import (
create_cutter_shape,
extrude,
shade_smooth_by_angle,
)
from ...functions.modifier import (
add_boolean_modifier,
apply_modifiers,
)
from ...functions.object import (
set_cutter_properties,
delete_cutter,
set_object_origin,
)
from ...functions.select import (
selection_fallback,
)
#### ------------------------------ FUNCTIONS ------------------------------ ####
def custom_modifier_event(self, context, event, modifier):
"""Creates custom modifier event when key is held and hides cursor until it's released"""
if event.value == 'PRESS':
if not self.move:
self.cached_mouse_position = (self.mouse_path[1][0], self.mouse_path[1][1])
context.window.cursor_set("NONE")
setattr(self, modifier, True)
elif event.value == 'RELEASE':
if not self.move:
context.window.cursor_set("MUTE")
context.window.cursor_warp(int(self.cached_mouse_position[0]), int(self.cached_mouse_position[1]))
setattr(self, modifier, False)
#### ------------------------------ /base/ ------------------------------ ####
class CarverModifierKeys():
"""NOTE: Order of the modifier key events is important, because key value might change after function checks for it"""
"""Functions that check last are most important because they can overwrite all modifier states"""
def modifier_snap(self, context, event):
"""Modifier keys for snapping"""
self.snap = context.scene.tool_settings.use_snap
if (self.move == False) and (not hasattr(self, "rotate") or (hasattr(self, "rotate") and not self.rotate)):
# change_the_snap_increment_value_using_the_wheel_mouse
for i, area in enumerate(context.screen.areas):
if area.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
# invert_snapping
if event.ctrl:
self.snap = not self.snap
def modifier_aspect(self, context, event):
"""Modifier keys for changing aspect of the shape"""
if event.shift:
if self.initial_aspect == 'FREE':
self.aspect = 'FIXED'
elif self.initial_aspect == 'FIXED':
self.aspect = 'FREE'
else:
self.aspect = self.initial_aspect
def modifier_origin(self, context, event):
"""Modifier keys for changing the origin of the shape"""
if event.alt:
if self.initial_origin == 'EDGE':
self.origin = 'CENTER'
elif self.initial_origin == 'CENTER':
self.origin = 'EDGE'
else:
self.origin = self.initial_origin
def modifier_rotate(self, context, event):
"""Modifier keys for rotating the shape"""
if event.type == 'R':
custom_modifier_event(self, context, event, "rotate")
def modifier_bevel(self, context, event):
"""Modifier keys for beveling the shape"""
if self.shape == 'BOX':
if event.type == 'B':
custom_modifier_event(self, context, event, "bevel")
if self.bevel:
self.use_bevel = True
if event.type == 'WHEELUPMOUSE':
self.bevel_segments += 1
elif event.type == 'WHEELDOWNMOUSE':
self.bevel_segments -= 1
def modifier_array(self, context, event):
"""Modifier keys for creating the array of the shape"""
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'):
custom_modifier_event(self, context, event, "gap")
def modifier_move(self, context, event):
"""Modifier keys for moving the shape"""
if event.type == 'SPACE':
if event.value == 'PRESS':
self.move = True
elif event.value == 'RELEASE':
self.move = False
if self.move:
# reset_initial_position_before_moving_the_shape
if self.initial_position is False:
self.position_offset_x = 0
self.position_offset_y = 0
self.last_mouse_region_x = event.mouse_region_x
self.last_mouse_region_y = event.mouse_region_y
self.initial_position = True
else:
# update_the_shape_coordinates
if self.initial_position:
for i in range(0, len(self.mouse_path)):
l = list(self.mouse_path[i])
l[0] += self.position_offset_x
l[1] += self.position_offset_y
self.mouse_path[i] = tuple(l)
self.position_offset_x = self.position_offset_y = 0
self.initial_position = False
class CarverBase():
def redraw_region(self, context):
"""Redraw region to find the limits of the 3D viewport"""
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()
def validate_selection(self, context, shape='BOX'):
"""Filters out objects that are not inside the selection shape bounding box"""
"""Returns selection state (so operator can be cancelled if there are no objects inside the selection bounding box)"""
self.selected_objects = selection_fallback(self, context, self.selected_objects, shape=shape, include_cutters=True)
# silently_fail_if_no_objects_inside_selection_bounding_box
if len(self.selected_objects) == 0:
selection = False
else:
selection = True
return selection
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.mode == 'OBJECT' else 'CROSSHAIR')
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':
# Select all faces of the cutter so that newly created faces in canvas
# are also selected after applying the modifier.
for face in self.cutter.data.polygons:
face.select = True
mod = add_boolean_modifier(self, context, obj, self.cutter, "DIFFERENCE", self.solver, pin=self.pin, redo=False)
apply_modifiers(context, obj, [mod])
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)
@@ -0,0 +1,133 @@
import bpy
import math
#### ------------------------------ PROPERTIES ------------------------------ ####
class CarverOperatorProperties():
# OPERATOR-properties
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 are placed inside the collection", 'MODIFIER_DATA', 1)),
default = 'DESTRUCTIVE',
)
depth: bpy.props.EnumProperty(
name = "Depth",
items = (('VIEW', "View", "Depth is automatically calculated from view orientation", 'VIEW_CAMERA_UNSELECTED', 0),
('CURSOR', "Cursor", "Depth is derived from 3D cursors location", 'PIVOT_CURSOR', 1)),
default = 'VIEW',
)
class CarverModifierProperties():
# MODIFIER-properties
solver: bpy.props.EnumProperty(
name = "Solver",
items = [('FLOAT', "Float", ""),
('EXACT', "Exact", ""),
('MANIFOLD', "Manifold", "")],
default = 'FLOAT',
)
pin: bpy.props.BoolProperty(
name = "Pin Boolean Modifier",
description = ("Boolean modifier will be placed first in modifier stack, above other modifier (if there are any).\n"
"NOTE: Order of modifiers can drastically affect the result (especially in destructive mode)"),
default = True,
)
class CarverCutterProperties():
# CUTTER-properties
hide: bpy.props.BoolProperty(
name = "Hide Cutter",
description = ("Hide cutter objects in the viewport after they're created."),
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 specified degrees) marked as sharp\n"
"NOTE: This is a one time operator. 'Smooth by Angle' modifier will not be added on cutter"),
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,
)
class CarverArrayProperties():
# ARRAY-properties
rows: bpy.props.IntProperty(
name = "Rows",
description = "Number of times shape is duplicated horizontally",
min = 1, soft_max = 16,
default = 1,
)
rows_gap: bpy.props.FloatProperty(
name = "Gap between rows (relative unit)",
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 vertically",
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 (relative unit)",
min = 0, soft_max = 250,
default = 50,
)
class CarverBevelProperties():
# 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,
)
@@ -0,0 +1,149 @@
import bpy
from ... import __package__ as base_package
#### ------------------------------ /toolbar/ ------------------------------ ####
def carver_ui_common(context, layout, props):
"""Common tool properties for all Carver tools"""
layout.prop(props, "mode", text="")
layout.prop(props, "depth", text="")
layout.prop(props, "solver", expand=True)
# Popovers
layout.popover("TOPBAR_PT_carver_shape", text="Shape")
layout.popover("TOPBAR_PT_carver_array", text="Array")
layout.popover("TOPBAR_PT_carver_cutter", text="Cutter")
#### ------------------------------ /popovers/ ------------------------------ ####
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
layout.use_property_decorate = False
prefs = context.preferences.addons[base_package].preferences
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
# Box
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
props = tool.operator_properties("object.carve_box")
if tool.idname == "object.carve_circle":
layout.prop(props, "subdivision", text="Vertices")
layout.prop(props, "rotation")
layout.prop(props, "aspect", expand=True)
layout.prop(props, "origin", expand=True)
# bevel
if tool.idname == 'object.carve_box':
layout.separator()
layout.prop(props, "use_bevel", text="Bevel")
col = layout.column(align=True)
row = col.row(align=True)
if prefs.experimental:
row.prop(props, "bevel_profile", text="Profile", expand=True)
col.prop(props, "bevel_segments", text="Segments")
col.prop(props, "bevel_radius", text="Radius")
if props.use_bevel == False:
col.enabled = False
# Polyline
elif tool.idname == "object.carve_polyline":
props = tool.operator_properties("object.carve_polyline")
layout.prop(props, "closed")
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
layout.use_property_decorate = False
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
props = tool.operator_properties("object.carve_box")
elif tool.idname == "object.carve_polyline":
props = tool.operator_properties("object.carve_polyline")
# Rows
col = layout.column(align=True)
col.prop(props, "rows")
row = col.row(align=True)
row.prop(props, "rows_direction", text="Direction", expand=True)
col.prop(props, "rows_gap", text="Gap")
# Columns
layout.separator()
col = layout.column(align=True)
col.prop(props, "columns")
row = col.row(align=True)
row.prop(props, "columns_direction", text="Direction", expand=True)
col.prop(props, "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
layout.use_property_decorate = False
tool = context.workspace.tools.from_space_view3d_mode('OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH')
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
props = tool.operator_properties("object.carve_box")
elif tool.idname == "object.carve_polyline":
props = tool.operator_properties("object.carve_polyline")
# modifier_&_cutter
col = layout.column()
col.prop(props, "pin", text="Pin Modifier")
if props.mode == 'MODIFIER':
col.prop(props, "parent")
col.prop(props, "hide")
# auto_smooth
layout.separator()
col = layout.column(align=True)
col.prop(props, "auto_smooth", text="Auto Smooth")
col.prop(props, "sharp_angle")
#### ------------------------------ REGISTRATION ------------------------------ ####
classes = [
TOPBAR_PT_carver_shape,
TOPBAR_PT_carver_array,
TOPBAR_PT_carver_cutter,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)