2026-01-01

This commit is contained in:
2026-03-17 15:16:34 -06:00
parent ec4cf523fb
commit b80274187b
263 changed files with 95164 additions and 3848 deletions
@@ -20,9 +20,10 @@ else:
#### ------------------------------ REGISTRATION ------------------------------ ####
"""NOTE: Order of modules is important because of dependancies. Don't change without a reason."""
modules = [
carver_circle,
carver_box,
# carver_circle,
carver_polyline,
ui,
]
@@ -1,31 +1,26 @@
import bpy
import mathutils
import os
from mathutils import Vector
from .. import __file__ as base_file
from .common.base import (
CarverModifierKeys,
CarverBase,
)
from .common.properties import (
CarverOperatorProperties,
CarverModifierProperties,
CarverCutterProperties,
CarverArrayProperties,
CarverBevelProperties,
CarverPropsArray,
CarverPropsBevel,
)
from .common.types import (
Selection,
Mouse,
Workplane,
Cutter,
Effects,
)
from .common.ui import (
carver_ui_common,
)
from ..functions.draw import (
carver_shape_box,
)
from ..functions.select import (
cursor_snap,
selection_fallback,
)
description = "Cut primitive shapes into mesh objects by box drawing"
@@ -39,16 +34,16 @@ class OBJECT_WT_carve_box(bpy.types.WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'OBJECT'
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "ops.object.carver_box")
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_box")
bl_keymap = (
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("shape", 'BOX')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("shape", 'BOX')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": [("shape", 'BOX')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("shape", 'BOX')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": [("shape", 'BOX')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": [("shape", 'BOX')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": None}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": None}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": None}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": None}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": None}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": None}),
)
def draw_settings(context, layout, tool):
@@ -63,26 +58,21 @@ class MESH_WT_carve_box(OBJECT_WT_carve_box):
#### ------------------------------ OPERATORS ------------------------------ ####
class OBJECT_OT_carve_box(CarverBase, CarverModifierKeys, bpy.types.Operator,
CarverOperatorProperties, CarverModifierProperties, CarverCutterProperties,
CarverArrayProperties, CarverBevelProperties):
class OBJECT_OT_carve_box(CarverBase,
CarverPropsArray,
CarverPropsBevel):
bl_idname = "object.carve_box"
bl_label = "Box Carve"
bl_description = description
bl_options = {'REGISTER', 'UNDO', 'DEPENDS_ON_CURSOR'}
bl_cursor_pending = 'PICK_AREA'
shape: bpy.props.EnumProperty(
name = "Shape",
items = (('BOX', "Box", ""),
('CIRCLE', "Circle", ""),
('POLYLINE', "Polyline", "")),
default = 'BOX',
)
# SHAPE-properties
shape = 'BOX'
aspect: bpy.props.EnumProperty(
name = "Aspect",
description = "The initial aspect",
items = (('FREE', "Free", "Use an unconstrained aspect"),
('FIXED', "Fixed", "Use a fixed 1:1 aspect")),
default = 'FREE',
@@ -100,12 +90,6 @@ class OBJECT_OT_carve_box(CarverBase, CarverModifierKeys, bpy.types.Operator,
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,
)
@classmethod
@@ -114,36 +98,31 @@ class OBJECT_OT_carve_box(CarverBase, CarverModifierKeys, bpy.types.Operator,
def invoke(self, context, event):
self.selected_objects = context.selected_objects
self.mouse_path = [(event.mouse_region_x, event.mouse_region_y),
(event.mouse_region_x, event.mouse_region_y)]
# Validate Selection
self.objects = Selection(*self.validate_selection(context))
# initialize_empty_values
self.verts = []
self.duplicates = []
self.cutter = None
self.view_depth = mathutils.Vector()
self.cached_mouse_position = () # needed_for_custom_modifier_keys
if len(self.objects.selected) == 0:
self.report({'WARNING'}, "Select mesh objects that should be carved")
bpy.ops.view3d.select_box('INVOKE_DEFAULT')
return {'CANCELLED'}
# Initialize Core Components
self.mouse = Mouse().from_event(event)
self.workplane = Workplane(*self.calculate_workplane(context))
self.cutter = Cutter(*self.create_cutter(context))
self.effects = Effects().from_invoke(self, context)
# cached_variables
"""Important for storing context as it was when operator was invoked (untouched by the modal)"""
self.initial_origin = self.origin
self.initial_aspect = self.aspect
# modifier_keys
self.snap = False
self.move = False
self.rotate = False
self.gap = False
self.bevel = False
# overlay_position (needed_for_moving_the_shape)
self.position_offset_x = 0
self.position_offset_y = 0
self.initial_position = False
"""Important for storing context as it was when operator was invoked (untouched by the modal)."""
self.phase = "DRAW"
self.initial_origin = self.origin # Initial shape origin.
self.initial_aspect = self.aspect # Initial shape aspect.
self._stored_phase = "DRAW"
# Add Draw Handler
self._handle = bpy.types.SpaceView3D.draw_handler_add(carver_shape_box, (self, context, self.shape), 'WINDOW', 'POST_PIXEL')
self._handler = bpy.types.SpaceView3D.draw_handler_add(self.draw_shaders,
(context,),
'WINDOW', 'POST_VIEW')
context.window.cursor_set("MUTE")
context.window_manager.modal_handler_add(self)
@@ -152,110 +131,177 @@ class OBJECT_OT_carve_box(CarverBase, CarverModifierKeys, bpy.types.Operator,
def modal(self, context, event):
# Status Bar Text
snap_text = ", [MOUSEWHEEL]: Change Snapping Increment" if self.snap 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)
self.status(context)
# find_the_limit_of_the_3d_viewport_region
self.redraw_region(context)
# Modifier Keys
self.modifier_snap(context, event)
self.modifier_aspect(context, event)
self.modifier_origin(context, event)
self.modifier_rotate(context, event)
self.modifier_bevel(context, event)
self.modifier_array(context, event)
self.modifier_move(context, event)
self.event_aspect(context, event)
self.event_origin(context, event)
self.event_rotate(context, event)
self.event_bevel(context, event)
self.event_array(context, event)
self.event_flip(context, event)
self.event_move(context, event)
if event.type in {'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9',
'MIDDLEMOUSE', 'N'}:
return {'PASS_THROUGH'}
if self.bevel == False and event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
if event.type in {'MIDDLEMOUSE'}:
return {'PASS_THROUGH'}
if event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
if self.phase != "BEVEL":
return {'PASS_THROUGH'}
# Mouse Move
if event.type == 'MOUSEMOVE':
# move
if self.move:
self.position_offset_x += (event.mouse_region_x - self.last_mouse_region_x)
self.position_offset_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
self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y))
# rotate
elif self.rotate:
self.rotation = event.mouse_region_x * 0.01
# Draw
if self.phase == "DRAW":
self.update_cutter_shape(context)
# array
elif self.gap:
self.rows_gap = event.mouse_region_x * 0.1
self.columns_gap = event.mouse_region_y * 0.1
# bevel
elif self.bevel:
self.bevel_radius = event.mouse_region_x * 0.002
# Draw Shape
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)
# Extrude
elif self.phase == "EXTRUDE":
self.set_extrusion_depth(context)
# Confirm
elif (event.type == 'LEFTMOUSE' and event.value == 'RELEASE') or (event.type == 'RET' and event.value == 'PRESS'):
# selection_fallback
if len(self.selected_objects) == 0:
self.selected_objects = selection_fallback(self, context, context.view_layer.objects, shape='BOX')
for obj in self.selected_objects:
obj.select_set(True)
elif event.type == 'LEFTMOUSE':
# Confirm Shape
if self.phase == "DRAW" and event.value == 'RELEASE':
"""
Protection against creating a very small rectangle (or even with 0 dimensions)
by clicking and releasing very quickly, in a very small distance.
"""
delta_x = abs(event.mouse_region_x - self.mouse.initial[0])
delta_y = abs(event.mouse_region_y - self.mouse.initial[1])
min_distance = 5
if len(self.selected_objects) == 0:
self.cancel(context)
return {'FINISHED'}
else:
selection = self.validate_selection(context, shape='BOX')
if not selection:
self.cancel(context)
if delta_x < min_distance or delta_y < min_distance:
self.finalize(context, clean_up=True, abort=True)
return {'FINISHED'}
# 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
self.extrude_cutter(context)
self.Cut(context)
if delta_x > min_distance or delta_y > min_distance:
# Not setting depth manually, performing a cut here.
if self.depth != 'MANUAL':
self.confirm(context)
return {'FINISHED'}
else:
return {'RUNNING_MODAL'}
# Confirm Depth
if self.phase == "EXTRUDE" and event.value == 'PRESS':
self.confirm(context)
return {'FINISHED'}
# Cancel
elif event.type in {'RIGHTMOUSE', 'ESC'}:
self.cancel(context)
self.finalize(context, clean_up=True, abort=True)
return {'FINISHED'}
return {'RUNNING_MODAL'}
def status(cls, context):
"""Set the status bar text to modal modifier keys."""
# Draw
def modal_keys_draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text="", icon='MOUSE_MOVE')
row.label(text="Draw")
row.label(text="", icon='MOUSE_LMB')
row.label(text="Confirm")
row.label(text="", icon='MOUSE_MMB')
row.label(text="Rotate View")
row.label(text="", icon='MOUSE_RMB')
row.label(text="Cancel")
row.label(text="", icon='EVENT_SPACEKEY')
row.label(text=" Move")
row.label(text="", icon='EVENT_R')
row.label(text="Rotate")
row.label(text="", icon='KEY_SHIFT')
row.label(text="Aspect")
row.label(text="", icon='EVENT_ALT')
row.label(text=" Origin")
row.label(text="", icon='EVENT_LEFT_ARROW')
row.label(text="", icon='EVENT_DOWN_ARROW')
row.label(text="", icon='EVENT_RIGHT_ARROW')
row.label(text="", icon='EVENT_UP_ARROW')
row.label(text="Array")
row.label(text="", icon='EVENT_B')
row.label(text="Bevel")
# Restore rest of the status bar.
layout.separator_spacer()
layout.template_reports_banner()
layout.separator_spacer()
layout.template_running_jobs()
layout.separator_spacer()
row = layout.row()
row.alignment = "RIGHT"
text = context.screen.statusbar_info()
row.label(text=text + " ")
# Extrude
def modal_keys_extrude(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text="", icon='MOUSE_MOVE')
row.label(text="Set Depth")
row.label(text="", icon='MOUSE_LMB')
row.label(text="Confirm")
row.label(text="", icon='MOUSE_MMB')
row.label(text="Rotate View")
row.label(text="", icon='MOUSE_RMB')
row.label(text="Cancel")
row.label(text="", icon='EVENT_SPACEKEY')
row.label(text=" Move")
row.label(text="", icon='EVENT_R')
row.label(text="Rotate")
row.label(text="", icon='EVENT_F')
row.label(text="Flip Direction")
row.label(text="", icon='EVENT_LEFT_ARROW')
row.label(text="", icon='EVENT_DOWN_ARROW')
row.label(text="", icon='EVENT_RIGHT_ARROW')
row.label(text="", icon='EVENT_UP_ARROW')
row.label(text="Array")
row.label(text="", icon='EVENT_B')
row.label(text="Bevel")
# Restore rest of the status bar.
layout.separator_spacer()
layout.template_reports_banner()
layout.separator_spacer()
layout.template_running_jobs()
layout.separator_spacer()
row = layout.row()
row.alignment = "RIGHT"
text = context.screen.statusbar_info()
row.label(text=text + " ")
# Missing keys:
# Wheelup and Wheeldown to control bevel segments when B is pressed.
# A to adjust array gap when array effect is used.
if cls.phase == 'DRAW':
context.workspace.status_text_set(modal_keys_draw)
elif cls.phase == 'EXTRUDE':
context.workspace.status_text_set(modal_keys_extrude)
#### ------------------------------ REGISTRATION ------------------------------ ####
@@ -5,6 +5,7 @@ from .. import __file__ as base_file
from .common.ui import (
carver_ui_common,
)
from .carver_box import OBJECT_OT_carve_box
description = "Cut primitive shapes into mesh objects with brush"
@@ -19,21 +20,70 @@ class OBJECT_WT_carve_circle(bpy.types.WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'OBJECT'
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "ops.object.carver_circle")
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_circle")
bl_keymap = (
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": [("shape", 'CIRCLE')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": [("shape", 'CIRCLE')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": [("shape", 'CIRCLE')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": [("shape", 'CIRCLE')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": [("shape", 'CIRCLE')]}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "alt": True}, {"properties": None}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True, "alt": True}, {"properties": None}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True}, {"properties": None}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True}, {"properties": None}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "alt": True}, {"properties": None}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "ctrl": True, "shift": True, "alt": True}, {"properties": None}),
)
def draw_settings(context, layout, tool):
props = tool.operator_properties("object.carve_box")
props = tool.operator_properties("object.carve_circle")
carver_ui_common(context, layout, props)
class MESH_WT_carve_circle(OBJECT_WT_carve_circle):
bl_context_mode = 'EDIT_MESH'
#### ------------------------------ OPERATORS ------------------------------ ####
class OBJECT_OT_carve_circle(OBJECT_OT_carve_box):
bl_idname = "object.carve_circle"
bl_label = "Box Carve"
bl_description = description
# SHAPE-properties
shape = 'CIRCLE'
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,
)
aspect: bpy.props.EnumProperty(
name = "Aspect",
description = "The initial aspect",
items = (('FREE', "Free", "Use an unconstrained aspect"),
('FIXED', "Fixed", "Use a fixed 1:1 aspect")),
default = 'FIXED',
)
origin: bpy.props.EnumProperty(
name = "Origin",
description = "The initial position for placement",
items = (('EDGE', "Edge", ""),
('CENTER', "Center", "")),
default = 'CENTER',
)
#### ------------------------------ REGISTRATION ------------------------------ ####
classes = [
OBJECT_OT_carve_circle,
]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
@@ -1,31 +1,27 @@
import bpy
import mathutils
import math
import os
from mathutils import Vector
from bpy_extras import view3d_utils
from .. import __file__ as base_file
from .common.base import (
CarverModifierKeys,
CarverBase,
)
from .common.properties import (
CarverOperatorProperties,
CarverModifierProperties,
CarverCutterProperties,
CarverArrayProperties,
CarverPropsArray,
)
from .common.types import (
Selection,
Mouse,
Workplane,
Cutter,
Effects,
)
from .common.ui import (
carver_ui_common,
)
from ..functions.draw import (
carver_shape_polyline,
)
from ..functions.select import (
cursor_snap,
selection_fallback,
)
description = "Cut custom polygonal shapes into mesh objects"
@@ -39,7 +35,7 @@ class OBJECT_WT_carve_polyline(bpy.types.WorkSpaceTool):
bl_space_type = 'VIEW_3D'
bl_context_mode = 'OBJECT'
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "ops.object.carver_polyline")
bl_icon = os.path.join(os.path.dirname(base_file), "icons", "tool_icons", "ops.object.carver_polyline")
bl_keymap = (
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK'}, None),
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True}, None),
@@ -61,8 +57,8 @@ class MESH_WT_carve_polyline(OBJECT_WT_carve_polyline):
#### ------------------------------ OPERATORS ------------------------------ ####
class OBJECT_OT_carve_polyline(CarverBase, CarverModifierKeys, bpy.types.Operator,
CarverOperatorProperties, CarverModifierProperties, CarverCutterProperties, CarverArrayProperties):
class OBJECT_OT_carve_polyline(CarverBase,
CarverPropsArray):
bl_idname = "object.carve_polyline"
bl_label = "Polyline Carve"
bl_description = description
@@ -70,11 +66,11 @@ class OBJECT_OT_carve_polyline(CarverBase, CarverModifierKeys, bpy.types.Operato
bl_cursor_pending = 'PICK_AREA'
# SHAPE-properties
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,
)
shape = 'POLYLINE'
origin = None
aspect = None
rotation = 0
@classmethod
def poll(cls, context):
@@ -82,34 +78,29 @@ class OBJECT_OT_carve_polyline(CarverBase, CarverModifierKeys, bpy.types.Operato
def invoke(self, context, event):
self.selected_objects = context.selected_objects
self.mouse_path = [(event.mouse_region_x, event.mouse_region_y),
(event.mouse_region_x, event.mouse_region_y)]
# Validate Selection
self.objects = Selection(*self.validate_selection(context))
# initialize_empty_values
self.verts = []
self.duplicates = []
self.cutter = None
self.view_depth = mathutils.Vector()
self.cached_mouse_position = () # needed_for_custom_modifier_keys
self.distance_from_first = 0
if len(self.objects.selected) == 0:
bpy.ops.view3d.select('INVOKE_DEFAULT')
return {'CANCELLED'}
# cached_variables
"""Important for storing context as it was when operator was invoked (untouched by the modal)"""
self.initial_selection = context.selected_objects
# Initialize Core Components
self.mouse = Mouse().from_event(event)
self.workplane = Workplane(*self.calculate_workplane(context))
self.cutter = Cutter(*self.create_cutter(context))
self.effects = Effects().from_invoke(self, context)
# modifier_keys
self.snap = False
self.move = False
self.gap = False
# overlay_position (needed_for_moving_the_shape)
self.position_offset_x = 0
self.position_offset_y = 0
self.initial_position = False
# cached_variables
"""Important for storing context as it was when operator was invoked (untouched by the modal)."""
self.phase = "DRAW"
self._distance_from_first = 0
self._stored_phase = "DRAW"
# Add Draw Handler
self._handle = bpy.types.SpaceView3D.draw_handler_add(carver_shape_polyline, (self, context), 'WINDOW', 'POST_PIXEL')
self._handler = bpy.types.SpaceView3D.draw_handler_add(self.draw_shaders,
(context,),
'WINDOW', 'POST_VIEW')
context.window.cursor_set("MUTE")
context.window_manager.modal_handler_add(self)
@@ -117,114 +108,284 @@ class OBJECT_OT_carve_polyline(CarverBase, CarverModifierKeys, bpy.types.Operato
def modal(self, context, event):
# Tool Settings Text
snap_text = ", [MOUSEWHEEL]: Change Snapping Increment" if self.snap else ""
shape_text = "[BACKSPACE]: Remove Last Point, [ENTER]: Confirm"
array_text = ", [A]: Gap" if (self.rows > 1 or self.columns > 1) else ""
context.workspace.status_text_set("[CTRL]: Snap Invert, [SPACEBAR]: Move, " + shape_text + array_text + snap_text)
# Status Bar Text
self.status(context)
# find_the_limit_of_the_3d_viewport_region
self.redraw_region(context)
# Modifier Keys
self.modifier_snap(context, event)
self.modifier_array(context, event)
self.modifier_move(context, event)
self.event_array(context, event)
self.event_move(context, event)
if event.type in {'NUMPAD_1', 'NUMPAD_2', 'NUMPAD_3', 'NUMPAD_4',
'NUMPAD_5', 'NUMPAD_6', 'NUMPAD_7', 'NUMPAD_8', 'NUMPAD_9',
'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE', 'N'}:
if event.type in {'MIDDLEMOUSE'}:
return {'PASS_THROUGH'}
if event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'}:
if self.phase != "BEVEL":
return {'PASS_THROUGH'}
# Mouse Move
if event.type == 'MOUSEMOVE':
# move
if self.move:
self.position_offset_x += (event.mouse_region_x - self.last_mouse_region_x)
self.position_offset_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
self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y))
# array
elif self.gap:
self.rows_gap = event.mouse_region_x * 0.1
self.columns_gap = event.mouse_region_y * 0.1
# Draw
if self.phase == "DRAW":
# Calculate the distance from the initial mouse position.
if self.mouse.current_3d:
first_vert_world = self.cutter.obj.matrix_world @ self.cutter.verts[0].co
first_vert_screen = view3d_utils.location_3d_to_region_2d(context.region,
context.region_data,
first_vert_world)
distance_screen = (Vector(self.mouse.current) - first_vert_screen).length
self._distance_from_first = max(100 - distance_screen, 0)
# Draw Shape
else:
if len(self.mouse_path) > 0:
self.mouse_path[len(self.mouse_path) - 1] = (event.mouse_region_x, event.mouse_region_y)
self.update_cutter_shape(context)
# 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)
# 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)
# Extrude
elif self.phase == "EXTRUDE":
self.set_extrusion_depth(context)
# Add Points & Confirm
elif (event.type == 'LEFTMOUSE' and event.value == 'RELEASE') or (event.type == 'RET' and event.value == 'PRESS'):
# selection_fallback (expand_selection_on_every_polyline_click)
if len(self.initial_selection) == 0:
self.selected_objects = selection_fallback(self, context, context.view_layer.objects, shape='POLYLINE')
for obj in self.selected_objects:
obj.select_set(True)
elif event.type == 'LEFTMOUSE' and event.value == 'PRESS':
if self.phase == "DRAW":
# Confirm Shape (if clicked on the first vert)
if self._distance_from_first > 75:
verts = self.cutter.verts
if len(verts) > 3:
self._remove_polyline_point(context, jump_mouse=False)
self.extrude_cutter(context)
self.Cut(context)
# Not setting depth manually, performing a cut here.
if self.depth != 'MANUAL':
self.confirm(context)
return {'FINISHED'}
else:
return {'RUNNING_MODAL'}
# add_new_points
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))
# confirm_cut
else:
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)
selection = self.validate_selection(context, shape='POLYLINE')
if not selection:
self.cancel(context)
return {'FINISHED'}
# Add Point
else:
self._insert_polyline_point()
# Confirm Depth
if self.phase == "EXTRUDE":
self.confirm(context)
return {'FINISHED'}
# Confirm
elif event.type == 'RET':
verts = self.cutter.verts
if len(verts) > 2:
# Confirm Shape
if self.phase == "DRAW" and event.value == 'RELEASE':
self.extrude_cutter(context)
self.Cut(context)
# Not setting depth manually, performing a cut here.
if self.depth != 'MANUAL':
self.confirm(context)
return {'FINISHED'}
else:
return {'RUNNING_MODAL'}
# Confirm Depth
if self.phase == "EXTRUDE" and event.value == 'PRESS':
self.confirm(context)
return {'FINISHED'}
else:
self.report({'WARNING'}, "At least three points are required to make a polygonal shape")
# Remove Last Point
if event.type == 'BACK_SPACE' and event.value == 'PRESS':
if len(self.mouse_path) > 2:
context.window.cursor_warp(int(self.mouse_path[-2][0]), int(self.mouse_path[-2][1]))
self.mouse_path = self.mouse_path[:-1]
self._remove_polyline_point(context)
# Cancel
elif event.type in {'RIGHTMOUSE', 'ESC'}:
self.cancel(context)
self.finalize(context, clean_up=True, abort=True)
return {'FINISHED'}
return {'RUNNING_MODAL'}
def status(cls, context):
"""Set the status bar text to modal modifier keys."""
# Draw
def modal_keys_draw(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text="", icon='MOUSE_LMB')
row.label(text="Insert Point")
row.label(text="", icon='MOUSE_MMB')
row.label(text="Rotate View")
row.label(text="", icon='MOUSE_RMB')
row.label(text="Cancel")
row.label(text="", icon='KEY_RETURN')
row.label(text="Confirm")
row.label(text="", icon='EVENT_SPACEKEY')
row.label(text=" Move")
row.label(text="", icon='EVENT_BACKSPACE')
row.label(text=" Remove Last Point")
row.label(text="", icon='EVENT_LEFT_ARROW')
row.label(text="", icon='EVENT_DOWN_ARROW')
row.label(text="", icon='EVENT_RIGHT_ARROW')
row.label(text="", icon='EVENT_UP_ARROW')
row.label(text="Array")
# Restore rest of the status bar.
layout.separator_spacer()
layout.template_reports_banner()
layout.separator_spacer()
layout.template_running_jobs()
layout.separator_spacer()
row = layout.row()
row.alignment = "RIGHT"
text = context.screen.statusbar_info()
row.label(text=text + " ")
# Extrude
def modal_keys_extrude(self, context):
layout = self.layout
row = layout.row(align=True)
row.label(text="", icon='MOUSE_MOVE')
row.label(text="Set Depth")
row.label(text="", icon='MOUSE_LMB')
row.label(text="", icon='KEY_RETURN')
row.label(text="Confirm")
row.label(text="", icon='MOUSE_MMB')
row.label(text="Rotate View")
row.label(text="", icon='MOUSE_RMB')
row.label(text="Cancel")
row.label(text="", icon='EVENT_SPACEKEY')
row.label(text=" Move")
row.label(text="", icon='EVENT_R')
row.label(text="Rotate")
row.label(text="", icon='EVENT_F')
row.label(text="Flip Direction")
row.label(text="", icon='EVENT_LEFT_ARROW')
row.label(text="", icon='EVENT_DOWN_ARROW')
row.label(text="", icon='EVENT_RIGHT_ARROW')
row.label(text="", icon='EVENT_UP_ARROW')
row.label(text="Array")
# Restore rest of the status bar.
layout.separator_spacer()
layout.template_reports_banner()
layout.separator_spacer()
layout.template_running_jobs()
layout.separator_spacer()
row = layout.row()
row.alignment = "RIGHT"
text = context.screen.statusbar_info()
row.label(text=text + " ")
# Missing keys:
# A to adjust array gap when array effect is used.
if cls.phase == 'DRAW':
context.workspace.status_text_set(modal_keys_draw)
elif cls.phase == 'EXTRUDE':
context.workspace.status_text_set(modal_keys_extrude)
# Polyline-specific features.
def _insert_polyline_point(self):
"""Inserts a new vertex in the cutter geometry and connects it to the previous last one."""
bm = self.cutter.bm
verts = self.cutter.verts
x, y = self.mouse.current_3d.x, self.mouse.current_3d.y
# Lock the position of the last vert to cursor position at the moment of press.
last_vert = verts[-1]
last_vert.co = Vector((x, y, 0))
# Find and remove edge between last vert and the first vert.
if verts.index(last_vert) != 1:
first_vert = verts[0]
edge_to_remove = None
for edge in last_vert.link_edges:
if first_vert in edge.verts:
edge_to_remove = edge
break
if edge_to_remove:
self.cutter.bm.edges.remove(edge_to_remove)
# Insert new point in bmesh and connect to last one.
new_vert = bm.verts.new(Vector((x, y, 0)))
bm.edges.new([last_vert, new_vert])
verts.append(new_vert)
# Create a new face.
if len(verts) >= 3:
face = self.cutter.bm.faces.new(verts)
self.cutter.faces = [face]
# Update bmesh.
bm.to_mesh(self.cutter.mesh)
def _remove_polyline_point(self, context, jump_mouse=True):
"""Removes the last vertex in cutter geometry and moves cursor to the one before that."""
if self.phase != "DRAW":
return
obj = self.cutter.obj
bm = self.cutter.bm
verts = self.cutter.verts
faces = self.cutter.faces
if len(verts) <= 2:
return
# Remove last vertex.
last_vert = verts[-1]
bm.verts.remove(last_vert)
verts.pop()
# Reconstruct the face.
face = faces[0]
if face is not None:
if len(verts) >= 3:
new_face = bm.faces.new(verts)
faces[0] = new_face
else:
faces[0] = None
# Create an edge between new last vertex and the first vertex.
new_last = verts[-1]
first_vert = verts[0]
edge_exists = any(first_vert in edge.verts for edge in new_last.link_edges)
if not edge_exists:
bm.edges.new([new_last, first_vert])
# Update bmesh.
bm.to_mesh(self.cutter.mesh)
# Jump mouse to the new last vert.
if jump_mouse:
vert_world = obj.matrix_world @ new_last.co
screen_pos = view3d_utils.location_3d_to_region_2d(context.region,
context.region_data,
vert_world)
if screen_pos:
context.window.cursor_warp(int(screen_pos.x), int(screen_pos.y))
#### ------------------------------ REGISTRATION ------------------------------ ####
File diff suppressed because it is too large Load Diff
@@ -2,25 +2,82 @@ import bpy
import math
# Import Custom Icons
from ... import icons
svg_icons = icons.svg_icons["main"]
icon_measure = svg_icons["MEASURE"].icon_id
icon_cpu = svg_icons["CPU"].icon_id
#### ------------------------------ PROPERTIES ------------------------------ ####
class CarverOperatorProperties():
class CarverPropsOperator():
# 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',
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 = 'MODIFIER',
)
alignment: bpy.props.EnumProperty(
name = "Alignment",
items = (('SURFACE', "Surface", "Align cutters to the surface normal of the mesh under the mouse", 'SNAP_NORMAL', 0),
('VIEW', "View", "Align cutters to the current view", 'VIEW_CAMERA_UNSELECTED', 1),
('CURSOR', "3D Cursor", "Align cutters to the 3D cursor orientation", 'ORIENTATION_CURSOR', 2),
('GRID', "Grid", "Align cutters to the world grid", 'GRID', 3)),
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 derived from 3D cursors location", 'PIVOT_CURSOR', 1)),
default = 'VIEW',
items = (('MANUAL', "Manual", "Depth can be manually set after creating a cutter shape", icon_measure, 0),
('AUTO', "Auto", "Depth is set automatically to cover selected objects entirely", icon_cpu, 1),
('CURSOR', "3D Cursor", "Depth is set to 3D cursors location", 'PIVOT_CURSOR', 2)),
default = 'MANUAL',
)
class CarverModifierProperties():
class CarverPropsShape():
# SHAPE-properties
orientation: bpy.props.EnumProperty(
name = "Orientation",
description = "Orientation method for the shape placement",
items = (('FACE', "Face Normal", "Orient the shape along the normal of the face"),
('CLOSEST_EDGE', "Closest Edge", "Orient the shape along the closest edge of the face"),
('LONGEST_EDGE', "Longest Edge", "Orient the shape along the longest edge of the face")),
default = 'CLOSEST_EDGE',
)
offset: bpy.props.FloatProperty(
name = "Offset from Surface",
description = ("Distance between the shape and the surface of the mesh.\n"
"Offset is important for avoiding Z-fighting issues and solver failures"),
min = 0.0, soft_max = 0.1,
default = 0.01,
)
align_to_all: bpy.props.BoolProperty(
name = "Align to Anything",
description = "Use all visible objects for surface alignment, not just selected objects",
default = True,
)
alignment_axis: bpy.props.EnumProperty(
name = "Alignment Axis",
description = "Which axis of the world grid or 3D cursor should be used for workplane alignment",
items = (('X', "X", ""),
('Y', "Y", ""),
('Z', "Z", "")),
default = 'Z',
)
flip_direction: bpy.props.BoolProperty(
name = "Flip Direction",
description = "Change which way the geometry is extruded",
options = {'SKIP_SAVE', 'HIDDEN', 'SKIP_PRESET', },
default = False,
)
class CarverPropsModifier():
# MODIFIER-properties
solver: bpy.props.EnumProperty(
name = "Solver",
@@ -37,7 +94,7 @@ class CarverModifierProperties():
)
class CarverCutterProperties():
class CarverPropsCutter():
# CUTTER-properties
hide: bpy.props.BoolProperty(
name = "Hide Cutter",
@@ -50,6 +107,21 @@ class CarverCutterProperties():
"If there is no active object in selection cutters parent might be chosen seemingly randomly"),
default = True,
)
display: bpy.props.EnumProperty(
name = "Cutter Display",
items = (('WIRE', "Wire", "Display the cutter object as a wireframe"),
('BOUNDS', "Bounds", "Display only the bounds of the cutter object")),
default = 'BOUNDS'
)
cutter_origin: bpy.props.EnumProperty(
name = "Cutter Origin Point",
items = (('CENTER_OBJ', "Bounding Box", "Put the object origin at the center of the cutters bounding box"),
('CENTER_MESH', "Geometry", "Put the object origin at the center of the cutters geometry (not including effects)"),
('FACE_CENTER', "First Face", "Put the object origin at the center of cutters first face (i.e. shape)"),
('MOUSE_INITIAL', "Mouse Click", "Put the object origin at the point where mouse was first clicked"),
('CANVAS', "Same as Canvas", "Put the object origin of the cutter to the origin point of the cutter")),
default = 'CENTER_MESH',
)
auto_smooth: bpy.props.BoolProperty(
name = "Shade Auto Smooth",
@@ -66,7 +138,7 @@ class CarverCutterProperties():
)
class CarverArrayProperties():
class CarverPropsArray():
# ARRAY-properties
rows: bpy.props.IntProperty(
name = "Rows",
@@ -74,60 +146,41 @@ class CarverArrayProperties():
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,
gap: bpy.props.FloatProperty(
name = "Gap",
description = "Spacing between duplicates, both in rows and columns (relative unit)",
min = 1, soft_max = 10,
default = 1.1,
)
class CarverBevelProperties():
class CarverPropsBevel():
# 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,
min = 1, 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,
bevel_width: bpy.props.FloatProperty(
name = "Bevel Width",
min = 0, soft_max = 5,
default = 0.1,
)
bevel_profile: bpy.props.FloatProperty(
name = "Bevel Profile",
description = "The bevel profile shape (0.5 = round)",
min = 0, max = 1,
default = 0.5,
)
@@ -0,0 +1,292 @@
import bpy
import math
import os
from mathutils import Vector, Matrix
from ...functions.mesh import (
ensure_attribute,
shade_smooth_by_angle,
)
from ...functions.modifier import (
add_modifier_asset,
)
#### ------------------------------ CLASSES ------------------------------ ####
class Selection:
"""Storage of viable selected and active object(s) throughout the modal."""
def __init__(self, selected, active):
self.selected: list = selected
self.active = active
self.modifiers = {}
class Mouse:
"""
Mouse positions throughout different phases of the modal operator.
Each class variable is a 2D vector in screen space (x, y).
"""
def __init__(self):
self.initial = Vector()
self.current = Vector()
self.extrude = Vector()
self.cached = Vector() # Used for custom modifier keys.
self.current_3d = Vector()
self.cached_3d = Vector()
@classmethod
def from_event(self, event):
self.initial = Vector((event.mouse_region_x, event.mouse_region_y))
self.current = Vector((event.mouse_region_x, event.mouse_region_y))
self.current_3d = None
return self
class Workplane:
"""Local 3D coordinate system used as the drawing plane for creating shapes."""
def __init__(self, matrix, location, normal):
self.matrix: Matrix = matrix # full 4x4 transform matrix.
self.location: Vector = location # origin point of the plane in world space.
self.normal: Vector = normal # perpendicular direction of the plane.
class Cutter:
"""Object created for cutting, as well as it's `bmesh`, and other properties."""
def __init__(self, obj, mesh, bm, faces, verts):
self.obj = obj
self.mesh = mesh
self.bm = bm
self.faces: list = faces
self.verts: list = verts
self.center = Vector() # Center of the geometry.
# Effects
class Effects:
def __init__(self):
self.array = None
self.bevel = None
self.smooth = None
self.weld = None
def from_invoke(self, cls, context):
"""Add modifiers to the cutter object during invoke, if they're enabled on tool level."""
# Smooth by Angle
if cls.auto_smooth:
self.add_auto_smooth_modifier(cls, context)
# Array
if cls.rows > 1 or cls.columns > 1:
self.add_array_modifier(cls)
else:
self.array = None
# Bevel
if hasattr(cls, "use_bevel") and cls.use_bevel:
self.add_bevel_modifier(cls, affect='VERTICES')
else:
self.bevel = None
return self
def update(self, cls, effect):
"""Update bevel modifier during modal."""
# Update array count.
if effect == 'ARRAY_COUNT':
if self.array is None:
self.add_array_modifier(cls)
else:
if cls.columns > 1 or cls.rows > 1:
self.array["Socket_2"] = cls.columns
self.array["Socket_3"] = cls.rows
# Remove modifier if it's no longer needed.
if cls.columns == 1 and cls.rows == 1:
cls.cutter.obj.modifiers.remove(self.array)
self.array = None
# Update array gap.
if effect == 'ARRAY_GAP':
if cls.columns > 1 or cls.row > 1:
if self.array is not None:
self.array["Socket_4"] = cls.gap
# Force the modifier to update in viewport.
self.array.show_viewport = False
self.array.show_viewport = True
# Update bevel width & segments
if effect == 'BEVEL':
self.bevel.segments = cls.bevel_segments
self.bevel.width = cls.bevel_width
# Array
def add_array_modifier(self, cls):
"""Adds an array modifier(s) on the cutter object."""
cutter = cls.cutter.obj
# Load geometry nodes modifier asset.
if self.array is None:
root = os.path.abspath(os.path.join(__file__, "..", "..", ".."))
assets_path = os.path.join(root, "assets.blend")
mod = add_modifier_asset(cutter, path=assets_path, asset="cutter_array")
if not mod:
cls.report({'WARNING'}, "Array modifier cannot be loaded for cutter")
return
# Columns
if cls.columns > 1:
mod["Socket_2"] = cls.columns
# Rows
if cls.rows > 1:
mod["Socket_3"] = cls.rows
# Gap
mod["Socket_4"] = cls.gap
self.array = mod
# Bevel
def add_bevel_modifier(self, cls, affect='EDGES'):
"""Adds a bevel modifier on the cutter object."""
cutter = cls.cutter.obj
bm = cls.cutter.bm
faces = cls.cutter.faces
mod = cutter.modifiers.new("cutter_bevel", 'BEVEL')
mod.limit_method = 'WEIGHT'
mod.segments = cls.bevel_segments
mod.width = cls.bevel_width
mod.profile = cls.bevel_profile
"""NOTE:
In order to allow beveling during the shape creation phase,
when we only have one face, we need to bevel vertices instead of edges,
and then change it to edges when cutter is manifold (and transfer weights).
"""
mod.affect = affect
if affect == 'EDGES':
attr = ensure_attribute(bm, "bevel_weight_edge", 'EDGE')
# Mark all edges except ones belonging to original and extruded face.
for edge in bm.edges:
if edge in faces[0].edges:
continue
if edge in faces[-1].edges:
continue
edge[attr] = 1.0
elif affect == 'VERTICES':
attr = ensure_attribute(bm, "bevel_weight_vert", 'VERTEX')
face = cls.cutter.faces[0]
# Mark vertices of the original face.
verts = [vert for vert in face.verts]
for v in verts:
v[attr] = 1.0
# Add Weld modifier (necessary for merging overlapping vertices).
# Otherwise live cut produces corrupted booleans because of non-manifold geometry.
self.add_weld_modifier(cls)
self.bevel = mod
def transfer_bevel_weights(self, cls):
"""Transfer bevel weights from vertices to edges."""
if not cls.use_bevel:
return
bm = cls.cutter.bm
faces = cls.cutter.faces
# Ensure default edge weights attribute.
edge_attr = ensure_attribute(bm, "bevel_weight_edge", 'EDGE')
for edge in bm.edges:
if edge in faces[0].edges:
continue
if edge in faces[-1].edges:
continue
edge[edge_attr] = 1.0
self.bevel.affect = 'EDGES'
# Smooth by Angle
def add_auto_smooth_modifier(self, cls, context):
"""Adds a 'Smooth by Angle' modifier on cutter object, a.k.a. Auto Smooth."""
obj = cls.cutter.obj
mesh = cls.cutter.mesh
bm = cls.cutter.bm
modifier_asset_path = "nodes\\geometry_nodes_essentials.blend\\NodeTree\\Smooth by Angle"
modifier_asset_file = modifier_asset_path[:modifier_asset_path.find(".blend") + 6]
modifier_asset_name = modifier_asset_path.rsplit("\\", 1)[1]
# Try adding modifier with `bpy.ops` operator(s) first.
context_override = {
"object": obj,
"active_object": obj,
"selected_objects": [obj],
"selected_editable_objects": [obj],
}
with context.temp_override(**context_override):
try:
# Try adding the modifier with `shade_auto_smooth` operator.
bpy.ops.object.shade_auto_smooth()
except:
# Try adding the modifier with path to Essentials library.
bpy.ops.object.modifier_add_node_group(asset_library_type="ESSENTIALS",
asset_library_identifier="",
relative_asset_identifier=modifier_asset_path)
mod = obj.modifiers.active
# Try loading the node group manually if `bpy.ops` operators fail.
if mod is None:
dir = os.path.join(os.path.dirname(bpy.app.binary_path), "5.0", "datafiles", "assets")
assets_path = os.path.join(dir, modifier_asset_file)
mod = add_modifier_asset(obj, path=assets_path, asset=modifier_asset_name)
# Resort to destructive editing if everything fails.
if mod is None:
print("Smooth by Angle modifier couldn't be added.")
print("Destructively marking sharp edges and smooth faces in the mesh")
shade_smooth_by_angle(bm, mesh, angle=math.degrees(cls.sharp_angle))
else:
# Set smoothing angle.
for face in bm.faces:
face.smooth = True
bm.to_mesh(mesh)
mod.use_pin_to_last = True
mod["Input_1"] = cls.sharp_angle
self.smooth = mod
# Weld
def add_weld_modifier(self, cls):
if self.weld is None:
self.weld = cls.cutter.obj.modifiers.new("cutter_weld", 'WELD')
return self.weld
@@ -7,13 +7,24 @@ from ... import __package__ as base_package
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)
if context.region.type == 'TOOL_HEADER':
layout.prop(props, "mode", text="")
layout.prop(props, "alignment", text="")
layout.prop(props, "depth", text="")
layout.prop(props, "solver", expand=True)
else:
# Use labels for Properties editor/sidebar.
layout.prop(props, "mode", text="Mode")
layout.prop(props, "alignment", text="Alignment")
layout.prop(props, "depth", text="Depth")
row = layout.row()
row.prop(props, "solver", expand=True)
layout.separator()
# Popovers
layout.popover("TOPBAR_PT_carver_shape", text="Shape")
layout.popover("TOPBAR_PT_carver_array", text="Array")
layout.popover("TOPBAR_PT_carver_effects", text="Effects")
layout.popover("TOPBAR_PT_carver_cutter", text="Cutter")
@@ -21,7 +32,7 @@ def carver_ui_common(context, layout, props):
#### ------------------------------ /popovers/ ------------------------------ ####
class TOPBAR_PT_carver_shape(bpy.types.Panel):
bl_label = "Carver Shape"
bl_label = "Cutter Shape"
bl_idname = "TOPBAR_PT_carver_shape"
bl_region_type = 'HEADER'
bl_space_type = 'TOPBAR'
@@ -32,12 +43,14 @@ class TOPBAR_PT_carver_shape(bpy.types.Panel):
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
# Box & Circle
if tool.idname == "object.carve_box" or tool.idname == "object.carve_circle":
props = tool.operator_properties("object.carve_box")
if tool.idname == "object.carve_box":
props = tool.operator_properties("object.carve_box")
else:
props = tool.operator_properties("object.carve_circle")
if tool.idname == "object.carve_circle":
layout.prop(props, "subdivision", text="Vertices")
@@ -45,29 +58,24 @@ class TOPBAR_PT_carver_shape(bpy.types.Panel):
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
if props.alignment == 'SURFACE':
layout.prop(props, "orientation")
layout.prop(props, "offset", text="Offset")
layout.prop(props, "align_to_all")
if props.alignment == 'CURSOR':
layout.prop(props, "alignment_axis", text="Align to", expand=True)
# Polyline
elif tool.idname == "object.carve_polyline":
props = tool.operator_properties("object.carve_polyline")
layout.prop(props, "closed")
if props.alignment == 'SURFACE':
layout.prop(props, "offset", text="Offset")
layout.prop(props, "align_to_all")
class TOPBAR_PT_carver_array(bpy.types.Panel):
bl_label = "Carver Array"
bl_idname = "TOPBAR_PT_carver_array"
class TOPBAR_PT_carver_effects(bpy.types.Panel):
bl_label = "Cutter Effects"
bl_idname = "TOPBAR_PT_carver_effects"
bl_region_type = 'HEADER'
bl_space_type = 'TOPBAR'
bl_category = 'Tool'
@@ -78,26 +86,35 @@ class TOPBAR_PT_carver_array(bpy.types.Panel):
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":
if tool.idname == "object.carve_box":
props = tool.operator_properties("object.carve_box")
elif tool.idname == "object.carve_circle":
props = tool.operator_properties("object.carve_circle")
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")
# Bevel
if tool.idname == 'object.carve_box':
header, panel = layout.panel("OBJECT_OT_carver_effects_bevel", default_closed=False)
header.label(text="Bevel")
if panel:
panel.prop(props, "use_bevel", text="Side Bevel")
col = panel.column(align=True)
col.prop(props, "bevel_segments", text="Segments")
col.prop(props, "bevel_width", text="Radius")
col.prop(props, "bevel_profile", text="Profile", slider=True)
# 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")
if props.use_bevel == False:
col.enabled = False
# Array
header, panel = layout.panel("OBJECT_OT_carver_effects_array", default_closed=False)
header.label(text="Array")
if panel:
col = panel.column(align=True)
col.prop(props, "columns")
col.prop(props, "rows")
col.prop(props, "gap")
class TOPBAR_PT_carver_cutter(bpy.types.Panel):
bl_label = "Carver Cutter"
@@ -112,23 +129,31 @@ class TOPBAR_PT_carver_cutter(bpy.types.Panel):
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":
if tool.idname == "object.carve_box":
props = tool.operator_properties("object.carve_box")
elif tool.idname == "object.carve_circle":
props = tool.operator_properties("object.carve_circle")
elif tool.idname == "object.carve_polyline":
props = tool.operator_properties("object.carve_polyline")
# modifier_&_cutter
col = layout.column()
row = col.row()
row.prop(props, "display", text="Display", expand=True)
col.prop(props, "pin", text="Pin Modifier")
if props.mode == 'MODIFIER':
col.prop(props, "parent")
col.prop(props, "hide")
col.prop(props, "cutter_origin", text="Origin")
# auto_smooth
layout.separator()
col = layout.column(align=True)
col.prop(props, "auto_smooth", text="Auto Smooth")
col.prop(props, "sharp_angle")
col1 = layout.column()
col1.prop(props, "sharp_angle")
if not props.auto_smooth:
col1.enabled = False
@@ -136,7 +161,7 @@ class TOPBAR_PT_carver_cutter(bpy.types.Panel):
classes = [
TOPBAR_PT_carver_shape,
TOPBAR_PT_carver_array,
TOPBAR_PT_carver_effects,
TOPBAR_PT_carver_cutter,
]