Skip to content

Commit

Permalink
feat: add snap_align operator
Browse files Browse the repository at this point in the history
  • Loading branch information
tristan-hm authored May 11, 2022
1 parent 1eb7964 commit 7548c66
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 0 deletions.
1 change: 1 addition & 0 deletions interface/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

object_transform_ops = [
("nd.set_origin", 'TRANSFORM_ORIGINS', None, None, False),
("nd.snap_align", 'SNAP_ON', None, None, False),
]

object_properties_ops = [
Expand Down
1 change: 1 addition & 0 deletions interface/utils_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def draw(self, context):
layout.operator("nd.set_lod_suffix", text="High LOD", icon='ANTIALIASED').mode = 'HIGH'
layout.separator()
layout.operator("nd.set_origin", icon='TRANSFORM_ORIGINS')
layout.operator("nd.snap_align", icon='SNAP_ON')
layout.separator()
layout.operator("nd.smooth", icon='MOD_SMOOTH')
layout.operator("nd.seams", icon='UV_DATA')
Expand Down
3 changes: 3 additions & 0 deletions lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from . import objects
from . import overlay
from . import axis
from . import points
from . import viewport
from . import assets
from . import updates
Expand All @@ -27,6 +28,7 @@
objects,
overlay,
axis,
points,
viewport,
assets,
updates,
Expand All @@ -42,3 +44,4 @@ def reload():

overlay.unregister_draw_handler()
axis.unregister_axis_handler()
points.unregister_points_handler()
4 changes: 4 additions & 0 deletions lib/math.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ def v3_elem(label, vector):
return vector[{'X': 0, 'Y': 1, 'Z': 2}[label]]


def v3_distance(a, b):
return (a - b).length


def get_edge_normal(edge):
return v3_sum_normalized([face.normal for face in edge.link_faces])

Expand Down
79 changes: 79 additions & 0 deletions lib/points.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# “Commons Clause” License Condition v1.0
#
# See LICENSE for license details. If you did not receive a copy of the license,
# it may be obtained at https://github.com/hugemenace/nd/blob/main/LICENSE.
#
# Software: ND Blender Addon
# License: MIT
# Licensor: T.S. & I.J. (HugeMenace)

import bpy
import gpu
import bgl
from mathutils import Vector, Matrix
from gpu_extras.batch import batch_for_shader


def register_points_handler(cls):
handler = bpy.app.driver_namespace.get('nd.points')

if not handler:
handler = bpy.types.SpaceView3D.draw_handler_add(update_points, (cls, ), 'WINDOW', 'POST_VIEW')
dns = bpy.app.driver_namespace
dns['nd.points'] = handler

redraw_regions()


def unregister_points_handler():
handler = bpy.app.driver_namespace.get('nd.points')

if handler:
bpy.types.SpaceView3D.draw_handler_remove(handler, 'WINDOW')
del bpy.app.driver_namespace['nd.points']

redraw_regions()


def redraw_regions():
for area in bpy.context.window.screen.areas:
if area.type == 'VIEW_3D':
for region in area.regions:
if region.type == 'WINDOW':
region.tag_redraw()


def init_points(cls):
cls.primary_points = []
cls.secondary_points = []
cls.tertiary_points = []
cls.guide_line = ()


def draw_points(shader, points, size, color):
gpu.state.point_size_set(size)
batch = batch_for_shader(shader, 'POINTS', {"pos": points})
shader.bind()
shader.uniform_float("color", color)
batch.draw(shader)


def draw_guideline(shader, line, size, color):
gpu.state.depth_test_set('NONE')
gpu.state.blend_set('ALPHA')
gpu.state.line_width_set(size)
batch = batch_for_shader(shader, 'LINES', {"pos": line})
shader.bind()
shader.uniform_float("color", color)
batch.draw(shader)


def update_points(cls):
shader = gpu.shader.from_builtin('3D_UNIFORM_COLOR')

draw_points(shader, cls.primary_points, 10, (82/255, 224/255, 82/255, 1.))
draw_points(shader, cls.secondary_points, 6, (255/255, 135/255, 55/255, 1.0))
draw_points(shader, cls.tertiary_points, 12, (82/255, 224/255, 82/255, 1.0))
draw_guideline(shader, cls.guide_line, 2, (82/255, 224/255, 82/255, 0.5))

redraw_regions()
2 changes: 2 additions & 0 deletions utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from . import clear_vgs
from . import cycle
from . import flare
from . import snap_align
from . import triangulate


Expand All @@ -30,6 +31,7 @@
clear_vgs,
cycle,
flare,
snap_align,
triangulate
)

