2026-01-01
This commit is contained in:
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user