From 7548c6645449c2598dd3bd61f7a6555cb6176661 Mon Sep 17 00:00:00 2001 From: Tristo <77056966+tristan-hm@users.noreply.github.com> Date: Wed, 11 May 2022 19:24:23 +1000 Subject: [PATCH] feat: add snap_align operator --- interface/ops.py | 1 + interface/utils_menu.py | 1 + lib/__init__.py | 3 + lib/math.py | 4 + lib/points.py | 79 +++++++++++++ utils/__init__.py | 2 + utils/snap_align.py | 248 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 338 insertions(+) create mode 100644 lib/points.py create mode 100644 utils/snap_align.py diff --git a/interface/ops.py b/interface/ops.py index 78d700c..659f939 100644 --- a/interface/ops.py +++ b/interface/ops.py @@ -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 = [ diff --git a/interface/utils_menu.py b/interface/utils_menu.py index fafb6b2..f0a50fb 100644 --- a/interface/utils_menu.py +++ b/interface/utils_menu.py @@ -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') diff --git a/lib/__init__.py b/lib/__init__.py index b12fb76..9892404 100644 --- a/lib/__init__.py +++ b/lib/__init__.py @@ -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 @@ -27,6 +28,7 @@ objects, overlay, axis, + points, viewport, assets, updates, @@ -42,3 +44,4 @@ def reload(): overlay.unregister_draw_handler() axis.unregister_axis_handler() + points.unregister_points_handler() diff --git a/lib/math.py b/lib/math.py index 62c9742..a4f1551 100644 --- a/lib/math.py +++ b/lib/math.py @@ -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]) diff --git a/lib/points.py b/lib/points.py new file mode 100644 index 0000000..5e55f75 --- /dev/null +++ b/lib/points.py @@ -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() diff --git a/utils/__init__.py b/utils/__init__.py index b41f983..72bf94c 100644 --- a/utils/__init__.py +++ b/utils/__init__.py @@ -17,6 +17,7 @@ from . import clear_vgs from . import cycle from . import flare +from . import snap_align from . import triangulate @@ -30,6 +31,7 @@ clear_vgs, cycle, flare, + snap_align, triangulate ) diff --git a/utils/snap_align.py b/utils/snap_align.py new file mode 100644 index 0000000..56a3855 --- /dev/null +++ b/utils/snap_align.py @@ -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()