Expand Down
248 changes: 248 additions & 0 deletions utils/snap_align.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
# “Commons Clause” License Condition v1.0
#
# See LICENSE for license details. If you did not receive a copy of the license,
# it may be obtained at https://github.com/hugemenace/nd/blob/main/LICENSE.
#
# Software: ND Blender Addon
# License: MIT
# Licensor: T.S. & I.J. (HugeMenace)

import bpy
import bmesh
from numpy.linalg import norm
from bpy_extras.view3d_utils import region_2d_to_vector_3d, region_2d_to_origin_3d
from .. lib.overlay import update_overlay, init_overlay, toggle_pin_overlay, toggle_operator_passthrough, register_draw_handler, unregister_draw_handler, draw_header, draw_property, draw_hint
from .. lib.events import capture_modifier_keys, pressed
from .. lib.points import init_points, register_points_handler, unregister_points_handler
from .. lib.math import v3_average, v3_distance, create_rotation_matrix_from_vertex, create_rotation_matrix_from_edge, create_rotation_matrix_from_face


class ND_OT_snap_align(bpy.types.Operator):
bl_idname = "nd.snap_align"
bl_label = "Snap Align"
bl_description = "Align and snap one object to another"
bl_options = {'UNDO'}


def modal(self, context, event):
capture_modifier_keys(self, event)

if self.key_toggle_operator_passthrough:
toggle_operator_passthrough(self)

elif self.key_toggle_pin_overlay:
toggle_pin_overlay(self, event)

elif self.operator_passthrough:
update_overlay(self, context, event)

return {'PASS_THROUGH'}

elif self.key_cancel:
self.revert(context)

return {'CANCELLED'}

elif pressed(event, {'C'}):
if self.snap_point:
self.snap_point_rotation_cache = self.snap_point[1]
self.tertiary_points = [self.snap_point[0]]

self.dirty = True

elif pressed(event, {'R'}):
self.snap_point_rotation_cache = None
self.tertiary_points = []

self.dirty = True

elif self.key_confirm:
self.finish(context)

return {'FINISHED'}

elif self.key_movement_passthrough:
return {'PASS_THROUGH'}

elif event.type == 'MOUSEMOVE':
coords = (event.mouse_region_x, event.mouse_region_y)
self.recalculate_points(context, coords)

if self.dirty:
self.operate(context)

update_overlay(self, context, event)

return {'RUNNING_MODAL'}


def invoke(self, context, event):
self.dirty = False
self.hit_location = None
self.primary_points = []
self.secondary_points = []
self.tertiary_points = []
self.snap_point = None
self.snap_point_rotation_cache = None

bpy.ops.object.hide_view_set(unselected=True)

a, b = context.selected_objects
self.reference_obj = a if a.name != context.object.name else b
self.reference_obj_original_location = self.reference_obj.location.copy()
self.reference_obj_original_rotation = self.reference_obj.rotation_euler.copy()

depsgraph = context.evaluated_depsgraph_get()
object_eval = context.object.evaluated_get(depsgraph)

bm = bmesh.new()
bm.from_mesh(object_eval.data)

world_matrix = context.object.matrix_world

vert_points = []
for vert in bm.verts:
vert_matrix = create_rotation_matrix_from_vertex(world_matrix, vert)
vert_points.append((world_matrix @ vert.co, vert_matrix))

