import bpy, mathutils, math
from mathutils import Matrix, Vector
from enum import Enum
from ... meshtools.applymod import apply_mod
from ....utility.screen import dpi_factor
from .... utility import addon
from .... ui_framework.master import Master
from .... utility.base_modal_controls import Base_Modal_Controls
from .... utils.toggle_view3d_panels import collapse_3D_view_panels
from .... utils.modal_frame_drawing import draw_modal_frame
from .... utils.objects import set_active
from .... utils.cursor_warp import mouse_warp
from .... utility import method_handler
from .... utility import math as hops_math
from .struct import Axis, Dice_Box_3D, selection_boundary, get_boxelize_ref
from .shader import SD, setup_draw_data
from .interface import alter_form_layout
from . import knife_project, knife_intersect, prepare, remove
MEM_X_COUNT = 5
MEM_Y_COUNT = 5
MEM_Z_COUNT = 5
class Edit_3D:
def __init__(self, op, context, event):
# States
self.knife_method = "Knife" if addon.preference().property.dice_method == 'KNIFE_PROJECT' else "Intersect"
# Dice Structs
self.x_dice = Dice_Box_3D(Axis.X, active=True, segments=MEM_X_COUNT)
self.y_dice = Dice_Box_3D(Axis.Y, active=False, segments=MEM_Y_COUNT)
self.z_dice = Dice_Box_3D(Axis.Z, active=False, segments=MEM_Z_COUNT)
# Entry events
if not event.ctrl:
if op.dice_axis == 'X':
self.x_dice.active = True
elif op.dice_axis == 'Y':
self.y_dice.active = True
elif op.dice_axis == 'Z':
self.z_dice.active = True
# Setup
self.selected = []
self.curves = []
# Controls
self.presets = [3,5,7,11,15,18,20,23,26,29,33,42,45,49,52,63]
self.preset_index = 0
self.prefs = addon.preference().property
self.dpi = dpi_factor(min=.5)
self.exit_to_twist = False
self.mouse_buffer = 0
self.mouse_mode_segments = max(MEM_X_COUNT, MEM_Y_COUNT, MEM_Z_COUNT)
self.modal_scale = addon.preference().ui.Hops_modal_scale
self.cut_active_only = event.shift
# Updates
self.rebuild_setup_data = False
self.rebuild_draw_data = True
# --- ACTIONS --- #
def update(self, op, context, event):
# Setup structs
if self.rebuild_setup_data:
self.rebuild_setup_data = False
self.setup(context, event)
# Check for draw update
self.check_for_updates()
if self.rebuild_draw_data:
self.rebuild_draw_data = False
setup_draw_data(op)
# Hot keys
if not op.form.active():
mouse_warp(context, event)
self.actions(op, context, event)
def actions(self, op, context, event):
# Toggle X
if event.type == 'X' and event.value == 'PRESS':
self.rebuild_draw_data = True
self.x_dice.active = not self.x_dice.active
self.x_dice.segments = self.mouse_mode_segments
self.set_segments_via_mouse_mode()
# Toggle Y
elif event.type == 'Y' and event.value == 'PRESS':
self.rebuild_draw_data = True
self.y_dice.active = not self.y_dice.active
self.y_dice.segments = self.mouse_mode_segments
self.set_segments_via_mouse_mode()
# Toggle Z
elif event.type == 'Z' and event.value == 'PRESS':
self.rebuild_draw_data = True
self.z_dice.active = not self.z_dice.active
self.z_dice.segments = self.mouse_mode_segments
self.set_segments_via_mouse_mode()
# Smart Apply
elif event.type == 'S' and event.value == 'PRESS':
self.smart_apply(context)
# Exit to Twist
elif event.type == 'T' and event.value == 'PRESS':
self.exit_to_twist = not self.exit_to_twist
bpy.ops.hops.display_notification(info=f"Exit To_Twist: {'ON' if self.exit_to_twist else 'OFF'}")
# Join
elif event.type == 'J' and event.value == 'PRESS':
self.join_objs(context, event)
# Exit to Twist
elif event.type == 'Q' and event.value == 'PRESS':
if self.knife_method == "Knife":
self.knife_method = "Intersect"
elif self.knife_method == "Intersect":
self.knife_method = "Knife"
bpy.ops.hops.display_notification(info=f"Method : {self.knife_method}")
# Boxelize
elif event.type == 'B' and event.value == 'PRESS':
self.toggle_boxelize(op)
# Graphics
elif event.type == 'N' and event.value == 'PRESS':
types = ['LINES', 'TICKS', 'DOTS']
index = types.index(self.prefs.dice_wire_type)
self.prefs.dice_wire_type = types[(index + 1) % len(types)]
self.rebuild_draw_data = True
if not op.form.is_dot_open():
self.mouse_actions(op, context, event)
# Keep at least the X axis alive
self.ensure_active_axis()
def mouse_actions(self, op, context, event):
boxelize = get_boxelize_ref()
# Segments
if op.base_controls.scroll:
scroll = op.base_controls.scroll
# Boxelize
if boxelize.active:
boxelize.segments += scroll
if boxelize.segments < 2:
boxelize.segments = 2
self.rebuild_draw_data = True
return
# Presets
if event.shift:
if scroll > 0: self.preset_index += 1
else: self.preset_index -= 1
if self.preset_index < 0:
self.preset_index = len(self.presets) - 1
elif self.preset_index > len(self.presets) - 1:
self.preset_index = 0
self.mouse_mode_segments = self.presets[self.preset_index]
# Standard
else:
self.mouse_mode_segments += scroll
self.set_segments_via_mouse_mode()
# Mouse Axis
if boxelize.active == False:
self.mouse_axis_change(context, event)
def set_segments_via_mouse_mode(self):
if self.x_dice.active:
self.x_dice.segments = self.mouse_mode_segments
if self.y_dice.active:
self.y_dice.segments = self.mouse_mode_segments
if self.z_dice.active:
self.z_dice.segments = self.mouse_mode_segments
def mouse_axis_change(self, context, event):
delta = event.mouse_x - event.mouse_prev_x
delta *= self.dpi
delta = delta * .5 if event.shift else delta
self.mouse_buffer += delta
alter = False
if abs(self.mouse_buffer) > self.modal_scale * 250 * self.dpi:
self.mouse_buffer = 0
alter = True
if not alter: return
structs = [self.x_dice, self.y_dice, self.z_dice]
active = None
for struct in structs:
if struct.active:
# Set the axis to use
if not active:
active = struct
# More than one axis in use
else: return
# Catch
if not active: return
index = structs.index(active)
index = index + 1 if self.mouse_buffer > 0 else index - 1
if index < 0: index = 2
if index > 2: index = 0
for struct in structs:
struct.active = False
structs[index].active = True
self.rebuild_draw_data = True
self.set_segments_via_mouse_mode()
# --- UTILS --- #
def setup(self, context, event):
self.selected = [o for o in context.selected_objects if o.type == 'MESH']
self.curves = [o for o in context.selected_objects if o.type == 'CURVE']
# Smart Apply
if event.alt or addon.preference().property.smart_apply_dice == 'SMART_APPLY':
self.smart_apply(context)
# Convert to mesh
elif addon.preference().property.smart_apply_dice == 'APPLY':
obj = context.active_object
if obj.mode != 'EDIT':
for obj in self.selected:
bpy.ops.object.convert(target='MESH')
bpy.ops.mesh.customdata_custom_splitnormals_clear()
bpy.ops.hops.display_notification(info="Converted To Mesh")
self.selected = [o for o in context.selected_objects if o.type == 'MESH']
self.curves = []
structs = [self.x_dice, self.y_dice, self.z_dice]
# Cut others based on bounds of active
cut_objects = self.selected[:]
if self.cut_active_only and len(self.selected) > 1:
if context.active_object in cut_objects:
cut_objects.remove(context.active_object)
# Matrix
obj = context.active_object
matrix = obj.matrix_world if obj in self.selected else Matrix()
# Rotation for empty
empty_matrix = None
for obj in context.selected_objects:
if obj.type == 'EMPTY':
loc, _, sca = matrix.decompose()
rot = obj.matrix_world.to_quaternion()
mat_loc = mathutils.Matrix.Translation(loc).to_4x4()
mat_rot = rot.to_matrix().to_4x4()
mat_sca = mathutils.Matrix.Diagonal(sca).to_4x4()
empty_matrix = mat_loc @ mat_rot @ mat_sca
break
# Cut active only
if self.cut_active_only:
if len(cut_objects) == 1:
matrix = cut_objects[0].matrix_world
else: matrix = Matrix()
mat = empty_matrix if empty_matrix else matrix
for struct in structs: struct.matrix = mat
# Bounds
bounds = selection_boundary(context, cut_objects, matrix, empty_matrix)
if not bounds: return False
for struct in structs: struct.bounds = bounds
# Center
center = hops_math.coords_to_center(bounds)
for struct in structs: struct.center = center
# Will boxelize if possible
self.boxelize_setup()
return True
def boxelize_setup(self):
boxelize = get_boxelize_ref()
if not boxelize.active: return
def check_for_updates(self):
structs = [self.x_dice, self.y_dice, self.z_dice]
for struct in structs:
if struct.pending_update:
struct.pending_update = False
self.rebuild_draw_data = True
def knife(self, context):
original_mode = context.mode
original_active = context.active_object
bpy.ops.object.mode_set(mode='OBJECT')
use_normal_offset = True if self.knife_method == "Intersect" else False
boxelize = get_boxelize_ref()
cut_objects = self.selected[:]
if self.cut_active_only:
if context.active_object in cut_objects:
cut_objects = [context.active_object]
if self.x_dice.active or boxelize.active:
x_obj = self.x_dice.create_mesh(context, use_normal_offset)
if x_obj:
for obj in cut_objects:
prepare(obj, x_obj)
if self.knife_method == "Knife":
knife_project(context, obj, x_obj, axis='X')
elif self.knife_method == "Intersect":
knife_intersect(context)
remove(x_obj)
if self.y_dice.active or boxelize.active:
y_obj = self.y_dice.create_mesh(context, use_normal_offset)
if y_obj:
for obj in cut_objects:
prepare(obj, y_obj)
if self.knife_method == "Knife":
knife_project(context, obj, y_obj, axis='Y')
elif self.knife_method == "Intersect":
knife_intersect(context)
remove(y_obj)
if self.z_dice.active or boxelize.active:
z_obj = self.z_dice.create_mesh(context, use_normal_offset)
if z_obj:
for obj in cut_objects:
prepare(obj, z_obj)
if self.knife_method == "Knife":
knife_project(context, obj, z_obj, axis='Z')
elif self.knife_method == "Intersect":
knife_intersect(context)
remove(z_obj)
for obj in self.selected:
obj.select_set(True)
set_active(original_active)
if original_mode == 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT')
def ensure_active_axis(self):
if self.x_dice.active: return
if self.y_dice.active: return
if self.z_dice.active: return
self.x_dice.active = True
self.rebuild_draw_data = True
def smart_apply(self, context):
mode = context.mode
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
for obj in self.selected: apply_mod(self, obj, clear_last=False)
if mode == 'EDIT_MESH': bpy.ops.object.mode_set(mode='EDIT', toggle=False)
bpy.ops.hops.display_notification(info="Smart Apply")
self.rebuild_setup_data = True
self.rebuild_draw_data = True
def join_objs(self, context, event):
self.smart_apply(context)
object_count = len(self.selected) + len(self.curves)
mode = context.mode
active_obj = context.active_object
if active_obj not in self.selected:
active_obj = self.selected[0]
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
for obj in self.curves:
context.view_layer.objects.active = obj
obj.select_set(True)
bpy.ops.object.convert('INVOKE_DEFAULT', target='MESH')
context.view_layer.objects.active = active_obj
bpy.ops.object.join()
self.setup(context, event)
if object_count > 1:
bpy.ops.hops.display_notification(info="Objects Joined")
if mode == 'EDIT_MESH':
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
self.rebuild_setup_data = True
self.rebuild_draw_data = True
# --- EXITS --- #
def confirm_exit(self, context, event):
if self.exit_to_twist:
self.smart_apply(context)
self.join_objs(context, event)
SD.exit_with_no_fade = True
self.knife(context)
if SD.exit_with_no_fade == False:
bpy.ops.hops.draw_wire_mesh_launcher('INVOKE_DEFAULT', target='SELECTED')
if self.exit_to_twist:
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
bpy.ops.hops.array_twist('INVOKE_DEFAULT')
if self.x_dice.active:
self.dice_axis = 'X'
elif self.y_dice.active:
self.dice_axis = 'Y'
elif self.z_dice.active:
self.dice_axis = 'Z'
boxelize = get_boxelize_ref()
boxelize.deactivate()
global MEM_X_COUNT, MEM_Y_COUNT, MEM_Z_COUNT
MEM_X_COUNT = self.x_dice.segments
MEM_Y_COUNT = self.y_dice.segments
MEM_Z_COUNT = self.z_dice.segments
def cancel_exit(self, context):
boxelize = get_boxelize_ref()
boxelize.deactivate()
# --- FORM --- #
@property
def boxelize_segments(self):
return get_boxelize_ref().segments
@boxelize_segments.setter
def boxelize_segments(self, val):
get_boxelize_ref().segments = int(val) if int(val) > 0 else 1
self.rebuild_setup_data = True
self.rebuild_draw_data = True
def set_knife_method(self, opt=''):
self.knife_method = opt
def knife_method_hook(self):
return self.knife_method
def set_shader_type(self, opt=''):
self.prefs.dice_wire_type = opt
def shader_type_hook(self):
return self.prefs.dice_wire_type
def toggle_boxelize(self, op):
if op.form.operator_stop_building(): return
boxelize = get_boxelize_ref()
if boxelize.active:
# Transfer the cuts to standard mode on exit
if op.form.is_dot_open():
self.x_dice.segments = len(self.x_dice.loops())
self.y_dice.segments = len(self.y_dice.loops())
self.z_dice.segments = len(self.z_dice.loops())
boxelize.deactivate()
self.rebuild_draw_data = True
self.rebuild_setup_data = True
alter_form_layout(op, preset_label='AXIAL')
return
boxelize.active = True
self.rebuild_draw_data = True
self.rebuild_setup_data = True
alter_form_layout(op, preset_label='BOXELIZE')
def toggle_see_through(self):
SD.see_though = not SD.see_though
def see_through_hook(self):
return SD.see_though