import bpy, mathutils, math, gpu, bmesh
from math import cos, sin
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader
from enum import Enum
from ... utility import addon
from ... utility.base_modal_controls import Base_Modal_Controls, numpad_types
from ... ui_framework.master import Master
from ... ui_framework.utils.checks import is_mouse_in_quad
from ... utils.toggle_view3d_panels import collapse_3D_view_panels
from ... utility import method_handler
from ... utils.space_3d import get_3D_point_from_mouse, scene_ray_cast, get_2d_point_from_3d_point
from ...utility.screen import dpi_factor
from ... utility.shader import point_uniform
class Widget:
def __init__(self):
# Dims
self.x_offset = 128 * dpi_factor()
self.y_offset = 128 * dpi_factor()
self.loc = Vector((self.x_offset, self.y_offset))
self.radius = 18 * dpi_factor()
# Shader
built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '2D_UNIFORM_COLOR'
self.shader = gpu.shader.from_builtin(built_in_shader)
# Center
self.center_batch = None
self.center_color = (1,1,1,.25)
self.center_bounds = []
self.center_loc = Vector((0,0))
self.center_setup()
# Line : 1
self.l1_batch = None
self.l1_color = (1,1,1,.25)
self.l1_setup()
# Mirror
self.mirror_batch = None
self.mirror_color = (1,1,1,.25)
self.mirror_bounds = []
self.mirror_loc = Vector((0,0))
self.mirror_setup()
# State
self.mouse_is_over = False
self.center_active = False
self.circle_active = False
def center_setup(self):
width = self.radius * 2
x = self.loc[0]
y = self.loc[1] + self.radius * 4
points = [
Vector((x, y)),
Vector((x, y + width))]
indices = [(0,1)]
self.center_batch = batch_for_shader(self.shader, 'LINES', {'pos': points}, indices=indices)
pad = 10 * dpi_factor()
self.center_bounds = (
(x - pad, y + width),
(x - pad, y),
(x + pad, y + width),
(x + pad, y))
self.center_loc = Vector((x, self.loc[1] + self.radius * 2))
def l1_setup(self):
width = self.radius * 2
x = self.loc[0] - self.radius
y = self.loc[1] + self.radius * 3.5
points = [
Vector((x, y)),
Vector((x + width, y))]
indices = [(0,1)]
self.l1_batch = batch_for_shader(self.shader, 'LINES', {'pos': points}, indices=indices)
def mirror_setup(self):
width = self.radius * 2
x = self.loc[0] - self.radius
y = self.loc[1] + self.radius
self.mirror_bounds = (
(x, y + width),
(x, y),
(x + width, y + width),
(x + width, y))
self.mirror_loc = Vector((self.loc[0], self.loc[1] + self.radius * .5))
x1 = x + width * .25
x2 = x + width * .75
x3 = x + width * .5
points = (
(x, y),
(x1, y + width),
(x3, y),
(x2, y + width),
(x + width, y)
)
indices = [(0,1), (1,2), (2,3), (3,4)]
self.mirror_batch = batch_for_shader(self.shader, 'LINES', {'pos': points}, indices=indices)
def update(self, context, event):
mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y))
# Center
if is_mouse_in_quad(self.center_bounds, mouse_pos):
self.center_active = True
self.center_color = (1,1,1,1)
else:
self.center_active = False
self.center_color = (1,1,1,.25)
# Mirror
if is_mouse_in_quad(self.mirror_bounds, mouse_pos):
self.mirror_active = True
self.mirror_color = (1,1,1,1)
else:
self.mirror_active = False
self.mirror_color = (1,1,1,.25)
def draw(self):
# GL Settings
#Enable(GL_LINE_SMOOTH)
gpu.state.blend_set('ALPHA')
gpu.state.line_width_set(3)
self.shader.bind()
# Center
self.shader.uniform_float('color', self.center_color)
self.center_batch.draw(self.shader)
# Line 1
self.shader.uniform_float('color', self.l1_color)
self.l1_batch.draw(self.shader)
# Mirror
self.shader.uniform_float('color', self.mirror_color)
self.mirror_batch.draw(self.shader)
class Boarder:
def __init__(self, context):
width = context.area.width
height = context.area.height
pad = 15 * dpi_factor(min=.5)
self.width = 4
self.color = (.5, .5, .5, 1)
self.points = [
(pad, pad),
(pad , height - pad * 2),
(width - pad * 2, height - pad * 2),
(width - pad * 2, pad)]
indices = ((0,1), (1,2), (2,3), (3,0))
built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '2D_UNIFORM_COLOR'
self.shader = gpu.shader.from_builtin(built_in_shader)
self.batch = batch_for_shader(self.shader, 'LINES', {"pos": self.points}, indices=indices)
def draw(self):
#Enable(GL_LINE_SMOOTH)
gpu.state.blend_set('ALPHA')
gpu.state.line_width_set(self.width)
self.shader.bind()
self.shader.uniform_float("color", self.color)
self.batch.draw(self.shader)
gpu.state.line_width_set(1)
gpu.state.blend_set('NONE')
class State(Enum):
FIRST = 0
SECOND = 1
ADJUST = 2
DESC = """Sculpt Arms
Add appendages
Press H for help"""
class HOPS_OT_Sculpt_Arms(bpy.types.Operator):
bl_idname = "hops.sculpt_arms"
bl_label = "Sculpt Arms"
bl_description = DESC
bl_options = {"REGISTER", "UNDO", "BLOCKING"}
@classmethod
def poll(cls, context):
if context.active_object:
if context.active_object.type == 'MESH':
if context.mode in {'SCULPT', 'OBJECT'}:
return True
return False
def invoke(self, context, event):
# States
self.og_mode = context.mode
self.state = State.FIRST
self.exit_on_click = False
self.exit_distance = 150 * dpi_factor(min=.5)
self.dyno_exit = False
if context.mode == 'SCULPT':
self.dyno_exit = True if bpy.context.sculpt_object.use_dynamic_topology_sculpting else False
self.centering = False
# Locks
self.move_locked = False
self.move_lock_index = None
self.scale_locked = False
self.scale_locked_index = None
self.scale_locked_mouse_start = Vector((0,0))
# Props
self.target_obj = context.active_object
self.obj = None
self.bm = None
self.cast_location = Vector()
self.subd_mod = None
self.mirror_mod = None
self.separate = False
# Widgets
self.boarder = Boarder(context)
self.widget = Widget()
self.can_widget_center = True
self.can_widget_mirror = True
# Drawing
built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '3D_UNIFORM_COLOR'
self.shader_3d = gpu.shader.from_builtin(built_in_shader)
self.point_shader = point_uniform.compile()
self.graphics_setup()
self.closest_point = None
# Setup
self.create_mesh(context)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = self.obj
bpy.ops.object.mode_set(mode='EDIT')
# Systems
self.master = Master(context=context)
self.master.only_use_fast_ui = True
self.base_controls = Base_Modal_Controls(context, event)
self.original_tool_shelf, self.original_n_panel = collapse_3D_view_panels(force=True)
self.draw_handle_2D = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_2D, (context,), 'WINDOW', 'POST_PIXEL')
self.draw_handle_3D = bpy.types.SpaceView3D.draw_handler_add(self.safe_draw_3D, (context,), 'WINDOW', 'POST_VIEW')
context.window_manager.modal_handler_add(self)
return {"RUNNING_MODAL"}
def modal(self, context, event):
# --- Systems --- #
self.master.receive_event(event=event)
self.base_controls.update(context, event)
#--- Widgets ---#
self.update_widget(context, event)
# --- Controls --- #
if self.base_controls.pass_through:
return {'PASS_THROUGH'}
elif event.type in {'WHEELUPMOUSE', 'WHEELDOWNMOUSE'} or event.type in numpad_types:
return {'PASS_THROUGH'}
elif self.base_controls.cancel:
return self.cancel_exit(context)
elif event.type in {'SPACE', 'RET', 'NUMPAD_ENTER'}:
return self.confirm_exit(context)
elif event.type == 'S' and event.value == 'PRESS':
if self.state == State.ADJUST:
if self.subd_mod.levels == 0:
self.subd_mod.levels = 1
elif self.subd_mod.levels == 1:
self.subd_mod.levels = 2
elif self.subd_mod.levels == 2:
self.subd_mod.levels = 0
elif event.type in {'X', 'ONE'} and event.value == 'PRESS':
self.cycle_mirror()
elif event.type == 'C' and event.value == 'PRESS':
self.centering = not self.centering
elif event.type == 'P' and event.value == 'PRESS':
self.separate = not self.separate
self.bm = bmesh.from_edit_mesh(self.obj.data)
# Exit on Click
self.exit_on_click = self.mouse_out_of_bounds(event, context)
if self.exit_on_click:
if self.base_controls.confirm:
return self.confirm_exit(context)
# FIRST
if self.state == State.FIRST:
self.set_cast_location(context, event)
if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
self.state = State.SECOND
self.bm.verts.new(self.cast_location)
# SECOND
elif self.state == State.SECOND:
self.set_cast_location(context, event)
self.second_point = self.cast_location
if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
self.state = State.ADJUST
self.bm.verts.ensure_lookup_table()
v1 = self.bm.verts[0]
v2 = self.bm.verts.new(self.cast_location)
self.bm.edges.new((v1, v2))
self.obj.modifiers.new('Skin', 'SKIN')
self.subd_mod = self.obj.modifiers.new('Subsurf', 'SUBSURF')
self.subd_mod.levels = 1
self.mirror_mod = self.obj.modifiers.new('Mirror', 'MIRROR')
self.mirror_mod.use_axis[0] = False
self.mirror_mod.use_bisect_axis[0] = False
self.mirror_mod.use_bisect_flip_axis[0] = False
self.mirror_mod.mirror_object = self.target_obj
# ADJUST
elif self.state == State.ADJUST:
index = self.vert_index_under_mouse(context, event)
if index != None:
self.closest_point = self.obj.matrix_world @ self.bm.verts[index].co
self.adjust_controls(context, event)
bmesh.update_edit_mesh(self.obj.data)
self.interface(context)
context.area.tag_redraw()
return {"RUNNING_MODAL"}
def interface(self, context):
self.master.setup()
if not self.master.should_build_fast_ui(): return
win_list = []
w_append = win_list.append
if self.state == State.FIRST:
w_append('Adding')
elif self.state == State.SECOND:
w_append('Adding')
elif self.state == State.ADJUST:
w_append('Adjusting')
w_append(f'SubD : {self.subd_mod.levels}')
w_append(f'Mirror : {self.mirror_mod.use_axis[0]}')
w_append('Separate' if self.separate else 'Join')
# Help
help_items = {"GLOBAL" : [], "STANDARD" : []}
help_items["GLOBAL"] = [
("M", "Toggle mods list"),
("H", "Toggle help"),
("~", "Toggle UI Display Type"),
("O", "Toggle viewport rendering")]
help_items["STANDARD"] = []
h_append = help_items["STANDARD"].append
if self.state == State.FIRST:
h_append(('Click', 'Add First Point'))
elif self.state == State.SECOND:
h_append(('Click', 'Add Second Point'))
elif self.state == State.ADJUST:
h_append(('Drag', 'Move Points'))
h_append(('Shift Drag', 'Scale Points'))
h_append(('Ctrl Drag', 'Extrude Points'))
h_append(('S', 'Subdivide Levels'))
h_append(('X', 'Mirror X'))
h_append(('C', f'Center {self.centering}'))
h_append(('P', f'Separate' if not self.separate else 'Join'))
help_items["STANDARD"].reverse()
self.master.receive_fast_ui(win_list=win_list, help_list=help_items, image="Array")
self.master.finished()
#--- UTILS ---#
def create_mesh(self, context):
mesh = bpy.data.meshes.new("HopsMesh")
self.obj = bpy.data.objects.new("HopsObj", mesh)
context.collection.objects.link(self.obj)
def mouse_out_of_bounds(self, event, context):
if self.state != State.ADJUST: return False
points = []
for vert in self.bm.verts:
point = get_2d_point_from_3d_point(context, self.obj.matrix_world @ vert.co)
if not point: continue
points.append(point)
if not points: return
mouse = Vector((event.mouse_region_x, event.mouse_region_y))
validations = []
for point in points:
mag = (point - mouse).magnitude
validations.append(mag < self.exit_distance)
return not any(validations)
def set_cast_location(self, context, event):
hit, location, normal, index, object, matrix = scene_ray_cast(context, event)
if hit:
self.cast_location = location
return
self.cast_location = self.plane_cast(context, event, self.cast_location)
def plane_cast(self, context, event, plane_co):
if self.centering:
normal = self.target_obj.matrix_world.decompose()[1] @ Vector((1,0,0))
plane_co = self.target_obj.matrix_world.translation
mouse_pos = (event.mouse_region_x, event.mouse_region_y)
return get_3D_point_from_mouse(mouse_pos, context, plane_co, normal)
view_quat = context.region_data.view_rotation
up = Vector((0,0,1))
view_normal = view_quat @ up
mouse_pos = (event.mouse_region_x, event.mouse_region_y)
return get_3D_point_from_mouse(mouse_pos, context, plane_co, view_normal)
def adjust_controls(self, context, event):
# Stop Locks
if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
self.close_locks()
if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
# Scale
if event.shift:
index = self.vert_index_under_mouse(context, event)
if index == None: return
self.scale_locked_index = index
self.scale_locked = True
self.scale_locked_mouse_start = Vector((event.mouse_region_x, event.mouse_region_y))
# Extrude
if event.ctrl:
index = self.vert_index_under_mouse(context, event)
if index == None: return
self.bm.verts.ensure_lookup_table()
v1 = self.bm.verts[index]
self.cast_location = self.obj.matrix_world @ v1.co
v2 = self.bm.verts.new(self.cast_location)
self.bm.edges.new((v1, v2))
self.move_locked = True
self.bm.verts.ensure_lookup_table()
self.move_lock_index = v2.index
# Move
else:
index = self.vert_index_under_mouse(context, event)
if index == None: return
self.move_lock_index = index
self.move_locked = True
# SCALE
if self.scale_locked:
self.scale(context, event)
return
# MOVE
if self.move_locked:
self.move(context, event)
return
def scale(self, context, event):
if self.scale_locked_index == None:
self.close_locks()
return
vert = self.bm.verts[self.scale_locked_index]
mouse = Vector((event.mouse_region_x, event.mouse_region_y))
scale = (self.scale_locked_mouse_start - mouse).magnitude * .01 * dpi_factor(min=.5)
if scale < .01:
scale = .01
layer = self.bm.verts.layers.skin.verify()
data = vert[layer]
data.radius = (scale, scale)
def move(self, context, event):
if self.move_lock_index == None:
self.close_locks()
return
vert = self.bm.verts[self.move_lock_index]
location = self.plane_cast(context, event, self.obj.matrix_world @ vert.co)
vert.co = location
def vert_index_under_mouse(self, context, event):
self.bm.verts.ensure_lookup_table()
vert_map = {}
for vert in self.bm.verts:
point = get_2d_point_from_3d_point(context, self.obj.matrix_world @ vert.co)
if not point: continue
vert_map[vert.index] = point
if not vert_map: return None
mouse = Vector((event.mouse_region_x, event.mouse_region_y))
compare = None
ret_index = None
for index, point in vert_map.items():
mag = (point - mouse).magnitude
if compare == None or mag < compare:
compare = mag
ret_index = index
return ret_index
def close_locks(self):
self.move_locked = False
self.move_lock_index = None
self.scale_locked = False
self.scale_locked_index = None
def cycle_mirror(self):
if not self.mirror_mod: return
# Right Side
if not self.mirror_mod.use_axis[0]:
self.mirror_mod.use_axis[0] = True
self.mirror_mod.use_bisect_axis[0] = True
self.mirror_mod.use_bisect_flip_axis[0] = False
bpy.ops.hops.display_notification(info="Right Side Mirror")
# Left Side
elif self.mirror_mod.use_axis[0] and not self.mirror_mod.use_bisect_flip_axis[0]:
self.mirror_mod.use_axis[0] = True
self.mirror_mod.use_bisect_axis[0] = True
self.mirror_mod.use_bisect_flip_axis[0] = True
bpy.ops.hops.display_notification(info="Left Side Mirror")
# Off
elif self.mirror_mod.use_axis[0] and self.mirror_mod.use_bisect_flip_axis[0]:
self.mirror_mod.use_axis[0] = False
self.mirror_mod.use_bisect_axis[0] = False
self.mirror_mod.use_bisect_flip_axis[0] = False
bpy.ops.hops.display_notification(info="Mirror Off")
def graphics_setup(self):
self.center_verts = []
self.center_indices_lines = []
self.center_batch_lines = None
self.center_indices_tris = []
self.center_batch_tris = None
rotation = self.target_obj.matrix_world.decompose()[1]
radius = max(self.target_obj.dimensions[:])
# Verts
segments = 64
for i in range(segments):
index = i + 1
angle = i * 6.28318 / segments
y = cos(angle) * radius
z = sin(angle) * radius
vert = Vector((0, y, z))
vert = rotation @ vert
vert += self.target_obj.matrix_world.translation
self.center_verts.append(vert)
# Line indices
if(index == segments): self.center_indices_lines.append((i, 0))
else: self.center_indices_lines.append((i, i + 1))
# Line batch
self.center_batch_lines = batch_for_shader(self.shader_3d, 'LINES', {'pos': self.center_verts}, indices=self.center_indices_lines)
# Tri indices
for i in range(len(self.center_verts) - 1):
self.center_indices_tris.append((0, i, i + 1))
# Tri batch
self.center_batch_tris = batch_for_shader(self.shader_3d, 'TRIS', {'pos': self.center_verts}, indices=self.center_indices_tris)
#--- WIDGET ---#
def update_widget(self, context, event):
if self.state != State.ADJUST:
return
mouse_pos = Vector((event.mouse_region_x, event.mouse_region_y))
if self.can_widget_center == False:
if (self.widget.center_loc - mouse_pos).magnitude > 90 * dpi_factor():
self.can_widget_center = True
if self.can_widget_mirror == False:
if (self.widget.mirror_loc - mouse_pos).magnitude > 90 * dpi_factor():
self.can_widget_mirror = True
self.widget.update(context, event)
if self.widget.center_active:
if self.can_widget_center:
self.can_widget_center = False
self.centering = not self.centering
if self.widget.mirror_active:
if self.can_widget_mirror:
self.can_widget_mirror = False
self.cycle_mirror()
#--- EXITS ---#
def common_exit(self, context):
if self.og_mode == 'SCULPT':
if context.mode != 'SCULPT':
bpy.ops.object.mode_set(mode='SCULPT')
if self.dyno_exit:
bpy.ops.sculpt.dynamic_topology_toggle()
self.remove_shaders()
collapse_3D_view_panels(self.original_tool_shelf, self.original_n_panel, force=True)
self.master.run_fade()
def confirm_exit(self, context):
# Update Mesh
bmesh.update_edit_mesh(self.obj.data)
# Object Mode / Flush
bpy.ops.object.mode_set(mode='OBJECT')
if self.og_mode != 'OBJECT':
bpy.ops.object.select_all(action='DESELECT')
# Apply Mods
context.view_layer.objects.active = self.obj
self.obj.select_set(True)
bpy.ops.object.convert(target='MESH')
if self.separate:
self.target_obj.select_set(False)
self.obj.select_set(True)
context.view_layer.objects.active = self.obj
else:
# Join
self.target_obj.select_set(True)
context.view_layer.objects.active = self.target_obj
self.obj.select_set(True)
bpy.ops.object.join()
else:
self.obj.select_set(True)
context.view_layer.objects.active = self.obj
self.common_exit(context)
return {'FINISHED'}
def cancel_exit(self, context):
bmesh.update_edit_mesh(self.obj.data)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = self.target_obj
self.target_obj.select_set(True)
self.common_exit(context)
if self.obj:
mesh = self.obj.data
bpy.data.objects.remove(self.obj)
bpy.data.meshes.remove(mesh)
return {'CANCELLED'}
#--- SHADERS ---#
def remove_shaders(self):
if self.draw_handle_2D:
self.draw_handle_2D = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle_2D, "WINDOW")
if self.draw_handle_3D:
self.draw_handle_3D = bpy.types.SpaceView3D.draw_handler_remove(self.draw_handle_3D, "WINDOW")
def safe_draw_2D(self, context):
method_handler(self.draw_shader_2D,
arguments = (context,),
identifier = 'Modal Shader 2D',
exit_method = self.remove_shaders)
def draw_shader_2D(self, context):
if self.exit_on_click:
self.boarder.draw()
if self.state == State.ADJUST:
self.widget.draw()
def safe_draw_3D(self, context):
method_handler(self.draw_shader_3D,
arguments = (context,),
identifier = 'Modal Shader 3D',
exit_method = self.remove_shaders)
def draw_shader_3D(self, context):
self.shader_3d.bind()
if self.centering:
#Enable(GL_LINE_SMOOTH)
gpu.state.blend_set('ALPHA')
gpu.state.depth_test_set('LESS')
#glDepthFunc(GL_LESS)
gpu.state.line_width_set(3)
self.shader_3d.uniform_float('color', (1,1,1,.125))
self.center_batch_lines.draw(self.shader_3d)
self.shader_3d.uniform_float('color', (1,1,1,.03125))
self.center_batch_tris.draw(self.shader_3d)
gpu.state.depth_test_set('NONE')
gpu.state.face_culling_set('NONE')
gpu.state.depth_test_set('NONE')
self.bm = bmesh.from_edit_mesh(self.obj.data)
if not self.bm.verts: return
points = [self.obj.matrix_world @ v.co for v in self.bm.verts]
batch = batch_for_shader(self.point_shader, 'POINTS', {'pos': points})
self.point_shader.uniform_float('color', (1,0,0,1))
self.point_shader.uniform_float('size', 8)
gpu.state.blend_set('ALPHA')
gpu.state.program_point_size_set(True)
batch.draw(self.shader_3d)
del batch
if self.closest_point:
batch = batch_for_shader(self.point_shader, 'POINTS', {'pos': [self.closest_point]})
self.point_shader.uniform_float('color', (1,1,0,1))
self.point_shader.uniform_float('size', 12)
gpu.state.blend_set('ALPHA')
batch.draw(self.shader_3d)
del batch
gpu.state.program_point_size_set(False)