# -*- coding:utf-8 -*- # ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110- 1301, USA. # # ##### END GPL LICENSE BLOCK ##### # # ---------------------------------------------------------- # Author: Stephen Leger (s-leger) # Inspired by Okavango's np_point_move # ---------------------------------------------------------- """ Usage: from .archipack_snap import snap_point snap_point(takeloc, draw_callback, action_callback, constraint_axis) arguments: takeloc Vector3d location of point to snap constraint_axis boolean tuple for each axis eg: (True, True, False) to constrtaint to xy plane draw_callback(context, sp) sp.takeloc sp.placeloc sp.delta action_callback(context, event, state, sp) state in {'RUNNING', 'SUCCESS', 'CANCEL'} sp.takeloc sp.placeloc sp.delta with 3d Vectors - delta = placeloc - takeloc - takeloc - placeloc NOTE: may change grid size to 0.1 round feature (SHIFT) see https://blenderartists.org/forum/showthread.php?205158-Blender-2-5-Snap-mode-increment then use a SHIFT use grid snap """ import bpy from bpy.types import Operator from mathutils import Vector, Matrix import logging logger = logging.getLogger("archipack") def dumb_callback(context, event, state, sp): return def dumb_draw(sp, context): return class SnapStore: """ Global store """ callback = None draw = None helper = None takeloc = Vector() placeloc = Vector() constraint_axis = (True, True, False) helper_matrix = Matrix() transform_orientation = 'GLOBAL' release_confirm = True instances_running = 0 # context related act = None sel = [] use_snap = False snap_elements = None snap_target = None pivot_point = None trans_orientation = None def snap_point(takeloc=None, draw=None, callback=dumb_callback, takemat=None, constraint_axis=(True, True, False), transform_orientation='GLOBAL', mode='OBJECT', release_confirm=True): """ Invoke op from outside world in a convenient importable function transform_orientation in [‘GLOBAL’, ‘LOCAL’, ‘NORMAL’, ‘GIMBAL’, ‘VIEW’] draw(sp, context) a draw callback callback(context, event, state, sp) action callback Use either : takeloc Vector, unconstraint or system axis constraints takemat Matrix, constaint to this matrix as 'LOCAL' coordsys The snap source helper use it as world matrix so it is possible to constraint to user defined coordsys. """ SnapStore.draw = draw SnapStore.callback = callback SnapStore.constraint_axis = constraint_axis SnapStore.release_confirm = release_confirm if takemat is not None: SnapStore.helper_matrix = takemat takeloc = takemat.translation.copy() transform_orientation = 'LOCAL' elif takeloc is not None: SnapStore.helper_matrix = Matrix.Translation(takeloc) else: raise ValueError("ArchipackSnap: Either takeloc or takemat must be defined") SnapStore.takeloc = takeloc SnapStore.placeloc = takeloc.copy() SnapStore.transform_orientation = transform_orientation # @NOTE: unused mode var to switch between OBJECT and EDIT mode # for ArchipackSnapBase to be able to handle both modes # must implements corresponding helper create and delete actions SnapStore.mode = mode bpy.ops.archipack.snap('INVOKE_DEFAULT') # return helper so we are able to move it "live" return SnapStore.helper class ArchipackSnapBase(): """ Helper class for snap Operators store and restore context create and destroy helper install and remove a draw_callback working while snapping store and provide access to 3d Vectors in draw_callback and action_callback - delta = placeloc - takeloc - takeloc - placeloc """ def __init__(self): self._draw_handler = None def init(self, context, event): # Store context data # if SnapStore.instances_running < 1: SnapStore.sel = context.selected_objects[:] SnapStore.act = context.object bpy.ops.object.select_all(action="DESELECT") ts = context.tool_settings SnapStore.use_snap = ts.use_snap SnapStore.snap_elements = ts.snap_elements SnapStore.snap_target = ts.snap_target SnapStore.pivot_point = ts.transform_pivot_point SnapStore.trans_orientation = context.scene.transform_orientation_slots[0].type self.create_helper(context) # Use a timer to broadcast a TIMER event while transform.translate is running self._timer = context.window_manager.event_timer_add(0.1, window=context.window) if SnapStore.draw is not None: args = (self, context) self._draw_handler = bpy.types.SpaceView3D.draw_handler_add(SnapStore.draw, args, 'WINDOW', 'POST_PIXEL') def remove_timer(self, context): if self._timer is not None: context.window_manager.event_timer_remove(self._timer) def exit(self, context): self.remove_timer(context) if self._draw_handler is not None: bpy.types.SpaceView3D.draw_handler_remove(self._draw_handler, 'WINDOW') # Restore original context if hasattr(context, "tool_settings"): ts = context.tool_settings ts.use_snap = SnapStore.use_snap ts.snap_elements = SnapStore.snap_elements ts.snap_target = SnapStore.snap_target ts.transform_pivot_point = SnapStore.pivot_point context.scene.transform_orientation_slots[0].type = SnapStore.trans_orientation for o in SnapStore.sel: o.select_set(state=True) if SnapStore.act is not None: SnapStore.act.select_set(state=True) context.view_layer.objects.active = SnapStore.act self.destroy_helper(context) logger.debug("Snap.exit %s", context.object.name) def create_helper(self, context): """ Create a helper with fake user or find older one in bpy data and relink to scene currently only support OBJECT mode Do target helper be linked to scene in order to work ? """ helper = bpy.data.objects.get('Archipack_snap_helper') if helper is not None: print("helper found") if context.scene.objects.get('Archipack_snap_helper') is None: print("link helper") # self.link_object_to_scene(context, helper) context.scene.collection.objects.link(helper) else: print("create helper") m = bpy.data.meshes.new("Archipack_snap_helper") m.vertices.add(count=1) helper = bpy.data.objects.new("Archipack_snap_helper", m) context.scene.collection.objects.link(helper) helper.use_fake_user = True helper.data.use_fake_user = True helper.matrix_world = SnapStore.helper_matrix helper.select_set(state=True) context.view_layer.objects.active = helper SnapStore.helper = helper def destroy_helper(self, context): """ Unlink helper currently only support OBJECT mode """ if SnapStore.helper is not None: # @TODO: Fix this # self.unlink_object_from_scene(context, SnapStore.helper) SnapStore.helper = None @property def delta(self): return self.placeloc - self.takeloc @property def takeloc(self): return SnapStore.takeloc @property def placeloc(self): # take from helper when there so the delta # is working even while modal is running if SnapStore.helper is not None: return SnapStore.helper.location else: return SnapStore.placeloc class ARCHIPACK_OT_snap(ArchipackSnapBase, Operator): bl_idname = 'archipack.snap' bl_label = 'Archipack snap' bl_options = {'INTERNAL'} # , 'UNDO' def modal(self, context, event): if SnapStore.helper is not None: logger.debug("Snap.modal event %s %s location:%s", event.type, event.value, SnapStore.helper.location) context.area.tag_redraw() if event.type in ('TIMER', 'NOTHING'): SnapStore.callback(context, event, 'RUNNING', self) return {'PASS_THROUGH'} if event.type not in ('ESC', 'RIGHTMOUSE', 'LEFTMOUSE', 'MOUSEMOVE', 'INBETWEEN_MOUSEMOVE'): return {'PASS_THROUGH'} if event.type in ('ESC', 'RIGHTMOUSE'): SnapStore.callback(context, event, 'CANCEL', self) else: SnapStore.placeloc = SnapStore.helper.location # on tt modal exit with right click, the delta is 0 so exit if self.delta.length == 0: SnapStore.callback(context, event, 'CANCEL', self) else: SnapStore.callback(context, event, 'SUCCESS', self) self.exit(context) # self.report({'INFO'}, "ARCHIPACK_OT_snap exit") return {'FINISHED'} def invoke(self, context, event): if context.area.type == 'VIEW_3D': if event.type in ('ESC', 'RIGHTMOUSE'): return {'FINISHED'} self.init(context, event) logger.debug("Snap.invoke event %s %s location:%s act:%s", event.type, event.value, SnapStore.helper.location, context.object.name) context.window_manager.modal_handler_add(self) bpy.ops.transform.translate('INVOKE_DEFAULT', constraint_axis=SnapStore.constraint_axis, orient_type=SnapStore.transform_orientation, release_confirm=SnapStore.release_confirm) logger.debug("Snap.invoke transform.translate done") return {'RUNNING_MODAL'} else: self.report({'WARNING'}, "View3D not found, cannot run operator") return {'FINISHED'} def register(): bpy.utils.register_class(ARCHIPACK_OT_snap) def unregister(): bpy.utils.unregister_class(ARCHIPACK_OT_snap)