2025-12-01
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user