import bpy, mathutils, math, gpu, bmesh
from math import cos, sin, radians, degrees
from mathutils import Vector, Matrix, Quaternion, Euler, geometry
from gpu_extras.batch import batch_for_shader
from bpy_extras import view3d_utils
from .. utility import addon
from ..utility.screen import dpi_factor
from .. utility import math as hops_math
from . space_3d import get_3D_point_from_mouse, get_2d_point_from_3d_point, scene_ray_cast
from .. utility.shader import point_uniform
class Grid_Data:
def __init__(self, key, active, shader_type):
self.key = key
self.active = active
self.shader_grid_points = []
self.shader_border_points = []
self.all_points = []
self.u = 10 # Along X (2d)
self.v = 10 # Along Y (2d)
self.size_x = 2
self.size_y = 2
self.mat = Matrix()
self.boxelize = False
self.color = addon.preference().color.grid_system_color
self.shader = gpu.shader.from_builtin(shader_type)
self.grid_batch = None
self.border_batch = None
class Grid_3D(Grid_Data):
def __init__(self, key, active):
built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >= 4 else '3D_UNIFORM_COLOR'
super().__init__(key, active, built_in_shader)
def locate_grid(self, context, event, method='raycast'):
if method == 'raycast':
hit, location, normal, index, object, matrix = scene_ray_cast(context, event)
if not hit: return False
self.mat = surface_matrix(object, matrix, location, normal, location)
self._setup_batch()
return True
return False
def alter_grid(self, u=None, v=None, loc=None, rot=None, size_x=None, size_y=None):
if u: self.u = u if u > 2 else 2
if v: self.v = v if v > 2 else 2
if loc and not rot:
rot = self.mat.decompose()[1]
self.mat = loc_rot_mat_3d(loc, rot)
elif not loc and rot:
loc = self.mat.decompose()[0]
self.mat = loc_rot_mat_3d(loc, rot)
if rot and loc:
self.mat = loc_rot_mat_3d(loc, rot)
if size_x: self.size_x = size_x
if size_y: self.size_y = size_y
self._setup_batch()
def grid_point(self, context, event):
point = self.mat @ Vector()
rot = self.mat.decompose()[1]
normal = rot @ Vector((0,0,1))
point = get_3D_point_from_mouse(mouse_pos(event), context, point, normal)
return compare_point_to_points(point, self.all_points)
def _update(self, context, event):
pass
def _setup_batch(self):
self._set_shader_points()
self._set_grid_points()
# Transform Grid
indices = [(i, i + 1) for i in range(0, len(self.shader_grid_points), 2)]
self.grid_batch = batch_for_shader(self.shader, 'LINES', {"pos": self.shader_grid_points}, indices=indices)
# Border
self.shader_border_points = [
Vector((0, 0, 0)),
Vector((0, self.size_y, 0)),
Vector((self.size_x, self.size_y, 0)),
Vector((self.size_x, 0, 0))]
for i in range(4):
# Center
self.shader_border_points[i] -= Vector((self.size_x * .5, self.size_y * .5, 0))
# Transform
self.shader_border_points[i] = self.mat @ self.shader_border_points[i]
indices = [[i, (i + 1) % 4] for i in range(4)]
self.border_batch = batch_for_shader(self.shader, 'LINES', {"pos": self.shader_border_points}, indices=indices)
def _set_grid_points(self):
square_width_x = self.size_x / self.u
square_width_y = self.size_y / self.v
offset = Vector((self.size_x * .5, self.size_y * .5, 0))
self.all_points = []
append = self.all_points.append
for i in range(self.u + 1):
for j in range(self.v + 1):
append( self.mat @ (Vector(( square_width_x * i, square_width_y * j, 0)) - offset ))
def _set_shader_points(self):
# Generate Flat Grid
square_width_x = self.size_x / self.u
square_width_y = self.size_y / self.v
self.shader_grid_points = []
append = self.shader_grid_points.append
for i in range(1, self.u, 1):
append( Vector((square_width_x * i, 0 , 0)) )
append( Vector((square_width_x * i, self.size_y, 0)) )
for i in range(1, self.v, 1):
if i == 0: continue
append( Vector((0 , square_width_y * i, 0)) )
append( Vector((self.size_x, square_width_y * i, 0)) )
for i in range(len(self.shader_grid_points)):
# Center
self.shader_grid_points[i] -= Vector((self.size_x * .5, self.size_y * .5, 0))
# Transform
self.shader_grid_points[i] = self.mat @ self.shader_grid_points[i]
def _draw_2d(self, context):
pass
def _draw_3d(self, context):
if not self.grid_batch: return
#Enable(GL_LINE_SMOOTH)
gpu.state.blend_set('ALPHA')
self.shader.bind()
gpu.state.line_width_set(2)
self.shader.uniform_float("color", self.color)
self.grid_batch.draw(self.shader)
gpu.state.line_width_set(4)
self.shader.uniform_float("color", self.color)
self.border_batch.draw(self.shader)
#Disable(GL_LINE_SMOOTH)
gpu.state.blend_set('NONE')
class Grid_2D(Grid_Data):
def __init__(self, key, active):
built_in_shader = 'UNIFORM_COLOR' if bpy.app.version[0] >= 4 else '2D_UNIFORM_COLOR'
super().__init__(key, active, built_in_shader)
self.size_x = 100
self.size_y = 100
self.boxelize = True
self.debug_points = []
def locate_grid(self, context, event, method='mouse_position', obj=None):
if method == 'mouse_position':
loc = mouse_pos(event).to_3d()
self.mat = Matrix.Translation(loc)
self._setup_batch()
elif method == 'object_bounds' or 'selected_verts':
if not obj: return False
points = self.selection_bounding_points(context, obj, method)
# Rotate
center = self.points_center(points)
rot = self.mat.decompose()[1]
size = hops_math.dimensions_2d(points)
bbp = [
Vector((center.x - size.x * .5, center.y - size.y * .5)),
Vector((center.x - size.x * .5, center.y + size.y * .5)),
Vector((center.x + size.x * .5, center.y + size.y * .5)),
Vector((center.x + size.x * .5, center.y - size.y * .5))
]
rotated_points = []
for i, p in enumerate(bbp[:]):
p = p - center
p = Vector((p.x, p.y, 0))
p = rot @ p
p = Vector((p.x, p.y))
p = p + center
rotated_points.append(p)
# Size
size = hops_math.dimensions_2d(points + rotated_points)
self.size_x = size.x
self.size_y = size.y
loc = hops_math.coords_to_center_2d(points).to_3d()
loc_mat = Matrix.Translation(loc)
rot_mat = rot.to_matrix().to_4x4()
self.mat = loc_mat @ rot_mat
self._setup_batch()
return True
return False
def selection_bounding_points(self, context, obj, method='object_bounds'):
region = context.region
rv3d = context.region_data
if method == 'object_bounds':
bb_points = object_bounds(obj)
elif method == 'selected_verts':
bb_points = selected_verts_bounds(obj)
else:
return []
points = []
append = points.append
for bbp in bb_points:
point = view3d_utils.location_3d_to_region_2d(region, rv3d, bbp)
if point: append(point)
return points
def points_center(self, points):
return hops_math.coords_to_center_2d(points)
def alter_grid(self, u=None, v=None, loc=None, rot=None, size_x=None, size_y=None):
if self.boxelize:
if u: self.u = u if u > 2 else 2
if v: self.v = v if v > 2 else 2
else:
if u: self.u = u if u > 0 else 1
if v: self.v = v if v > 0 else 1
if loc and not rot:
rot = self.mat.decompose()[1]
self.mat = loc_rot_mat_3d(loc, rot)
elif not loc and rot:
loc = self.mat.decompose()[0]
self.mat = loc_rot_mat_3d(loc, rot)
if rot and loc:
self.mat = loc_rot_mat_3d(loc, rot)
if size_x: self.size_x = size_x
if size_y: self.size_y = size_y
self._setup_batch()
def grid_point(self, context, event):
point = mouse_pos(event)
return compare_point_to_points(point, self.all_points)
def to_object_bounds(self, context, obj, method='object_bounds'):
'''Returns 3D grid data for a Gird -> Object Bounds relation.'''
# 3D border points
casted_border = self._border_to_3d(context)
# Get view point location
view_loc = self._view_location(context)
# Closest point to camera
bounding_box = None
if method == 'object_bounds':
bounding_box = object_bounds(obj)
elif method == 'selected_verts':
bounding_box = selected_verts_bounds(obj)
if bounding_box == None: return
closest_bbp = compare_point_to_points(view_loc, bounding_box, closest=True)
farthest_bbp = compare_point_to_points(view_loc, bounding_box, closest=False)
# Distance to object
view_normal = view_direction(context)
# Project each border point onto closest plane
projected_border = []
for point in casted_border:
end_point = point - view_normal
loc = geometry.intersect_line_plane(point, end_point, closest_bbp, view_normal)
round_vector(loc)
if loc: projected_border.append(loc)
# UV lines from project border
u_lines, v_lines = self._create_uv_from_3d_border(projected_border)
# Distance from from obj bounds to back obj bounds
obj_bounds_view_depth = geometry.distance_point_to_plane(farthest_bbp, closest_bbp, view_normal)
return {
'casted_border' : casted_border,
'u_lines' : u_lines,
'v_lines' : v_lines,
'clostest_point' : closest_bbp,
'farthest_point' : farthest_bbp,
'view_normal' : view_normal,
'obj_bounds_view_depth' : obj_bounds_view_depth}
def _create_uv_from_3d_border(self, border_points):
u_lines = []
v_lines = []
BL = border_points[0]
BR = border_points[3]
TL = border_points[1]
TR = border_points[2]
for i in range(self.u + 1):
factor = (1 / self.u) * i
u_lines.append((BL.lerp(BR, factor), TL.lerp(TR, factor)))
for i in range(self.v + 1):
factor = (1 / self.v) * i
v_lines.append((BL.lerp(TL, factor), BR.lerp(TR, factor)))
return u_lines, v_lines
def _view_location(self, context):
rv3d = None
if hasattr(context.region, 'data'):
rv3d = context.region.data
else:
rv3d = context.space_data.region_3d
if not rv3d: return
view = rv3d.view_matrix.inverted()
clip_end = rv3d.perspective_matrix.inverted().decompose()[2][2]
loc = (view.decompose()[1] @ Vector((0,0,clip_end))) * -1
loc += view.decompose()[0]
return loc
def _border_to_3d(self, context):
region = context.region
rv3d = context.region_data
points = []
for point in self.shader_border_points:
loc = view3d_utils.region_2d_to_location_3d(region, rv3d, point, rv3d.view_location)
points.append(loc)
return points
def _update(self, context, event):
pass
def _setup_batch(self):
self._boxelize_dims()
self._set_shader_points()
self._set_grid_points()
# Transform Grid
indices = [(i, i + 1) for i in range(0, len(self.shader_grid_points), 2)]
self.grid_batch = batch_for_shader(self.shader, 'LINES', {"pos": self.shader_grid_points}, indices=indices)
# Border
self.shader_border_points = [
Vector((0, 0, 0)),
Vector((0, self.size_y, 0)),
Vector((self.size_x, self.size_y, 0)),
Vector((self.size_x, 0, 0))]
for i in range(4):
# Center
self.shader_border_points[i] -= Vector((self.size_x * .5, self.size_y * .5, 0))
# Transform
self.shader_border_points[i] = (self.mat @ self.shader_border_points[i]).to_2d()
indices = [[i, (i + 1) % 4] for i in range(4)]
self.border_batch = batch_for_shader(self.shader, 'LINES', {"pos": self.shader_border_points}, indices=indices)
def _boxelize_dims(self):
'''Uses the U for auto divisions'''
if not self.boxelize: return
x_gap = self.size_x / self.u
if round(x_gap, 5) == 0: return
y_splits = self.size_y / x_gap
self.v = int(round(y_splits))
self.v = 1 if self.v < 1 else self.v
y_gap = self.size_y / self.v
if y_gap > x_gap:
self.size_x = y_gap * self.u
else:
self.size_y = x_gap * self.v
def _set_grid_points(self):
square_width_x = self.size_x / self.u
square_width_y = self.size_y / self.v
offset = Vector((self.size_x * .5, self.size_y * .5, 0))
self.all_points = []
append = self.all_points.append
for i in range(self.u + 1):
for j in range(self.v + 1):
append( (self.mat @ (Vector(( square_width_x * i, square_width_y * j, 0)) - offset )).to_2d() )
def _set_shader_points(self):
# Generate Flat Grid
square_width_x = self.size_x / self.u
square_width_y = self.size_y / self.v
self.shader_grid_points = []
append = self.shader_grid_points.append
for i in range(1, self.u, 1):
append( Vector((square_width_x * i, 0 , 0)) )
append( Vector((square_width_x * i, self.size_y, 0)) )
for i in range(1, self.v, 1):
if i == 0: continue
append( Vector((0 , square_width_y * i, 0)) )
append( Vector((self.size_x, square_width_y * i, 0)) )
for i in range(len(self.shader_grid_points)):
# Center
self.shader_grid_points[i] -= Vector((self.size_x * .5, self.size_y * .5, 0))
# Transform
self.shader_grid_points[i] = (self.mat @ self.shader_grid_points[i]).to_2d()
def _draw_2d(self, context):
if not self.grid_batch: return
#Enable(GL_LINE_SMOOTH)
gpu.state.blend_set('ALPHA')
self.shader.bind()
gpu.state.line_width_set(1)
self.shader.uniform_float("color", self.color)
self.grid_batch.draw(self.shader)
gpu.state.line_width_set(2)
self.shader.uniform_float("color", self.color)
self.border_batch.draw(self.shader)
if self.debug_points:
shader = point_uniform.compile()
batch = batch_for_shader(shader, 'POINTS', {'pos': self.debug_points})
shader.bind()
shader.uniform_float('color', (1,0,0,1))
shader.uniform_float('size', 6)
gpu.state.program_point_size_set(True)
batch.draw(shader)
gpu.state.program_point_size_set(False)
del shader
del batch
#Disable(GL_LINE_SMOOTH)
gpu.state.blend_set('NONE')
def _draw_3d(self, context):
pass
class Grid_Controller:
def __init__(self):
self.grids_3d = []
self.grids_2d = []
def create_grid(self, key=0, active=True, mode_type='3D'):
# 3D Grids
if mode_type == '3D':
grid = Grid_3D(key, active)
self.grids_3d.append(grid)
return grid
# 2D Grids
elif mode_type == '2D':
grid = Grid_2D(key, active)
self.grids_2d.append(grid)
return grid
return None
def get_grid(self, key=0, mode_type='3D'):
# 3D Grids
if mode_type == '3D':
for grid in self.grids_3d:
if grid.key == key:
return grid
# 2D Grids
elif mode_type == '2D':
for grid in self.grids_2d:
if grid.key == key:
return grid
return None
def update(self, context, event):
# 3D Grids
for grid in self.grids_3d:
if grid.active:
grid._update(context, event)
# 2D Grids
for grid in self.grids_2d:
if grid.active:
grid._update(context, event)
def draw_2d(self, context):
# 3D Grids
for grid in self.grids_3d:
if grid.active:
grid._draw_2d(context)
# 2D Grids
for grid in self.grids_2d:
if grid.active:
grid._draw_2d(context)
def draw_3d(self, context):
# 3D Grids
for grid in self.grids_3d:
if grid.active:
grid._draw_3d(context)
# 2D Grids
for grid in self.grids_2d:
if grid.active:
grid._draw_3d(context)
# --- UTILS --- #
def surface_normal(point_a, point_b, point_c):
u = point_b - point_a
v = point_c - point_a
normal = Vector((
(u.y * v.z) - (u.z * v.y),
(u.z * v.x) - (u.x * v.z),
(u.x * v.y) - (u.y * v.x)))
round_vector(normal)
normal.normalize()
return normal
def round_vector(vector, precision=4):
vector.x = round(vector.x, 4)
vector.y = round(vector.y, 4)
vector.z = round(vector.z, 4)
def view_direction(context):
return context.region_data.view_rotation @ Vector((0,0,1))
def object_bounds(obj):
return hops_math.coords_to_bounds([obj.matrix_world @ v.co for v in obj.data.vertices])
def selected_verts_bounds(obj):
if obj.mode == 'EDIT':
bm = bmesh.from_edit_mesh(obj.data)
return hops_math.coords_to_bounds([obj.matrix_world @ v.co for v in bm.verts if v.select])
return hops_math.coords_to_bounds([obj.matrix_world @ v.co for v in obj.data.vertices if v.select])
def mesh_object_by_name(context, object_name=''):
for obj in context.view_layer.objects:
if obj.name == object_name:
if obj.type == 'MESH':
return obj
return None
def compare_point_to_points(point=Vector(), points=[], closest=True):
chosen_point = None
compare = None
for p in points:
dist = (point - p).magnitude
# Getting closest point
if closest:
if not chosen_point or not compare or dist < compare:
chosen_point = p
compare = dist
# Getting farthest point
else:
if not chosen_point or not compare or dist > compare:
chosen_point = p
compare = dist
return chosen_point
def loc_rot_mat_3d(loc=(0,0,0), rot=(0,0,0), quat=None):
loc = Matrix.Translation(loc)
if quat:
rot = quat
else:
rot = Euler((radians(rot[0]), radians(rot[1]), radians(rot[2])), 'XYZ')
return loc.to_4x4() @ rot.to_matrix().to_4x4()
def surface_matrix(obj, matrix, location, normal, position, orient_method='LOCAL', face_index=0, edge_index=0, force_edge=False):
track_matrix = get_track_matrix(normal, matrix=matrix)
custom_orient = False
tangent = Vector((0, 0, 0))
active_edges = []
if orient_method == 'EDIT':
bm = bmesh.from_edit_mesh(obj.data)
active_edges = [elem for elem in bm.select_history if isinstance(elem, bmesh.types.BMEdge)]
if active_edges:
custom_orient = True
v1, v2 = active_edges[0].verts
_, rot, _ = matrix.decompose()
rot_matrix = rot.to_matrix().to_4x4()
tangent = (rot_matrix @ (v2.co - v1.co)).normalized()
if orient_method != 'LOCAL' and not active_edges:
custom_orient = True
object_eval = obj.evaluated_get(bpy.context.evaluated_depsgraph_get())
bm = bmesh.new()
bm.from_mesh(object_eval.data)
bm.faces.ensure_lookup_table()
if orient_method == 'TANGENT':
tangent = bm.faces[face_index].calc_tangent_edge()
elif orient_method == 'NEAREST':
bm.edges.ensure_lookup_table()
face = bm.faces[face_index]
distance = 0.0
index = 0
for i, edge in enumerate(face.edges):
edge = [obj.matrix_world @ v.co for v in edge.verts]
current, normalized_distance = geometry.intersect_point_line(location, *edge)
if normalized_distance > 1:
current = edge[1]
elif normalized_distance < 0:
current = edge[0]
length = (current - location).length
if length < distance or not distance:
distance = length
index = i
tangent = (face.edges[index].verts[0].co - face.edges[index].verts[1].co).normalized()
elif orient_method == 'EDGE' or force_edge:
bm.edges.ensure_lookup_table()
edge = [e for e in bm.faces[face_index].edges][edge_index]
tangent = (edge.verts[0].co - edge.verts[1].co).normalized()
if not force_edge:
if len(set([str(Vector((abs(f) for f in face.normal))) for face in edge.link_faces])) == 1 and len(edge.link_faces) > 1:
custom_orient = False
elif orient_method == 'FACE_FIT':
face = bm.faces[face_index]
face_matrix = view3d.track_matrix(normal=face.normal)
face_matrix.translation = face.calc_center_median()
local_verts = [(face_matrix.inverted() @ vert.co).to_2d() for vert in face.verts]
angle = geometry.box_fit_2d(local_verts)
tangent = face_matrix.to_3x3() @ (Matrix.Rotation(-angle, 2) @ Vector((1, 0))).to_3d()
tangent = (obj.matrix_world.to_3x3() @ tangent).normalized()
bm.free()
if custom_orient:
mat = Matrix.Identity(3)
cross = tangent.cross(normal)
mat.col[0] = cross
mat.col[1] = tangent
mat.col[2] = normal
track_quat = mat.to_quaternion()
track_matrix = matrix.inverted() @ track_quat.to_matrix().to_4x4()
track_matrix = matrix @ track_matrix
track_matrix.translation = position
return track_matrix
def get_track_matrix(normal=Vector(), location=Vector(), matrix=Matrix(), up='Z', align='Y'):
track_mat = (matrix.copy().to_3x3().inverted() @ normal).to_track_quat(up, align).to_matrix().to_4x4()
track_mat.translation = location if location != Vector() else matrix.translation
return track_mat
def mouse_pos(event):
return Vector((event.mouse_region_x, event.mouse_region_y))