update addons (for 5.2)

This commit is contained in:
Nathan
2026-07-14 14:44:58 -06:00
parent a232ea1c9c
commit e3310263f3
246 changed files with 21097 additions and 9607 deletions
@@ -21,12 +21,12 @@ else:
#### ------------------------------ REGISTRATION ------------------------------ ####
"""NOTE: Order of modules is important because of dependancies. Don't change without a reason."""
modules = [
modules = (
carver_circle,
carver_box,
carver_polyline,
ui,
]
)
main_tools = [
carver_box.OBJECT_WT_carve_box,
@@ -1,13 +1,18 @@
import bpy
import os
from mathutils import Vector
from .. import __file__ as base_file
from ..constants import (
ICONS_PATH,
)
from ..functions.view import (
redraw_regions,
)
from .common.base import (
CarverBase,
)
from .common.properties import (
CarverPropsArray,
CarverPropsBevel,
)
from .common.types import (
@@ -34,7 +39,7 @@ 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", "tool_icons", "ops.object.carver_box")
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_box")
bl_keymap = (
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}),
("object.carve_box", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}),
@@ -58,9 +63,7 @@ class MESH_WT_carve_box(OBJECT_WT_carve_box):
#### ------------------------------ OPERATORS ------------------------------ ####
class OBJECT_OT_carve_box(CarverBase,
CarverPropsArray,
CarverPropsBevel):
class OBJECT_OT_carve_box(CarverBase, CarverPropsBevel):
bl_idname = "object.carve_box"
bl_label = "Box Carve"
bl_description = description
@@ -70,6 +73,7 @@ class OBJECT_OT_carve_box(CarverBase,
# SHAPE-properties
shape = 'BOX'
# NOTE: There are registered on operator level because they need to be overriden or hidden per-tool.
aspect: bpy.props.EnumProperty(
name = "Aspect",
description = "The initial aspect",
@@ -115,8 +119,8 @@ class OBJECT_OT_carve_box(CarverBase,
# cached_variables
"""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._initial_origin = self.origin # Initial shape origin.
self._initial_aspect = self.aspect # Initial shape aspect.
self._stored_phase = "DRAW"
# Add Draw Handler
@@ -130,12 +134,11 @@ class OBJECT_OT_carve_box(CarverBase,
def modal(self, context, event):
redraw_regions(context)
# Status Bar Text
self.status(context)
# find_the_limit_of_the_3d_viewport_region
self.redraw_region(context)
# Modifier Keys
self.event_aspect(context, event)
self.event_origin(context, event)
@@ -151,7 +154,6 @@ class OBJECT_OT_carve_box(CarverBase,
if self.phase != "BEVEL":
return {'PASS_THROUGH'}
# Mouse Move
if event.type == 'MOUSEMOVE':
self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y))
@@ -164,7 +166,6 @@ class OBJECT_OT_carve_box(CarverBase,
elif self.phase == "EXTRUDE":
self.set_extrusion_depth(context)
# Confirm
elif event.type == 'LEFTMOUSE':
# Confirm Shape
@@ -178,7 +179,7 @@ class OBJECT_OT_carve_box(CarverBase,
min_distance = 5
if delta_x < min_distance or delta_y < min_distance:
self.finalize(context, clean_up=True, abort=True)
self.finalize(context, abort=True)
return {'FINISHED'}
self.extrude_cutter(context)
@@ -188,18 +189,15 @@ class OBJECT_OT_carve_box(CarverBase,
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.finalize(context, clean_up=True, abort=True)
self.finalize(context, abort=True)
return {'FINISHED'}
return {'RUNNING_MODAL'}
@@ -305,9 +303,9 @@ class OBJECT_OT_carve_box(CarverBase,
#### ------------------------------ REGISTRATION ------------------------------ ####
classes = [
classes = (
OBJECT_OT_carve_box,
]
)
def register():
for cls in classes:
@@ -1,6 +1,9 @@
import bpy
import os
from .. import __file__ as base_file
from ..constants import (
ICONS_PATH,
)
from .common.ui import (
carver_ui_common,
@@ -20,7 +23,7 @@ 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", "tool_icons", "ops.object.carver_circle")
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_circle")
bl_keymap = (
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG'}, {"properties": None}),
("object.carve_circle", {"type": 'LEFTMOUSE', "value": 'CLICK_DRAG', "shift": True}, {"properties": None}),
@@ -51,6 +54,7 @@ class OBJECT_OT_carve_circle(OBJECT_OT_carve_box):
# SHAPE-properties
shape = 'CIRCLE'
# NOTE: There are registered on operator level because they need to be overriden or hidden per-tool.
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",
@@ -76,9 +80,9 @@ class OBJECT_OT_carve_circle(OBJECT_OT_carve_box):
#### ------------------------------ REGISTRATION ------------------------------ ####
classes = [
classes = (
OBJECT_OT_carve_circle,
]
)
def register():
for cls in classes:
@@ -1,16 +1,18 @@
import bpy
import math
import os
from mathutils import Vector
from bpy_extras import view3d_utils
from .. import __file__ as base_file
from ..constants import (
ICONS_PATH,
)
from ..functions.view import (
redraw_regions,
)
from .common.base import (
CarverBase,
)
from .common.properties import (
CarverPropsArray,
)
from .common.types import (
Selection,
Mouse,
@@ -35,7 +37,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", "tool_icons", "ops.object.carver_polyline")
bl_icon = os.path.join(ICONS_PATH, "dat", "ops.object.carver_polyline")
bl_keymap = (
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK'}, None),
("object.carve_polyline", {"type": 'LEFTMOUSE', "value": 'CLICK', "ctrl": True}, None),
@@ -57,8 +59,7 @@ class MESH_WT_carve_polyline(OBJECT_WT_carve_polyline):
#### ------------------------------ OPERATORS ------------------------------ ####
class OBJECT_OT_carve_polyline(CarverBase,
CarverPropsArray):
class OBJECT_OT_carve_polyline(CarverBase):
bl_idname = "object.carve_polyline"
bl_label = "Polyline Carve"
bl_description = description
@@ -108,12 +109,11 @@ class OBJECT_OT_carve_polyline(CarverBase,
def modal(self, context, event):
redraw_regions(context)
# Status Bar Text
self.status(context)
# find_the_limit_of_the_3d_viewport_region
self.redraw_region(context)
# Modifier Keys
self.event_array(context, event)
self.event_move(context, event)
@@ -124,7 +124,6 @@ class OBJECT_OT_carve_polyline(CarverBase,
if self.phase != "BEVEL":
return {'PASS_THROUGH'}
# Mouse Move
if event.type == 'MOUSEMOVE':
self.mouse.current = Vector((event.mouse_region_x, event.mouse_region_y))
@@ -146,12 +145,15 @@ class OBJECT_OT_carve_polyline(CarverBase,
elif self.phase == "EXTRUDE":
self.set_extrusion_depth(context)
# Add Points & Confirm
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:
# Add Point
if self._distance_from_first < 75:
self._insert_polyline_point()
# Confirm Shape (if clicked near the first vert).
else:
verts = self.cutter.verts
if len(verts) > 3:
self._remove_polyline_point(context, jump_mouse=False)
@@ -165,17 +167,12 @@ class OBJECT_OT_carve_polyline(CarverBase,
else:
return {'RUNNING_MODAL'}
# Add Point
else:
self._insert_polyline_point()
# Confirm Depth
if self.phase == "EXTRUDE":
self.confirm(context)
return {'FINISHED'}
# Confirm
# Enter (Confirm)
elif event.type == 'RET':
verts = self.cutter.verts
if len(verts) > 2:
@@ -195,18 +192,14 @@ class OBJECT_OT_carve_polyline(CarverBase,
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':
self._remove_polyline_point(context)
# Cancel
elif event.type in {'RIGHTMOUSE', 'ESC'}:
self.finalize(context, clean_up=True, abort=True)
self.finalize(context, abort=True)
return {'FINISHED'}
return {'RUNNING_MODAL'}
@@ -301,7 +294,7 @@ class OBJECT_OT_carve_polyline(CarverBase,
context.workspace.status_text_set(modal_keys_extrude)
# Polyline-specific features.
# Polyline-specific methods.
def _insert_polyline_point(self):
"""Inserts a new vertex in the cutter geometry and connects it to the previous last one."""
@@ -383,15 +376,16 @@ class OBJECT_OT_carve_polyline(CarverBase,
context.region_data,
vert_world)
if screen_pos:
context.window.cursor_warp(int(screen_pos.x), int(screen_pos.y))
x, y = int(screen_pos.x), int(screen_pos.y)
context.window.cursor_warp(x, y)
#### ------------------------------ REGISTRATION ------------------------------ ####
classes = [
classes = (
OBJECT_OT_carve_polyline,
]
)
def register():
for cls in classes:
@@ -1,33 +1,22 @@
import bpy
import bmesh
import math
import mathutils
from bpy_extras import view3d_utils
from mathutils import Vector, Matrix
from .types import (
Effects,
from ...functions.cutter import (
make_cutter,
)
from .properties import (
CarverPropsOperator,
CarverPropsShape,
CarverPropsModifier,
CarverPropsCutter,
)
from ...functions.draw import (
draw_shader,
draw_bmesh_faces,
draw_circle_around_point,
)
from ...functions.math import (
distance_from_point_to_segment,
region_2d_to_plane_3d,
region_2d_to_line_3d,
draw_circle_billboard,
)
from ...functions.mesh import (
is_instanced_mesh,
extrude_face,
are_intersecting,
raycast,
)
from ...functions.modifier import (
add_boolean_modifier,
@@ -35,300 +24,44 @@ from ...functions.modifier import (
get_modifiers_to_apply,
)
from ...functions.object import (
set_cutter_properties,
delete_empty_collection,
delete_cutter,
set_object_origin,
)
from ...functions.poll import (
is_linked,
is_instanced_data,
set_object_origin,
delete_object,
)
from ...functions.scene import (
delete_empty_collection,
raycast,
)
from ...functions.view import (
distance_from_point_to_segment,
region_2d_to_ray_3d,
region_2d_to_plane_3d,
)
from .events import (
CarverEvents,
)
from .properties import (
CarverPropsOperator,
CarverPropsShape,
CarverPropsModifier,
CarverPropsCutter,
CarverPropsArray,
)
#### ------------------------------ /base/ ------------------------------ ####
class CarverEvents():
def _custom_modifier_event(self, context, event, modifier,
cursor='NONE', store_values=False, restore_mouse=True,
postprocess=None):
"""Creates custom modifier event when key is held and hides cursor until it's released"""
# Initialize Modifier Phase
if event.value == 'PRESS':
if self.phase in ("DRAW", "EXTRUDE"):
self._stored_phase = self.phase
self.phase = modifier
self.mouse.cached = self.mouse.current
context.window.cursor_set(cursor)
if store_values:
# Store center of the geometry as a Vector.
verts = [v.co for v in self.cutter.bm.verts]
center = sum(verts, Vector()) / len(verts)
self.cutter.center = self.cutter.obj.matrix_world @ center
# End Modifier Phase
elif event.value == 'RELEASE':
if self.phase == modifier:
context.window.cursor_set('MUTE')
if restore_mouse:
context.window.cursor_warp(int(self.mouse.cached[0]), int(self.mouse.cached[1]) + 100)
self.mouse.current = self.mouse.cached
self.phase = self._stored_phase
if postprocess is not None:
postprocess(self)
return self._stored_phase
# Individual Events
def event_aspect(self, context, event):
"""Modifier keys for changing aspect of the shape"""
if event.shift and self.phase == "DRAW":
if self.initial_aspect == 'FREE':
self.aspect = 'FIXED'
elif self.initial_aspect == 'FIXED':
self.aspect = 'FREE'
else:
self.aspect = self.initial_aspect
def event_origin(self, context, event):
"""Modifier keys for changing the origin of the shape"""
if event.alt and self.phase == "DRAW":
if self.initial_origin == 'EDGE':
self.origin = 'CENTER'
elif self.initial_origin == 'CENTER':
self.origin = 'EDGE'
else:
self.origin = self.initial_origin
def event_rotate(self, context, event):
"""Modifier keys for rotating the shape"""
def _remove_rotate_phase_properties(self):
del self._stored_mouse_pos_3d
del self._stored_rotation
del self._stored_cutter_center
del self._stored_cutter_euler
# Restore origin at edge (first vertex).
if self.origin == 'EDGE':
point = self.cutter.bm.verts[0].co
set_object_origin(self.cutter.obj, self.cutter.bm, point='CUSTOM', custom=point)
# Set correct phase.
if event.type == 'R':
stored_phase = self._custom_modifier_event(context, event, "ROTATE",
cursor='MOVE_X', store_values=True, restore_mouse=False,
postprocess=_remove_rotate_phase_properties)
if self.phase == "ROTATE":
region = context.region
rv3d = context.region_data
# Project current mouse position onto the workplane.
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
self.mouse.current,
(self.workplane.location, self.workplane.normal))
if current_mouse_pos_3d is not None:
# Store values.
obj = self.cutter.obj
if not hasattr(self, '_stored_mouse_pos_3d'):
self._stored_mouse_pos_3d = current_mouse_pos_3d.copy()
self._stored_rotation = self.rotation
self._stored_cutter_center = self.cutter.center
self._stored_cutter_euler = obj.rotation_euler.copy()
# Calculate angle and direction.
to_start = (self._stored_mouse_pos_3d - self._stored_cutter_center).normalized()
to_current = (current_mouse_pos_3d - self._stored_cutter_center).normalized()
angle = to_start.angle(to_current)
cross = to_start.cross(to_current)
if cross.dot(self.workplane.normal) < 0:
angle = -angle
if abs(angle) > 0.0001:
self.rotation = self._stored_rotation + angle
# Offset the object location when drawing from edge to move rotation pivot to center.
if self.origin == 'EDGE':
set_object_origin(obj, self.cutter.bm, point='CENTER_OBJ')
# Calculate rotation amount.
rotation_total = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
rotation_stored = Matrix.Rotation(self._stored_rotation, 4, self.workplane.normal)
rotation_matrix = rotation_total @ rotation_stored.inverted()
new_rot = rotation_matrix @ self._stored_cutter_euler.to_matrix().to_4x4()
# Rotate.
obj.rotation_euler = new_rot.to_euler()
def event_bevel(self, context, event):
"""Modifier keys for beveling the shape"""
def _remove_empty_bevel_modifier(self):
bevel = self.effects.bevel
if bevel.width == 0:
self.cutter.obj.modifiers.remove(bevel)
self.effects.bevel = None
if self.effects.weld is not None:
self.cutter.obj.modifiers.remove(self.effects.weld)
self.effects.weld = None
if self.shape != 'BOX':
return
# Set correct phase.
if event.type == 'B':
stored_phase = self._custom_modifier_event(context, event, "BEVEL",
cursor='PICK_AREA', store_values=True,
postprocess=_remove_empty_bevel_modifier)
if self.phase == "BEVEL":
self.use_bevel = True
# Initialize bevel effect if it doesn't exist.
if self.effects.bevel is None:
self.bevel_width = 0
affect = 'VERTICES' if stored_phase == "DRAW" else 'EDGES'
self.effects.add_bevel_modifier(self, affect=affect)
# Force the geometry to update.
if stored_phase == "DRAW":
self.update_cutter_shape(context)
elif stored_phase == "EXTRUDE":
self.set_extrusion_depth(context)
# Calculate bevel width.
region = context.region
rv3d = context.region_data
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.cached, self.cutter.center)
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.current, self.cutter.center)
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
self.bevel_width = d * 0.2
# Adjust bevel segments.
if event.type == 'WHEELUPMOUSE':
self.bevel_segments += 1
elif event.type == 'WHEELDOWNMOUSE':
self.bevel_segments -= 1
# Update modifier.
self.effects.update(self, 'BEVEL')
def event_array(self, context, event):
"""Modifier keys for creating the array of the shape"""
# Add duplicates.
if event.type == 'LEFT_ARROW' and event.value == 'PRESS':
self.columns -= 1
if event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
self.columns += 1
if event.type == 'DOWN_ARROW' and event.value == 'PRESS':
self.rows -= 1
if event.type == 'UP_ARROW' and event.value == 'PRESS':
self.rows += 1
if event.type in ['LEFT_ARROW',
'RIGHT_ARROW',
'DOWN_ARROW',
'UP_ARROW',] and event.value == 'PRESS':
self.effects.update(self, 'ARRAY_COUNT')
# Force the geometry to update.
if self.phase == "DRAW":
self.update_cutter_shape(context)
elif self.phase == "EXTRUDE":
self.set_extrusion_depth(context)
# Adjust gap.
if (self.rows > 1 or self.columns > 1) and (event.type == 'A'):
stored_phase = self._custom_modifier_event(context, event, "ARRAY", cursor='MUTE',
store_values=True, restore_mouse=True)
if self.phase == "ARRAY":
region = context.region
rv3d = context.region_data
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.cached, self.cutter.center)
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d, self.mouse.current, self.cutter.center)
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
self.gap = 1 + (d * 0.2)
self.effects.update(self, 'ARRAY_GAP')
def event_flip(self, context, event):
"""Modifier keys for flipping the direction of extrusion."""
if event.type == 'F' and event.value == 'PRESS':
if self.phase == 'EXTRUDE':
self.flip_direction = not self.flip_direction
def event_move(self, context, event):
"""Modifier keys for moving the cutter shape."""
def _remove_move_phase_properties(self):
del self._stored_cutter_location
self.mouse.cached_3d = None
if event.type == 'SPACE':
stored_phase = self._custom_modifier_event(context, event, "MOVE",
cursor='SCROLL_XY', restore_mouse=False,
postprocess=_remove_move_phase_properties)
if self.phase == "MOVE":
region = context.region
rv3d = context.region_data
# Project current mouse position onto the workplane.
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
self.mouse.current,
(self.workplane.location, self.workplane.normal))
if current_mouse_pos_3d is not None:
if not hasattr(self, '_stored_cutter_location'):
self.mouse.cached_3d = current_mouse_pos_3d.copy()
self._stored_cutter_location = self.cutter.obj.location.copy()
offset = current_mouse_pos_3d - self.mouse.cached_3d
self.cutter.obj.location = self._stored_cutter_location + offset
#### ------------------------------ CLASSES ------------------------------ ####
class CarverBase(bpy.types.Operator,
CarverEvents,
CarverPropsOperator,
CarverPropsShape,
CarverPropsModifier,
CarverPropsCutter):
CarverPropsCutter,
CarverPropsArray):
"""Base class for Carver operators."""
def redraw_region(self, context):
"""Redraw the 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()
# Core Methods
def validate_selection(self, context):
"""Filter out selection to get the list of viable canvases."""
@@ -349,10 +82,14 @@ class CarverBase(bpy.types.Operator,
continue
if self.mode == 'DESTRUCTIVE':
if is_instanced_data(obj):
if is_instanced_mesh(obj.data):
self.report({'WARNING'}, f"Modifiers cannot be applied to {obj.name} because it has instanced object data")
continue
if obj.data.shape_keys:
self.report({'WARNING'}, f"Modifiers cannot be applied to {obj.name} because it has shape keys")
continue
selected.append(obj)
# Ensure the active object.
@@ -367,11 +104,10 @@ class CarverBase(bpy.types.Operator,
return selected, active
# Core Methods
def calculate_workplane(self, context):
"""
Calculates matrix, location (origin point), and normal (direction)
of the workplane based on the active alignment method.
of the workplane based on the chosen alignment method.
"""
if self.alignment == 'SURFACE':
@@ -393,7 +129,7 @@ class CarverBase(bpy.types.Operator,
def create_cutter(self, context):
"""Creates a cutter object with correct properties & initializes `bmesh` geometry."""
"""Creates a cutter object with correct properties & initializes `bmesh`."""
# Create the Mesh & bmesh
mesh = bpy.data.meshes.new(name="boolean_cutter")
@@ -402,15 +138,15 @@ class CarverBase(bpy.types.Operator,
# Create the Object
obj = bpy.data.objects.new("boolean_cutter", mesh)
obj.matrix_world = self.workplane.matrix
set_cutter_properties(context, obj, "Difference", collection=True,
display='WIRE' if self.shape == 'POLYLINE' else self.display)
make_cutter(context, obj, "Difference", collection=True,
display='WIRE' if self.shape == 'POLYLINE' else self.display)
obj.booleans.carver = True
# Initial Rotation
if self.rotation != 0:
rotation_matrix = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
temp_matrix_world = rotation_matrix @ obj.matrix_world
obj.rotation_euler = temp_matrix_world.to_euler()
rotation_matrix = rotation_matrix @ obj.matrix_world
obj.rotation_euler = rotation_matrix.to_euler()
# Create Verts
if self.shape == 'BOX':
@@ -471,18 +207,21 @@ class CarverBase(bpy.types.Operator,
# Draw Line
if self.phase in ("BEVEL", "ROTATE", "ARRAY"):
current_mouse_pos_3d = region_2d_to_plane_3d(context.region, context.region_data,
self.mouse.current,
(self.workplane.location, self.workplane.normal))
self.mouse.current,
(self.workplane.location, self.workplane.normal))
if current_mouse_pos_3d is not None:
vertices = [self.cutter.center, current_mouse_pos_3d]
if vertices is not None:
draw_shader('LINES', (0.00, 0.00, 0.00), 1.0, vertices)
# Draw Circle around First Vertex
# Draw circle around first vertex.
if self.shape == 'POLYLINE' and self.phase == 'DRAW':
verts = self.cutter.verts
if len(verts) > 3:
vertices = draw_circle_around_point(context, obj, verts[0], self._distance_from_first, 4)
vertices = draw_circle_billboard(context,
obj.matrix_world @ verts[0].co,
radius=self._distance_from_first,
segments=4)
if len(vertices) > 0:
draw_shader('LINE_LOOP', (0, 0, 0), 1.0, vertices)
@@ -497,7 +236,7 @@ class CarverBase(bpy.types.Operator,
bm = self.cutter.bm
face = self.cutter.faces[0]
# Get the mouse positon on the workplane.
# Get the mouse positon x, y on the workplane.
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
self.mouse.current,
(self.workplane.location, self.workplane.normal))
@@ -582,7 +321,6 @@ class CarverBase(bpy.types.Operator,
if self.alignment == 'CURSOR' and self.depth == 'CURSOR':
self.depth = 'AUTO'
# Push the extruded face towards the furthest point of the collective bounding box.
if self.depth == 'AUTO':
corners = []
@@ -599,10 +337,9 @@ class CarverBase(bpy.types.Operator,
offset = self.offset if self.alignment == 'SURFACE' else 0.1
for v in face.verts:
if self.depth == 'AUTO':
v.co += normal * (furthest_corner - offset)
v.co += normal * (furthest_corner - offset)
# Push the extruded face towards the plane of 3D cursor.
# Push the extruded face towards the plane of the 3D cursor.
elif self.depth == 'CURSOR':
local_cursor = self.cutter.obj.matrix_world.inverted() @ context.scene.cursor.location
for v in extruded_verts:
@@ -647,20 +384,24 @@ class CarverBase(bpy.types.Operator,
region = context.region
rv3d = context.region_data
normal = self.cutter.obj.matrix_world.to_3x3().inverted() @ self.workplane.normal
# Find the point on 3D line (along the normal) closest to the cursor.
# and calculate the distance between that and the extrude origin.
closest_points = region_2d_to_line_3d(region, rv3d,
self.mouse.current,
self._extrude_origin, self.workplane.normal)
# Convert the mouse position into a 3D ray and find closest points between...
# that ray and the line coming from the extrude origin along the workplane normal.
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.current)
closest_points = mathutils.geometry.intersect_line_line(ray_origin,
ray_origin + ray_direction,
self._extrude_origin,
self._extrude_origin + self.workplane.normal)
if closest_points is None:
return
# Calculate the distance between the extrude origin and mouse position.
offset_vector = closest_points[1] - self._extrude_origin
distance = offset_vector.dot(self.workplane.normal)
# Offset vertices of the extruded face from their their original coordinates.
normal = self.cutter.obj.matrix_world.to_3x3().inverted() @ self.workplane.normal
if distance is not None:
for v, vert_co in zip(self._extrude_faces[-1].verts, self._extrude_verts_co):
offset = normal * distance
@@ -695,68 +436,71 @@ class CarverBase(bpy.types.Operator,
ray_obj_matrix = ray.obj.matrix_world
mesh = ray.obj.evaluated_get(context.view_layer.depsgraph).to_mesh()
if mesh is not None:
# create_temporary_bmesh
temp_bm = bmesh.new()
temp_bm.from_mesh(mesh)
temp_bm.faces.ensure_lookup_table()
if mesh is None:
self.alignment = 'VIEW'
return None, None, None
face = temp_bm.faces[ray.index]
# Create temporary `bmesh`.
temp_bm = bmesh.new()
temp_bm.from_mesh(mesh)
temp_bm.faces.ensure_lookup_table()
if self.orientation == 'FACE':
# Get the tangent, normal, and bitangent from the face normal.
tangent = face.calc_tangent_edge()
normal = face.normal
bitangent = normal.cross(tangent)
face = temp_bm.faces[ray.index]
elif self.orientation in ('CLOSEST_EDGE', 'LONGEST_EDGE'):
# Get the tangent, normal, and bitangent from the longest or the closest edge of the face.
if self.orientation == 'FACE':
# Get the tangent, normal, and bitangent from the face normal.
tangent = face.calc_tangent_edge()
normal = face.normal
bitangent = normal.cross(tangent)
if self.orientation == 'LONGEST_EDGE':
lengths = [
(ray_obj_matrix @ edge.verts[0].co - ray_obj_matrix @ edge.verts[1].co).length
for edge in face.edges
]
longest_edge = sorted(zip(lengths, face.edges), key=lambda x: x[0], reverse=True)[0][1]
edge = longest_edge
elif self.orientation in ('CLOSEST_EDGE', 'LONGEST_EDGE'):
# Get the tangent, normal, and bitangent from the longest or the closest edge of the face.
elif self.orientation == 'CLOSEST_EDGE':
distances = [
distance_from_point_to_segment(
ray.location,
ray_obj_matrix @ edge.verts[0].co,
ray_obj_matrix @ edge.verts[1].co,
)
for edge in face.edges
]
closest_edge = sorted(zip(distances, face.edges), key=lambda x: x[0])[0][1]
edge = closest_edge
if self.orientation == 'LONGEST_EDGE':
lengths = [
(ray_obj_matrix @ edge.verts[0].co - ray_obj_matrix @ edge.verts[1].co).length
for edge in face.edges
]
longest_edge = sorted(zip(lengths, face.edges), key=lambda x: x[0], reverse=True)[0][1]
edge = longest_edge
# Get the loop (face corner) for the edge that is also in the face.
face_corner = next(loop for loop in edge.link_loops if loop.face == face)
elif self.orientation == 'CLOSEST_EDGE':
distances = [
distance_from_point_to_segment(
ray.location,
ray_obj_matrix @ edge.verts[0].co,
ray_obj_matrix @ edge.verts[1].co,
)
for edge in face.edges
]
closest_edge = sorted(zip(distances, face.edges), key=lambda x: x[0])[0][1]
edge = closest_edge
start = face_corner.vert
end = face_corner.link_loop_next.vert
direction = (end.co - start.co)
# Get the loop (face corner) for the edge that is also in the face.
face_corner = next(loop for loop in edge.link_loops if loop.face == face)
tangent = edge.calc_tangent(face_corner)
normal = direction.cross(tangent)
bitangent = normal.cross(tangent)
start = face_corner.vert
end = face_corner.link_loop_next.vert
direction = (end.co - start.co)
# Construct Matrix
matrix = Matrix.Identity(4)
matrix[0].xyz = (ray_obj_matrix.to_3x3() @ tangent).normalized()
matrix[1].xyz = (ray_obj_matrix.to_3x3() @ bitangent).normalized()
matrix[2].xyz = (ray_obj_matrix.to_3x3() @ normal).normalized()
matrix[3].xyz = ray.location + (ray.normal * self.offset)
tangent = edge.calc_tangent(face_corner)
normal = direction.cross(tangent)
bitangent = normal.cross(tangent)
# destroy_temporary_bmesh
temp_bm.free()
del mesh
# Construct Matrix
matrix = Matrix.Identity(4)
matrix[0].xyz = (ray_obj_matrix.to_3x3() @ tangent).normalized()
matrix[1].xyz = (ray_obj_matrix.to_3x3() @ bitangent).normalized()
matrix[2].xyz = (ray_obj_matrix.to_3x3() @ normal).normalized()
matrix[3].xyz = ray.location + (ray.normal * self.offset)
matrix = matrix.transposed()
location = ray.location + (ray.normal * self.offset)
normal = ray.normal
# destroy_temporary_bmesh
temp_bm.free()
del mesh
matrix = matrix.transposed()
location = ray.location + (ray.normal * self.offset)
normal = ray.normal
return matrix, location, normal
@@ -776,8 +520,7 @@ class CarverBase(bpy.types.Operator,
context.scene.cursor.location)
else:
# Put the location at the closest point of the bounding box of all selected objects.
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse.initial)
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse.initial)
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.initial)
corners = []
for obj in self.objects.selected:
@@ -803,8 +546,7 @@ class CarverBase(bpy.types.Operator,
matrix = Matrix.Identity(4)
normal = matrix.col[2].xyz
ray_origin = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse.initial)
ray_direction = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse.initial)
ray_origin, ray_direction = region_2d_to_ray_3d(region, rv3d, self.mouse.initial)
t = (matrix.translation.z - ray_origin.z) / ray_direction.z
location = ray_origin + ray_direction * t
matrix.translation = location
@@ -851,7 +593,6 @@ class CarverBase(bpy.types.Operator,
cutter = self.cutter.obj
# Add Modifier(s)
for obj in self.objects.selected:
mod = add_boolean_modifier(self, context, obj,
cutter, "DIFFERENCE",
@@ -877,7 +618,7 @@ class CarverBase(bpy.types.Operator,
obj.modifiers.remove(mod)
if not intersecting_canvases:
self.finalize(context, clean_up=True)
self.finalize(context)
return
# Select all faces of the cutter so that newly created faces in canvas
@@ -920,7 +661,7 @@ class CarverBase(bpy.types.Operator,
for obj in intersecting_canvases:
obj.booleans.canvas = True
self.finalize(context)
self.finalize(context, clean_up=False)
return
elif self.mode == 'DESTRUCTIVE':
@@ -930,11 +671,11 @@ class CarverBase(bpy.types.Operator,
modifiers = get_modifiers_to_apply(context, obj, [modifiers])
apply_modifiers(context, obj, modifiers, force_clean=True)
self.finalize(context, clean_up=True)
self.finalize(context)
return
def finalize(self, context, clean_up=False, abort=False):
def finalize(self, context, clean_up=True, abort=False):
"""
Finalize and clean-up after the operation ends.
Regardless of whether it was confirmed or cancelled.
@@ -943,15 +684,19 @@ class CarverBase(bpy.types.Operator,
# Operation was aborted, or successfully finished in the Destructive mode.
# Delete everything created by the operator (i.e. cutter).
if clean_up:
delete_cutter(self.cutter.obj)
delete_object(self.cutter.obj)
self.cutter.bm.free()
delete_empty_collection()
delete_empty_collection(context)
# Remove modifiers added by the operator.
if abort:
for obj, mod in self.objects.modifiers.items():
obj.modifiers.remove(mod)
# Clean-up temporary changes made by operator.
if self.effects.array:
self.effects.array.use_pin_to_last = False
bpy.types.SpaceView3D.draw_handler_remove(self._handler, 'WINDOW')
context.workspace.status_text_set(None)
context.window.cursor_set('DEFAULT' if context.mode == 'OBJECT' else 'CROSSHAIR')
@@ -0,0 +1,285 @@
import bpy
from bpy_extras import view3d_utils
from mathutils import Vector, Matrix
from ...functions.object import (
set_object_origin,
)
from ...functions.view import (
region_2d_to_plane_3d,
)
#### ------------------------------ CLASSES ------------------------------ ####
class CarverEvents():
# Private Methods.
def _custom_modifier_event(self, context, event, modifier,
cursor='NONE', store_values=False, restore_mouse=True,
postprocess=None):
"""Creates a custom modifier event when the key is held down."""
# Initialize Modifier Phase
if event.value == 'PRESS':
if self.phase in ("DRAW", "EXTRUDE"):
self._stored_phase = self.phase
self.phase = modifier
self.mouse.cached = self.mouse.current
context.window.cursor_set(cursor)
if store_values:
# Store center of the geometry as a Vector.
verts = [v.co for v in self.cutter.bm.verts]
center = sum(verts, Vector()) / len(verts)
self.cutter.center = self.cutter.obj.matrix_world @ center
# End Modifier Phase
elif event.value == 'RELEASE':
if self.phase == modifier:
context.window.cursor_set('MUTE')
if restore_mouse:
context.window.cursor_warp(int(self.mouse.cached[0]), int(self.mouse.cached[1]) + 100)
self.mouse.current = self.mouse.cached
self.phase = self._stored_phase
if postprocess is not None:
postprocess(self)
return self._stored_phase
# Public Methods.
def event_aspect(self, context, event):
"""Modifier key for changing the aspect of the shape."""
if event.shift and self.phase == "DRAW":
if self._initial_aspect == 'FREE':
self.aspect = 'FIXED'
elif self._initial_aspect == 'FIXED':
self.aspect = 'FREE'
else:
self.aspect = self._initial_aspect
def event_origin(self, context, event):
"""Modifier key for changing the origin of the shape."""
if event.alt and self.phase == "DRAW":
if self._initial_origin == 'EDGE':
self.origin = 'CENTER'
elif self._initial_origin == 'CENTER':
self.origin = 'EDGE'
else:
self.origin = self._initial_origin
def event_rotate(self, context, event):
"""Modifier key for rotating the shape."""
def _remove_rotate_phase_properties(self):
del self._stored_mouse_pos_3d
del self._stored_rotation
del self._stored_cutter_center
del self._stored_cutter_euler
# Restore origin at edge (first vertex).
if self.origin == 'EDGE':
self.cutter.bm.verts.ensure_lookup_table()
point = self.cutter.bm.verts[0].co
set_object_origin(self.cutter.obj, self.cutter.bm, point='CUSTOM', custom=point)
# Set correct phase.
if event.type == 'R':
_rm = False if self._stored_phase == "DRAW" else True
stored_phase = self._custom_modifier_event(context, event, "ROTATE",
cursor='MOVE_X', store_values=True, restore_mouse=_rm,
postprocess=_remove_rotate_phase_properties)
if self.phase == "ROTATE":
region = context.region
rv3d = context.region_data
# Project current mouse position onto the workplane.
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
self.mouse.current,
(self.workplane.location, self.workplane.normal))
if current_mouse_pos_3d is not None:
# Store values.
obj = self.cutter.obj
if not hasattr(self, '_stored_mouse_pos_3d'):
self._stored_mouse_pos_3d = current_mouse_pos_3d.copy()
self._stored_rotation = self.rotation
self._stored_cutter_center = self.cutter.center
self._stored_cutter_euler = obj.rotation_euler.copy()
# Calculate angle and direction.
to_start = (self._stored_mouse_pos_3d - self._stored_cutter_center).normalized()
to_current = (current_mouse_pos_3d - self._stored_cutter_center).normalized()
angle = to_start.angle(to_current)
cross = to_start.cross(to_current)
if cross.dot(self.workplane.normal) < 0:
angle = -angle
if abs(angle) > 0.0001:
self.rotation = self._stored_rotation + angle
# Offset the object location when drawing from edge to move rotation pivot to center.
if self.origin == 'EDGE':
set_object_origin(obj, self.cutter.bm, point='CENTER_OBJ')
# Calculate rotation amount.
rotation_total = Matrix.Rotation(self.rotation, 4, self.workplane.normal)
rotation_stored = Matrix.Rotation(self._stored_rotation, 4, self.workplane.normal)
rotation_matrix = rotation_total @ rotation_stored.inverted()
new_rot = rotation_matrix @ self._stored_cutter_euler.to_matrix().to_4x4()
# Rotate.
obj.rotation_euler = new_rot.to_euler()
def event_bevel(self, context, event):
"""Modifier key for beveling the shape."""
def _remove_empty_bevel_modifier(self):
bevel = self.effects.bevel
if bevel.width == 0:
self.cutter.obj.modifiers.remove(bevel)
self.effects.bevel = None
if self.effects.weld is not None:
self.cutter.obj.modifiers.remove(self.effects.weld)
self.effects.weld = None
if self.shape != 'BOX':
return
# Set correct phase.
if event.type == 'B':
stored_phase = self._custom_modifier_event(context, event, "BEVEL",
cursor='PICK_AREA', store_values=True,
postprocess=_remove_empty_bevel_modifier)
if self.phase == "BEVEL":
self.use_bevel = True
# Initialize bevel effect if it doesn't exist.
if self.effects.bevel is None:
self.bevel_width = 0
affect = 'VERTICES' if stored_phase == "DRAW" else 'EDGES'
self.effects.add_bevel_modifier(self, affect=affect)
# Force the geometry to update.
if stored_phase == "DRAW":
self.update_cutter_shape(context)
elif stored_phase == "EXTRUDE":
self.set_extrusion_depth(context)
# Calculate bevel width.
region = context.region
rv3d = context.region_data
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
self.mouse.cached,
self.cutter.center)
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
self.mouse.current,
self.cutter.center)
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
self.bevel_width = d * 0.2
# Adjust bevel segments.
if event.type == 'WHEELUPMOUSE':
self.bevel_segments += 1
elif event.type == 'WHEELDOWNMOUSE':
self.bevel_segments -= 1
# Update modifier.
self.effects.update(self, "BEVEL")
def event_array(self, context, event):
"""Modifier key for arraying the shape."""
# Add duplicates.
if event.type == 'LEFT_ARROW' and event.value == 'PRESS':
self.columns -= 1
if event.type == 'RIGHT_ARROW' and event.value == 'PRESS':
self.columns += 1
if event.type == 'DOWN_ARROW' and event.value == 'PRESS':
self.rows -= 1
if event.type == 'UP_ARROW' and event.value == 'PRESS':
self.rows += 1
if event.type in ['LEFT_ARROW',
'RIGHT_ARROW',
'DOWN_ARROW',
'UP_ARROW',] and event.value == 'PRESS':
self.effects.update(self, "ARRAY_COUNT")
# Force the geometry to update.
if self.phase == "DRAW":
self.update_cutter_shape(context)
elif self.phase == "EXTRUDE":
self.set_extrusion_depth(context)
# Adjust gap.
if (self.rows > 1 or self.columns > 1) and (event.type == 'A'):
stored_phase = self._custom_modifier_event(context, event, "ARRAY",
cursor='MUTE', store_values=True)
if self.phase == "ARRAY":
region = context.region
rv3d = context.region_data
self.mouse.cached_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
self.mouse.cached,
self.cutter.center)
self.mouse.current_3d = view3d_utils.region_2d_to_location_3d(region, rv3d,
self.mouse.current,
self.cutter.center)
d = (self.cutter.center - self.mouse.current_3d).length - (self.cutter.center - self.mouse.cached_3d).length
self.gap = 1 + (d * 0.2)
self.effects.update(self, "ARRAY_GAP")
def event_flip(self, context, event):
"""Modifier key for flipping the direction of extrusion."""
if event.type == 'F' and event.value == 'PRESS':
if self.phase == 'EXTRUDE':
self.flip_direction = not self.flip_direction
def event_move(self, context, event):
"""Modifier key for moving the shape."""
def _remove_move_phase_properties(self):
del self._stored_cutter_location
self.mouse.cached_3d = None
if event.type == 'SPACE':
stored_phase = self._custom_modifier_event(context, event, "MOVE",
cursor='SCROLL_XY', restore_mouse=False,
postprocess=_remove_move_phase_properties)
if self.phase == "MOVE":
region = context.region
rv3d = context.region_data
# Project current mouse position onto the workplane.
current_mouse_pos_3d = region_2d_to_plane_3d(region, rv3d,
self.mouse.current,
(self.workplane.location, self.workplane.normal))
if current_mouse_pos_3d is not None:
if not hasattr(self, '_stored_cutter_location'):
self.mouse.cached_3d = current_mouse_pos_3d.copy()
self._stored_cutter_location = self.cutter.obj.location.copy()
offset = current_mouse_pos_3d - self.mouse.cached_3d
self.cutter.obj.location = self._stored_cutter_location + offset
@@ -1,40 +1,59 @@
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
from ...ui.icons import (
get_custom_icon,
)
#### ------------------------------ PROPERTIES ------------------------------ ####
class CarverPropsOperator():
# NOTE: Defining enum items inside a function is necessary to allow correct import of custom icons.
# Without this, classes (and their properties) are registered before `register` function of modules
# are called, resulting in empty preview collections.
def get_depth_items(self, context):
icon_measure = get_custom_icon("MEASURE")
icon_cpu = get_custom_icon("CPU")
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),
]
return items
# OPERATOR-properties
mode: bpy.props.EnumProperty(
name = "Mode",
items = (('DESTRUCTIVE', "Destructive",
"Boolean cutters are immediatelly applied and removed after the cut", 'MESH_DATA', 0),
"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)),
"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)),
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 = (('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',
items = get_depth_items,
default = 0,
)
@@ -72,7 +91,7 @@ class CarverPropsShape():
flip_direction: bpy.props.BoolProperty(
name = "Flip Direction",
description = "Change which way the geometry is extruded",
options = {'SKIP_SAVE', 'HIDDEN', 'SKIP_PRESET', },
options = {'SKIP_SAVE', 'HIDDEN'},
default = False,
)
@@ -88,7 +107,7 @@ class CarverPropsModifier():
)
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"
description = ("Always make new Boolean modifiers first in the modifier stack.\n"
"NOTE: Order of modifiers can drastically affect the result (especially in destructive mode)"),
default = True,
)
@@ -98,13 +117,13 @@ class CarverPropsCutter():
# CUTTER-properties
hide: bpy.props.BoolProperty(
name = "Hide Cutter",
description = ("Hide cutter objects in the viewport after they're created."),
description = ("Hide cutter objects in the viewport after they are 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"),
description = ("Parent new cutters to active object (even if they are cutting multiple objects).\n"
"If there is no active object in the selection, parent might be chosen randomly"),
default = True,
)
display: bpy.props.EnumProperty(
@@ -115,24 +134,29 @@ class CarverPropsCutter():
)
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")),
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",
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"),
description = ("Add 'Smooth by Angle' modifier to cutter object"),
default = True,
)
sharp_angle: bpy.props.FloatProperty(
name = "Angle",
description = "Maximum face angle for sharp edges",
subtype = "ANGLE",
precision = 3,
min = 0, max = math.pi,
default = 0.523599,
)
@@ -142,13 +166,13 @@ class CarverPropsArray():
# ARRAY-properties
rows: bpy.props.IntProperty(
name = "Rows",
description = "Number of times shape is duplicated horizontally",
description = "Number of times the shape is duplicated horizontally",
min = 1, soft_max = 16,
default = 1,
)
columns: bpy.props.IntProperty(
name = "Columns",
description = "Number of times shape is duplicated vertically",
description = "Number of times the shape is duplicated vertically",
min = 1, soft_max = 16,
default = 1,
)
@@ -9,13 +9,14 @@ from ...functions.mesh import (
)
from ...functions.modifier import (
add_modifier_asset,
update_modifier_input,
)
#### ------------------------------ CLASSES ------------------------------ ####
class Selection:
"""Storage of viable selected and active object(s) throughout the modal."""
"""Storage of viable cannvas objects and their Boolean modifiers."""
def __init__(self, selected, active):
self.selected: list = selected
@@ -24,10 +25,7 @@ class Selection:
class Mouse:
"""
Mouse positions throughout different phases of the modal operator.
Each class variable is a 2D vector in screen space (x, y).
"""
"""Mouse positions throughout different phases of the modal operator."""
def __init__(self):
self.initial = Vector()
@@ -99,17 +97,17 @@ class Effects:
return self
def update(self, cls, effect):
"""Update bevel modifier during modal."""
"""Update cutter modifiers during modal."""
# Update array count.
if effect == '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
update_modifier_input(self.array, "Socket_2", cls.columns)
update_modifier_input(self.array, "Socket_3", cls.rows)
# Remove modifier if it's no longer needed.
if cls.columns == 1 and cls.rows == 1:
@@ -117,12 +115,12 @@ class Effects:
self.array = None
# Update array gap.
if effect == 'ARRAY_GAP':
if cls.columns > 1 or cls.row > 1:
if effect == "ARRAY_GAP":
if cls.columns > 1 or cls.rows > 1:
if self.array is not None:
self.array["Socket_4"] = cls.gap
update_modifier_input(self.array, "Socket_4", cls.gap)
# Force the modifier to update in viewport.
# NOTE: Force the modifier to update in viewport when key is pressed.
self.array.show_viewport = False
self.array.show_viewport = True
@@ -150,15 +148,16 @@ class Effects:
# Columns
if cls.columns > 1:
mod["Socket_2"] = cls.columns
update_modifier_input(mod, "Socket_2", cls.columns)
# Rows
if cls.rows > 1:
mod["Socket_3"] = cls.rows
update_modifier_input(mod, "Socket_3", cls.rows)
# Gap
mod["Socket_4"] = cls.gap
update_modifier_input(mod, "Socket_4", cls.gap)
mod.use_pin_to_last = True
self.array = mod
@@ -233,7 +232,7 @@ class Effects:
# 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."""
"""Adds a 'Smooth by Angle' modifier on the cutter object (a.k.a. Auto Smooth)."""
obj = cls.cutter.obj
mesh = cls.cutter.mesh
@@ -262,7 +261,7 @@ class Effects:
mod = obj.modifiers.active
# Try loading the node group manually if `bpy.ops` operators fail.
# Try loading the node group manually if `bpy.ops` operators failed.
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)
@@ -274,13 +273,13 @@ class Effects:
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.
# Set smoothing angle (necessary for all methods except `shade_auto_smooth()`).
for face in bm.faces:
face.smooth = True
bm.to_mesh(mesh)
mod.use_pin_to_last = True
mod["Input_1"] = cls.sharp_angle
update_modifier_input(mod, "Input_1", cls.sharp_angle)
self.smooth = mod
@@ -1,5 +1,4 @@
import bpy
from ... import __package__ as base_package
#### ------------------------------ /toolbar/ ------------------------------ ####
@@ -14,7 +13,7 @@ def carver_ui_common(context, layout, props):
layout.prop(props, "solver", expand=True)
else:
# Use labels for Properties editor/sidebar.
# NOTE: Use labels for Properties editor/sidebar.
layout.prop(props, "mode", text="Mode")
layout.prop(props, "alignment", text="Alignment")
layout.prop(props, "depth", text="Depth")
@@ -43,20 +42,17 @@ class TOPBAR_PT_carver_shape(bpy.types.Panel):
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')
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
tool = context.workspace.tools.from_space_view3d_mode(mode)
props = tool.operator_properties(tool.idname)
# Box & Circle
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")
else:
props = tool.operator_properties("object.carve_circle")
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)
layout.prop(props, "aspect", expand=True)
if props.alignment == 'SURFACE':
layout.prop(props, "orientation")
@@ -67,7 +63,6 @@ class TOPBAR_PT_carver_shape(bpy.types.Panel):
# Polyline
elif tool.idname == "object.carve_polyline":
props = tool.operator_properties("object.carve_polyline")
if props.alignment == 'SURFACE':
layout.prop(props, "offset", text="Offset")
layout.prop(props, "align_to_all")
@@ -85,13 +80,9 @@ class TOPBAR_PT_carver_effects(bpy.types.Panel):
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":
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")
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
tool = context.workspace.tools.from_space_view3d_mode(mode)
props = tool.operator_properties(tool.idname)
# Bevel
if tool.idname == 'object.carve_box':
@@ -116,6 +107,7 @@ class TOPBAR_PT_carver_effects(bpy.types.Panel):
col.prop(props, "rows")
col.prop(props, "gap")
class TOPBAR_PT_carver_cutter(bpy.types.Panel):
bl_label = "Carver Cutter"
bl_idname = "TOPBAR_PT_carver_cutter"
@@ -128,25 +120,25 @@ class TOPBAR_PT_carver_cutter(bpy.types.Panel):
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":
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")
mode = 'OBJECT' if context.mode == 'OBJECT' else 'EDIT_MESH'
tool = context.workspace.tools.from_space_view3d_mode(mode)
props = tool.operator_properties(tool.idname)
# modifier_&_cutter
# Modifier properties.
col = layout.column()
col.prop(props, "pin", text="Pin Modifier")
col.separator()
# Cutter object properties.
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
# Auto Smooth.
layout.separator()
col = layout.column(align=True)
col.prop(props, "auto_smooth", text="Auto Smooth")
@@ -159,11 +151,11 @@ class TOPBAR_PT_carver_cutter(bpy.types.Panel):
#### ------------------------------ REGISTRATION ------------------------------ ####
classes = [
classes = (
TOPBAR_PT_carver_shape,
TOPBAR_PT_carver_effects,
TOPBAR_PT_carver_cutter,
]
)
def register():
for cls in classes: