import bpy
import gpu
import math
import bmesh
import mathutils
from mathutils import Vector
from math import cos, sin, pi, radians, degrees
from gpu_extras.batch import batch_for_shader
from bpy.props import IntProperty, FloatProperty, BoolProperty, StringProperty
from mathutils import Matrix, Vector, geometry, Quaternion
from ... graphics.drawing2d import draw_text
from ... utility import method_handler
from ... utility import addon
from ... ui_framework.master import Master
from ... utils.space_3d import get_3D_point_from_mouse
from ... utils.bmesh import get_edges_center, is_an_edge_selected, get_face_normal_from_vert
from ... utils.curve import copy_curve, convert_curve_to_mesh, add_curve_to_scene, add_bezier_point
class HOPS_OT_Curve_Draw(bpy.types.Operator):
"""Curve Draw - ST3"""
bl_idname = "hops.curve_draw"
bl_label = "Draw a curve"
bl_options = {"REGISTER", "UNDO"}
bl_description = """Draw a curve"""
adaptive_segments: IntProperty(name="Adaptive Segments", default=15, min=2, max=100)
stop_location: IntProperty(name="Stop Location", default=100, min=0, max=100)
smooth_repeat: IntProperty(name="Smooth Repeat", default=0, min=0, max=50)
smoothing_factor: FloatProperty(name="Smooth Factor", default=.5, min=0, max=1)
# @classmethod
# def poll(cls, context):
# mesh, junk_obj = False, False
# mesh_obj = None
# for obj in context.selected_editable_objects:
# if obj.type == "MESH":
# mesh_obj = obj
# mesh = True
# else:
# junk_obj = True
# if mesh and not junk_obj:
# return True
# return False
def invoke(self, context, event):
start_state = context.active_object.mode
if start_state == 'OBJECT':
bpy.ops.object.mode_set(mode='EDIT')
# cancel_due_to_bad_selection = False
# if is_an_edge_selected(context, context.active_object) == False:
# cancel_due_to_bad_selection = True
# if cancel_due_to_bad_selection:
# if start_state == 'OBJECT':
# bpy.ops.object.mode_set(mode='OBJECT')
# return {'CANCELLED'}
# Props
self.obj = context.active_object
self.intersection = Vector((0,0,0))
self.point = Vector((0,0,0))
self.normal = Vector((0,0,0))
mesh = self.obj.data
bm = bmesh.from_edit_mesh(mesh)
loc = get_face_normal_from_vert(bm=bm)
self.curve = add_curve_to_scene(context, location=loc, select=False)
self.master = Master(context=context, show_fast_ui=False)
self.intersection_draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_intersection, (context,), 'WINDOW', 'POST_VIEW')
self.curve_draw_handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_curve, (context,), 'WINDOW', 'POST_VIEW')
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def execute(self, context):
print("TOP")
print(context.selected_editable_objects)
start_state = context.active_object.mode
if start_state == 'OBJECT':
bpy.ops.object.mode_set(mode='EDIT')
cancel_due_to_bad_selection = False
if is_an_edge_selected(context, context.active_object) == False:
cancel_due_to_bad_selection = True
if start_state == 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
if cancel_due_to_bad_selection:
return {'CANCELLED'}
# Get Mesh and Curve
self.mesh, self.curve = None, None
for obj in context.selected_editable_objects:
if obj.type == "MESH":
if context.active_object.name == obj.name:
self.mesh = obj
if obj.type == "CURVE":
self.curve = obj
if self.mesh == None or self.curve == None:
return {'CANCELLED'}
#self.extrude_along_curve(context=context)
bpy.ops.object.mode_set(mode='OBJECT')
print("BOTTOM")
return {"FINISHED"}
def modal(self, context, event):
# Fade effect
self.master.receive_event(event=event)
#Navigation
if (event.type in {'MIDDLEMOUSE', 'WHEELUPMOUSE', 'WHEELDOWNMOUSE'} and event.ctrl != True):
if not self.master.is_mouse_over_ui():
return {'PASS_THROUGH'}
#Confirm
elif (event.type in {'SPACE', 'NUMPAD_ENTER', 'RET'}):
if not self.master.is_mouse_over_ui():
self.remove_intersection()
self.remove_curve()
self.master.run_fade()
self.curve.select_set(True)
self.obj.select_set(True)
#bpy.ops.hops.curve_extrude('EXEC_DEFAULT')
self.execute(context)
return {'FINISHED'}
#Cancel
elif (event.type in {'RIGHTMOUSE', 'ESC'}):
if not self.master.is_mouse_over_ui():
self.remove_intersection()
self.remove_curve()
try:
self.cancel(context)
except:
print("Error: Could not revert.")
self.master.run_fade()
return {'CANCELLED'}
# Raycasting
#elif event.type == 'MOUSEMOVE':
if not self.master.is_mouse_over_ui():
# Mouse Pos
mouse_x = event.mouse_region_x
mouse_y = event.mouse_region_y
mouse_pos = (mouse_x, mouse_y)
# Plane Point
self.obj = context.object
mesh = self.obj.data
bm = bmesh.from_edit_mesh(mesh)
self.point = get_edges_center(bm)
self.point = self.point @ self.obj.matrix_world
quat_mat = Quaternion((0, 1, 0), math.radians(90.0)).to_matrix()
self.normal = get_face_normal_from_vert(bm=bm)
self.normal = self.normal @ quat_mat
self.intersection = get_3D_point_from_mouse(
mouse_pos=mouse_pos,
context=context,
point=self.point,
normal=self.normal)
if event.type == 'LEFTMOUSE' and event.value == "PRESS":
add_bezier_point(curve=self.curve, location=self.intersection)
self.draw_ui(context=context)
context.area.tag_redraw()
return {'RUNNING_MODAL'}
def cancel(self, context):
pass
def draw_ui(self, context):
# Start
self.master.setup()
########################
# Fast UI
########################
if not self.master.should_build_fast_ui():
########################
# Main
########################
main_window = {
"main_count" : [],
"header_sub_text" : [],
"last_col_cell_1" : [],
"last_col_cell_2" : [],
"last_col_cell_3" : []
}
window_name = ""
main_window["main_count"] = ["TEST"]
main_window["header_sub_text"] = ["TEST"]
main_window["last_col_cell_1"] = ["TEST"]
main_window["last_col_cell_2"] = ["TEST"]
main_window["last_col_cell_3"] = ["TEST"]
self.master.receive_main(win_dict=main_window, window_name=window_name)
########################
# Help
########################
hot_keys_dict = {}
quick_ops_dict = {}
hot_keys_dict["V, B, N"] = "Change the mode."
self.master.receive_help(hot_keys_dict=hot_keys_dict, quick_ops_dict={})
########################
# Mods
########################
win_dict = {}
for mod in reversed(context.active_object.modifiers):
win_dict[mod.name] = str(mod.type)
self.master.receive_mod(win_dict=win_dict, active_mod_name="")
# Finished
self.master.finished()
def draw_intersection(self, context):
method_handler(self._draw_intersection,
arguments = (context, ),
identifier = 'Curve Draw Intersection Shader',
exit_method = self.remove_intersection)
def remove_intersection(self):
if self.intersection_draw_handle:
self.intersection_draw_handle = bpy.types.SpaceView3D.draw_handler_remove(self.intersection_draw_handle, "WINDOW")
def draw_curve(self, context):
method_handler(self._draw_curve,
arguments = (context, ),
identifier = 'Curve Draw Shader',
exit_method = self.remove_curve)
def remove_curve(self):
if self.curve_draw_handle:
self.curve_draw_handle = bpy.types.SpaceView3D.draw_handler_remove(self.curve_draw_handle, "WINDOW")
def _draw_curve(self, context):
pass
def _draw_intersection(self, context):
radius = .25
vertices = []
indices = []
segments = 32
width = 2
color = (0,0,0,1)
#Build ring
for i in range(segments):
index = i + 1
angle = i * 3.14159 * 2 / segments
x = cos(angle) * radius
y = sin(angle) * radius
z = 0
vert = Vector((x, y, z))
rot_mat = self.normal.rotation_difference(Vector((0,0,1))).to_matrix()
vert = vert @ rot_mat
vert[0] = vert[0] + self.intersection[0]
vert[1] = vert[1] + self.intersection[1]
vert[2] = vert[2] + self.intersection[2]
vertices.append(vert)
if(index == segments):
indices.append((i, 0))
else:
indices.append((i, i + 1))
# Build Grid
grid_segments = 3
for i in range(grid_segments):
index = i + 1
angle = i * 3.14159 * 2 / grid_segments
x = cos(angle) * 5
y = sin(angle) * 5
z = 0
vert = Vector((x, y, z))
rot_mat = self.normal.rotation_difference(Vector((0,0,1))).to_matrix()
vert = vert @ rot_mat
vert[0] = vert[0] + self.point[0]
vert[1] = vert[1] + self.point[1]
vert[2] = vert[2] + self.point[2]
vertices.append(vert)
if(index == grid_segments):
indices.append((i + segments, segments))
else:
indices.append((i + segments, i + segments + 1))
color = (1,0,1,1)
built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >=4 else '3D_UNIFORM_COLOR'
shader = gpu.shader.from_builtin(built_in_shader)
batch = batch_for_shader(shader, 'LINES', {'pos': vertices}, indices=indices)
shader.bind()
shader.uniform_float('color', color)
#Enable(GL_LINE_SMOOTH)
gpu.state.blend_set('ALPHA')
gpu.state.line_width_set(width)
batch.draw(shader)
del shader
del batch
def extrude_along_curve(self, context):
# Copy original curve
original_segments = self.curve.data.resolution_u
mesh_curve = copy_curve(context, self.curve)
# Set curve segs
mesh_curve.data.resolution_u = self.adaptive_segments
# Convert curve to mesh
m_curve = convert_curve_to_mesh(context, mesh_curve, self.smooth_repeat, self.smoothing_factor)
self.curve.data.resolution_u = original_segments
# Get the points along the mesh curve
points = get_points_from_mesh_curve(context, self.curve, m_curve, context.object)
# Extrude
extrude(context, points, self.stop_location, self.adaptive_segments)
# Remove the mesh curve
bpy.data.objects.remove(mesh_curve, do_unlink=True)
self.curve.select_set(True)
self.obj.select_set(True)
def extrude(context, points, stop_location, adaptive_segments):
obj = context.object
mesh = obj.data
bm = bmesh.from_edit_mesh(mesh)
stop_location = int(len(points) * (stop_location * .01))
index = 0
for point in points:
if stop_location == index:
break
# Extrude edges
if bm.faces.active == None:
edge_extrude(bm, point, points, obj, index)
# Extrude faces
else:
face_extrude(bm, point, points, obj, index)
index += 1
bmesh.update_edit_mesh(mesh)
def face_extrude(bm, point, points, obj, index):
bm.normal_update()
active_face = bm.faces.active
face_normal = active_face.normal
face_normal = obj.matrix_world @ face_normal
face_center_loc = get_edges_center(bm)
face_center_loc = obj.matrix_world @ face_center_loc
face_to_point_vec = point - face_center_loc
if index == 0:
if int(face_to_point_vec.magnitude) == 0:
return
if face_center_loc == point:
return
# Extrude faces
faces = [f for f in bm.faces if f.select]
ret_geom = bmesh.ops.extrude_face_region(bm, geom=faces)
for face in faces:
face.select = False
for face in ret_geom["geom"]:
if isinstance(face, bmesh.types.BMFace):
face.select = True
# Flip extruded geo
bmesh.ops.reverse_faces(bm, faces=faces)
# Offset vector
offset_vec = point - face_to_point_vec
# Translate
verts = [v for v in ret_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
ret_geom = bmesh.ops.translate(bm, vec=face_to_point_vec, space=obj.matrix_world, verts=verts)
# Rotate
if index + 1 < len(points):
rot = face_normal.rotation_difference(points[index + 1] - point).to_matrix()
bmesh.ops.rotate(bm, cent=point, matrix=rot, verts=verts, space=obj.matrix_world)
# Delete
bmesh.ops.delete(bm, geom=faces, context='FACES_KEEP_BOUNDARY')
def edge_extrude(bm, point, points, obj, index):
edge_center_loc = get_edges_center(bm)
edge_center_loc = obj.matrix_world @ edge_center_loc
bm.normal_update()
edge_center_to_point = point - edge_center_loc
if index == 0:
if int(edge_center_to_point.magnitude) == 0:
return
if edge_center_loc == point:
return
# Extrude edges
edges = [e for e in bm.edges if e.select]
ret_geom = bmesh.ops.extrude_edge_only(bm, edges=edges, use_normal_flip=True)
# Recalc the extruded normals
faces = []
for face in ret_geom["geom"]:
if isinstance(face, bmesh.types.BMFace):
faces.append(face)
bmesh.ops.recalc_face_normals(bm, faces=faces)
for edge in edges:
edge.select = False
for edge in ret_geom["geom"]:
if isinstance(edge, bmesh.types.BMEdge):
edge.select = True
# Offset vector
offset_vec = point - edge_center_to_point
# Translate
verts = [v for v in ret_geom["geom"] if isinstance(v, bmesh.types.BMVert)]
ret_geom = bmesh.ops.translate(bm, vec=edge_center_to_point, space=obj.matrix_world, verts=verts)
# Rotate
if index + 1 < len(points):
rot = edge_center_to_point.rotation_difference(points[index + 1] - point).to_matrix()
bmesh.ops.rotate(bm, cent=point, matrix=rot, verts=verts, space=obj.matrix_world)
def get_points_from_mesh_curve(context, original_curve, mesh_curve, obj):
'''Get the points from the curve mesh, this also makes sure to send back the correct order.'''
# Get the face center location
bm = bmesh.from_edit_mesh(obj.data)
face = bm.faces.active
face_center_loc = Vector((0,0,0))
# If there is no active face
if face == None:
face_center_loc = get_edges_center(bm)
face_center_loc = obj.matrix_world @ face_center_loc
else:
face_center_loc = face.calc_center_median()
face_center_loc = obj.matrix_world @ face_center_loc
points = []
verts = mesh_curve.data.vertices
# Generate the points with the tail end closer to the face center
first_coord = original_curve.matrix_world @ verts[0].co
last_coord = original_curve.matrix_world @ verts[-1].co
reverse = False if (first_coord - face_center_loc).magnitude < (last_coord - face_center_loc).magnitude else True
if reverse:
for vert in reversed(verts):
points.append(original_curve.matrix_world @ vert.co)
else:
for vert in verts:
points.append(original_curve.matrix_world @ vert.co)
return points