edge_points = []
edge_lengths = []
for edge in bm.edges:
edge_matrix = create_rotation_matrix_from_edge(world_matrix, edge)
edge_points.append((world_matrix @ v3_average([v.co for v in edge.verts]), edge_matrix))
edge_lengths.append(v3_distance(world_matrix @ edge.verts[0].co, world_matrix @ edge.verts[1].co))

face_points = []
for face in bm.faces:
face_matrix = create_rotation_matrix_from_face(world_matrix, face)
face_points.append((world_matrix @ v3_average([v.co for v in face.verts]), face_matrix))

shortest_edge_length = min(edge_lengths)
self.snap_distance_factor = shortest_edge_length / 2.0

self.points_cache = vert_points + edge_points + face_points

bm.free()

self.operate(context)

capture_modifier_keys(self, None, event.mouse_x)

init_overlay(self, event)
register_draw_handler(self, draw_text_callback)

init_points(self)
register_points_handler(self)

context.window_manager.modal_handler_add(self)

return {'RUNNING_MODAL'}


@classmethod
def poll(cls, context):
if context.mode == 'OBJECT':
return len(context.selected_objects) == 2 and context.active_object.type == 'MESH'


def operate(self, context):
if self.snap_point:
vect, rotation_matrix = self.snap_point
self.reference_obj.location = vect
if self.snap_point_rotation_cache is None:
self.reference_obj.rotation_euler = rotation_matrix.to_euler()
else:
self.reference_obj.rotation_euler = self.snap_point_rotation_cache.to_euler()

elif self.hit_location:
self.reference_obj.location = self.hit_location

if self.tertiary_points:
self.guide_line = (self.reference_obj.location, self.tertiary_points[0])

self.dirty = False


def recalculate_points(self, context, mouse_coords):
self.reference_obj.hide_set(True)

region = context.region
region_data = context.space_data.region_3d

view_vector = region_2d_to_vector_3d(region, region_data, mouse_coords)
ray_origin = region_2d_to_origin_3d(region, region_data, mouse_coords)

ray_target = ray_origin + view_vector
ray_direction = ray_target - ray_origin

depsgraph = context.evaluated_depsgraph_get()

hit, location, normal, face_index, object, matrix = context.scene.ray_cast(depsgraph, ray_origin, ray_direction)

if hit and object.name == context.object.name:
self.hit_location = location

self.primary_points = []
self.secondary_points = []
snap_points = []
for (vect, rotation_matrix) in self.points_cache:
if v3_distance(vect, location) <= (0.2 * self.snap_distance_factor):
snap_points.append((vect, rotation_matrix))
elif v3_distance(vect, location) <= (0.8 * self.snap_distance_factor):
self.secondary_points.append(vect)

snap_points.sort(key=lambda p: v3_distance(p[0], location))
self.snap_point = snap_points[0] if snap_points else None
self.primary_points = [self.snap_point[0]] if self.snap_point else []
else:
self.hit_location = None
self.primary_points = []
self.secondary_points = []

self.reference_obj.hide_set(False)

self.dirty = True
self.operate(context)


def finish(self, context):
bpy.ops.object.hide_view_clear()

unregister_draw_handler()
unregister_points_handler()


def revert(self, context):
bpy.ops.object.hide_view_clear()

self.reference_obj.location = self.reference_obj_original_location
self.reference_obj.rotation_euler = self.reference_obj_original_rotation

unregister_draw_handler()
unregister_points_handler()


def draw_text_callback(self):
draw_header(self)

draw_hint(self, "Select snap point", "Hover over the selected object to select a snap point")

draw_property(
self,
"Capture Rotation [C] / Reset [R]".format(),
"{}".format("Snap point rotation captured!" if self.snap_point_rotation_cache else "Free rotation enabled..."),
active=self.snap_point_rotation_cache is not None,
alt_mode=False)


def menu_func(self, context):
self.layout.operator(ND_OT_snap_align.bl_idname, text=ND_OT_snap_align.bl_label)


def register():
bpy.utils.register_class(ND_OT_snap_align)


def unregister():
bpy.utils.unregister_class(ND_OT_snap_align)
unregister_draw_handler()

0 comments on commit 7548c66

Please sign in to comment.