# -*- 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) # # ---------------------------------------------------------- import bpy from math import atan2, pi from mathutils import Vector, Matrix from mathutils.geometry import intersect_line_plane, intersect_point_line, intersect_line_sphere from bpy_extras import view3d_utils from bpy.types import PropertyGroup, Operator from bpy.props import ( FloatVectorProperty, StringProperty, CollectionProperty, BoolProperty ) from bpy.app.handlers import persistent from .archipack_snap import snap_point from .archipack_keymaps import Keymaps from .archipack_gl import ( GlLine, GlArc, GlText, GlPolyline, GlPolygon, TriHandle, SquareHandle, EditableText, FeedbackPanel, GlCursorArea ) # NOTE: # Snap aware manipulators use a dirty hack : # draw() as a callback to update values in realtime # as transform.translate in use to allow snap # does catch all events. # This however has a wanted side effect: # the manipulator take precedence over already running # ones, and prevent select mode to start. # # TODO: # Other manipulators should use same technique to take # precedence over already running ones when active # # NOTE: # Select mode does suffer from this stack effect: # the last running wins. The point is left mouse select mode # requiring left drag to be RUNNING_MODAL to prevent real # objects select and move during manipulators selection. # # TODO: # First run a separate modal dedicated to select mode. # Selecting in whole manips stack when required # (manips[key].manipulable.manip_stack) # Must investigate for a way to handle unselect after drag done. import logging logger = logging.getLogger("archipack") """ Change object location when moving 1 point When False, change data.origin instead """ USE_MOVE_OBJECT = True # Arrow sizes (world units) arrow_size = 0.05 # Handle area size (pixels) handle_size = 10 # a global manipulator stack reference # prevent Blender "ACCESS_VIOLATION" crashes # use a dict to prevent collisions # between many objects being in manipulate mode # use object names as loose keys # NOTE : use app.drivers to reset before file load manips = {} class ArchipackActiveManip: """ Store manipulated object - object_name: manipulated object name - stack: array of Manipulators instances - manipulable: Manipulable instance """ def __init__(self, object_name): self.object_name = object_name # manipulators stack for object self.stack = [] # reference to object manipulable instance self.manipulable = None def is_snapping(self, ctx): """ Check if snap is active """ return ctx.active_object and ctx.active_object.name.startswith("Archipack_") @property def dirty(self): """ Check for manipulable validity to disable modal when required """ o = bpy.data.objects.get(self.object_name) return ( self.manipulable is None or o is None or # The object is not selected and snap is not active not (self.is_snapping(bpy.context) or o.select_get()) ) def exit(self): """ Exit manipulation mode - exit from all running manipulators - empty manipulators stack - set manipulable.manipulate_mode to False - remove reference to manipulable """ for m in self.stack: if m is not None: m.exit() if self.manipulable is not None: self.manipulable.manipulate_mode = False self.manipulable = None self.object_name = "" self.stack.clear() def remove_manipulable(key): """ disable and remove a manipulable from stack """ global manips # print("remove_manipulable key:%s" % (key)) if key in manips.keys(): manips[key].exit() manips.pop(key) def check_stack(key): """ check for stack item validity use in modal to destroy invalid modals return true when invalid / not found false when valid """ global manips if key not in manips.keys(): # print("check_stack : key not found %s" % (key)) return True elif manips[key].dirty: # print("check_stack : key.dirty %s" % (key)) remove_manipulable(key) return True return False def empty_stack(): # print("empty_stack()") """ kill every manipulators in stack and cleanup stack """ global manips for key in manips.keys(): manips[key].exit() manips.clear() def add_manipulable(key, manipulable): """ add a ArchipackActiveManip into the stack if not already present setup reference to manipulable return manipulators stack """ global manips if key not in manips.keys(): # print("add_manipulable() key:%s not found create new" % (key)) manips[key] = ArchipackActiveManip(key) manips[key].manipulable = manipulable return manips[key].stack # ------------------------------------------------------------------ # Define Manipulators # ------------------------------------------------------------------ class Manipulator(): """ Manipulator base class to derive other handle keyboard and modal events provide convenient funcs including getter and setter for datablock values store reference of base object, datablock and manipulator """ keyboard_ascii = { ".", ",", "-", "+", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "c", "m", "d", "k", "h", "a", " ", "/", "*", "'", "\"" # "=" } keyboard_type = { 'BACK_SPACE', 'DEL', 'LEFT_ARROW', 'RIGHT_ARROW' } def __init__(self, context, o, datablock, manipulator, snap_callback=None): """ o : object to manipulate datablock : object data to manipulate manipulator: object archipack_manipulator datablock snap_callback: on snap enabled manipulators, will be called when drag occurs """ self.keymap = Keymaps(context) self.feedback = FeedbackPanel() self.active = False self.selectable = False self.selected = False # active text input value for manipulator self.keyboard_input_active = False self.label_value = 0 # unit for keyboard input value self.value_type = 'LENGTH' self.pts_mode = 'SIZE' self.o = o self.datablock = datablock self.manipulator = manipulator self.snap_callback = snap_callback self.origin = Vector((0, 0, 1)) self.mouse_pos = Vector((0, 0)) self.length_entered = "" self.line_pos = 0 args = (self, context) self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL') @classmethod def poll(cls, context): """ Allow manipulator enable/disable in given context handles will not show """ return True def exit(self): """ Modal exit, DON'T EVEN TRY TO OVERRIDE """ if self._handle is not None: bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW') self._handle = None else: print("Manipulator.exit() handle not found %s" % (type(self).__name__)) # Mouse event handlers, MUST be overridden def mouse_press(self, context, event): """ Manipulators must implement mouse press event handler return True to callback manipulable_manipulate """ raise NotImplementedError def mouse_release(self, context, event): """ Manipulators must implement mouse mouse_release event handler return False to callback manipulable_release """ raise NotImplementedError def mouse_move(self, context, event): """ Manipulators must implement mouse move event handler return True to callback manipulable_manipulate """ raise NotImplementedError # Keyboard event handlers, MAY be overridden def keyboard_done(self, context, event, value): """ Manipulators may implement keyboard value validated event handler value: changed by keyboard return True to callback manipulable_manipulate """ return False def keyboard_editing(self, context, event, value): """ Manipulators may implement keyboard value changed event handler value: string changed by keyboard allow realtime update of label return False to show edited value on window header return True when feedback show right on screen """ self.label_value = value return True def keyboard_cancel(self, context, event): """ Manipulators may implement keyboard entry cancelled """ return def cancel(self, context, event): """ Manipulators may implement cancelled event (ESC RIGHTCLICK) """ self.active = False return def undo(self, context, event): """ Manipulators may implement undo event (CTRL+Z) """ return False # Internal, do not override unless you really # really really deeply know what you are doing def keyboard_eval(self, context, event): """ evaluate keyboard entry while typing do not override this one """ c = event.ascii if c: if c == ",": c = "." self.length_entered = self.length_entered[:self.line_pos] + c + self.length_entered[self.line_pos:] self.line_pos += 1 if self.length_entered: if event.type == 'BACK_SPACE': self.length_entered = self.length_entered[:self.line_pos - 1] + self.length_entered[self.line_pos:] self.line_pos -= 1 elif event.type == 'DEL': self.length_entered = self.length_entered[:self.line_pos] + self.length_entered[self.line_pos + 1:] elif event.type == 'LEFT_ARROW': self.line_pos = (self.line_pos - 1) % (len(self.length_entered) + 1) elif event.type == 'RIGHT_ARROW': self.line_pos = (self.line_pos + 1) % (len(self.length_entered) + 1) try: value = bpy.utils.units.to_value(context.scene.unit_settings.system, self.value_type, self.length_entered) draw_on_header = self.keyboard_editing(context, event, value) except: # ValueError: draw_on_header = True pass if draw_on_header: a = "" if self.length_entered: pos = self.line_pos a = self.length_entered[:pos] + '|' + self.length_entered[pos:] context.area.header_text_set("%s" % (a)) # modal mode: do not let event bubble up return True def modal(self, context, event): """ Modal handler handle mouse, and keyboard events enable and disable feedback """ # print("Manipulator modal:%s %s" % (event.value, event.type)) if event.type == 'MOUSEMOVE': return self.mouse_move(context, event) elif event.value == 'PRESS': if event.type == 'LEFTMOUSE': active = self.mouse_press(context, event) if active: self.feedback.enable() return active elif self.keymap.check(event, self.keymap.undo): if self.keyboard_input_active: self.keyboard_input_active = False self.keyboard_cancel(context, event) self.feedback.disable() # prevent undo CRASH return True elif self.keyboard_input_active and ( event.ascii in self.keyboard_ascii or event.type in self.keyboard_type ): # get keyboard input return self.keyboard_eval(context, event) elif event.type in {'ESC', 'RIGHTMOUSE'}: self.feedback.disable() if self.keyboard_input_active: # allow keyboard exit without setting value self.length_entered = "" self.line_pos = 0 self.keyboard_input_active = False self.keyboard_cancel(context, event) return True elif self.active: self.cancel(context, event) return True return False elif event.value == 'RELEASE': if event.type == 'LEFTMOUSE': if not self.keyboard_input_active: self.feedback.disable() return self.mouse_release(context, event) elif self.keyboard_input_active and event.type in {'RET', 'NUMPAD_ENTER'}: # validate keyboard input if self.length_entered != "": try: value = bpy.utils.units.to_value( context.scene.unit_settings.system, self.value_type, self.length_entered) self.length_entered = "" ret = self.keyboard_done(context, event, value) except: # ValueError: ret = False self.keyboard_cancel(context, event) pass context.area.header_text_set(None) self.keyboard_input_active = False self.feedback.disable() return ret return False def mouse_position(self, event): """ store mouse position in a 2d Vector """ self.mouse_pos.x, self.mouse_pos.y = event.mouse_region_x, event.mouse_region_y def get_pos3d(self, context): """ convert mouse pos to 3d point over plane defined by origin and normal pt is in world space """ region = context.region rv3d = context.region_data rM = context.active_object.matrix_world.to_3x3() view_vector_mouse = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse_pos) ray_origin_mouse = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse_pos) pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse, self.origin, rM @ self.manipulator.normal, False) # fix issue with parallel plane if pt is None: pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse, self.origin, view_vector_mouse, False) return pt def get_value(self, data, attr, index=-1): """ Datablock value getter with index support """ try: if index > -1: return getattr(data, attr)[index] else: return getattr(data, attr) except: print("get_value of %s %s failed" % (data, attr)) return 0 def set_value(self, context, data, attr, value, index=-1): """ Datablock value setter with index support """ try: if self.get_value(data, attr, index) != value: o = self.o # switch context so unselected object may be manipulable too old = context.object state = o.select_get() o.select_set(state=True) context.view_layer.objects.active = o if index > -1: getattr(data, attr)[index] = value else: setattr(data, attr, value) o.select_set(state=state) old.select_set(state=True) context.view_layer.objects.active = old except: pass def preTranslate(self, tM, vec): """ return a preTranslated Matrix tM Matrix source vec Vector translation """ return tM @ Matrix.Translation(vec) def _move(self, o, axis, value): if axis == 'x': vec = Vector((value, 0, 0)) elif axis == 'y': vec = Vector((0, value, 0)) else: vec = Vector((0, 0, value)) o.matrix_world = self.preTranslate(o.matrix_world, vec) def move_linked(self, context, axis, value): """ Move an object along local axis takes care of linked too, fix issue #8 """ old = context.active_object bpy.ops.object.select_all(action='DESELECT') self.o.select_set(state=True) context.view_layer.objects.active = self.o bpy.ops.object.select_linked(type='OBDATA') for o in context.selected_objects: if o != self.o: self._move(o, axis, value) bpy.ops.object.select_all(action='DESELECT') old.select_set(state=True) context.view_layer.objects.active = old def move(self, context, axis, value): """ Move an object along local axis """ self._move(self.o, axis, value) # Generic snap tool for line based archipack objects (fence, wall, maybe stair too) gl_pts3d = [] class WallSnapManipulator(Manipulator): """ np_station snap inspired manipulator Use prop1_name as string part index Use prop2_name as string identifier height property for placeholders Misnamed as it work for all line based archipack's primitives, currently wall and fences, but may also work with stairs (sharing same data structure) """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): self.placeholder_area = GlPolygon((0.5, 0, 0, 0.2)) self.placeholder_line = GlPolyline((0.5, 0, 0, 0.8)) self.placeholder_line.closed = True self.label = GlText() self.line = GlLine() self.handle = SquareHandle(handle_size, 1.2 * arrow_size, draggable=True, selectable=True) Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) self.selectable = True def select(self, cursor_area): self.selected = self.selected or cursor_area.in_area(self.handle.pos_2d) self.handle.selected = self.selected def deselect(self, cursor_area): self.selected = not cursor_area.in_area(self.handle.pos_2d) self.handle.selected = self.selected def check_hover(self): self.handle.check_hover(self.mouse_pos) def mouse_press(self, context, event): global gl_pts3d global manips if self.handle.hover: self.active = True self.handle.active = True gl_pts3d = [] idx = int(self.manipulator.prop1_name) # get selected manipulators idx selection = [] for m in manips[self.o.name].stack: if m is not None and m.selected: selection.append(int(m.manipulator.prop1_name)) # store all points of wall for i, part in enumerate(self.datablock.parts): p0, p1, side, normal = part.manipulators[2].get_pts(self.o.matrix_world) # if selected p0 will move and require placeholder gl_pts3d.append((p0, p1, i in selection or i == idx)) self.feedback.instructions(context, "Move / Snap", "Drag to move, use keyboard to input values", [ ('CTRL', 'Snap'), ('X Y', 'Constraint to axis (toggle Global Local None)'), ('SHIFT+Z', 'Constraint to xy plane'), ('MMBTN', 'Constraint to axis'), ('RIGHTCLICK or ESC', 'exit without change') ]) self.feedback.enable() self.handle.hover = False self.o.select_set(state=True) takeloc, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) dx = (right - takeloc).normalized() dy = dz.cross(dx) takemat = Matrix([ [dx.x, dy.x, dz.x, takeloc.x], [dx.y, dy.y, dz.y, takeloc.y], [dx.z, dy.z, dz.z, takeloc.z], [0, 0, 0, 1] ]) snap_point(takemat=takemat, draw=self.sp_draw, callback=self.sp_callback, constraint_axis=(True, True, False)) # this prevent other selected to run return True return False def mouse_release(self, context, event): self.check_hover() self.handle.active = False self.active = False self.feedback.disable() # False to callback manipulable_release return False def sp_callback(self, context, event, state, sp): """ np station callback on moving, place, or cancel """ global gl_pts3d logger.debug("WallSnapManipulator.sp_callback") if state == 'SUCCESS': o = self.o o.select_set(state=True) context.view_layer.objects.active = o # apply changes to wall d = self.datablock g = d.get_generator() # rotation relative to object rM = o.matrix_world.inverted().to_3x3() delta =rM @ sp.delta # x_axis = (rM @ Vector((1, 0, 0))).to_2d() # update generator idx = 0 for p0, p1, selected in gl_pts3d: if selected: # new location in object space pt = g.segs[idx].lerp(0) + delta.to_2d() # move last point of segment before current if idx > 0: g.segs[idx - 1].p1 = pt # move first point of current segment g.segs[idx].p0 = pt idx += 1 # update properties from generator idx = 0 d.auto_update = False for p0, p1, selected in gl_pts3d: if selected: # adjust segment before current if idx > 0: w = g.segs[idx - 1] part = d.parts[idx - 1] if idx > 1: part.a0 = w.delta_angle(g.segs[idx - 2]) else: part.a0 = w.a0 if "C_" in part.type: part.radius = w.r else: part.length = w.length # adjust current segment w = g.segs[idx] part = d.parts[idx] if idx > 0: part.a0 = w.delta_angle(g.segs[idx - 1]) else: part.a0 = w.a0 # move object when point 0 if USE_MOVE_OBJECT: d.move_object(o, o.matrix_world.translation + sp.delta) # self.o.location += sp.delta # self.o.matrix_world.translation += sp.delta else: d.origin += sp.delta if "C_" in part.type: part.radius = w.r else: part.length = w.length # adjust next one if idx + 1 < d.n_parts: d.parts[idx + 1].a0 = g.segs[idx + 1].delta_angle(w) idx += 1 self.mouse_release(context, event) if hasattr(d, "relocate_childs"): d.relocate_childs(context, o, g) d.auto_update = True d.update(context) if state == 'CANCEL': self.mouse_release(context, event) logger.debug("WallSnapManipulator.sp_callback done") return def sp_draw(self, sp, context): # draw wall placeholders logger.debug("WallSnapManipulator.sp_draw") global gl_pts3d if self.o is None: return z = self.get_value(self.datablock, self.manipulator.prop2_name) placeholders = [] for p0, p1, selected in gl_pts3d: pt = p0.copy() if selected: # when selected, p0 is moving # last one p1 should move too # last one require a placeholder too pt += sp.delta if len(placeholders) > 0: placeholders[-1][1] = pt placeholders[-1][2] = True placeholders.append([pt, p1, selected]) # first selected and closed -> should move last p1 too if gl_pts3d[0][2] and self.datablock.closed: placeholders[-1][1] = placeholders[0][0].copy() placeholders[-1][2] = True # last one not visible when not closed if not self.datablock.closed: placeholders[-1][2] = False for p0, p1, selected in placeholders: if selected: self.placeholder_area.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))]) self.placeholder_line.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))]) self.placeholder_area.draw(context, render=False) self.placeholder_line.draw(context, render=False) p0, p1, side, normal = self.manipulator.get_pts(self.o.matrix_world) self.line.p = p0 self.line.v = sp.delta self.label.set_pos(context, self.line.length, self.line.lerp(0.5), self.line.v, normal=Vector((0, 0, 1))) self.line.draw(context, render=False) self.label.draw(context, render=False) logger.debug("WallSnapManipulator.sp_draw done") def mouse_move(self, context, event): self.mouse_position(event) if self.handle.active: # False here to pass_through # print("i'm able to pick up mouse move event while transform running") return False else: self.check_hover() return False def draw_callback(self, _self, context, render=False): left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) self.handle.set_pos(context, left, (left - right).normalized(), normal=normal) self.handle.draw(context, render) self.feedback.draw(context, render) class CounterManipulator(Manipulator): """ increase or decrease an integer step by step right on click to prevent misuse """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): self.handle_left = TriHandle(handle_size, arrow_size, draggable=True) self.handle_right = TriHandle(handle_size, arrow_size, draggable=True) self.line_0 = GlLine() self.label = GlText() self.label.unit_mode = 'NONE' self.label.precision = 0 Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) def check_hover(self): self.handle_right.check_hover(self.mouse_pos) self.handle_left.check_hover(self.mouse_pos) def mouse_press(self, context, event): if self.handle_right.hover: value = self.get_value(self.datablock, self.manipulator.prop1_name) self.set_value(context, self.datablock, self.manipulator.prop1_name, value + 1) self.handle_right.active = True return True if self.handle_left.hover: value = self.get_value(self.datablock, self.manipulator.prop1_name) self.set_value(context, self.datablock, self.manipulator.prop1_name, value - 1) self.handle_left.active = True return True return False def mouse_release(self, context, event): self.check_hover() self.handle_right.active = False self.handle_left.active = False return False def mouse_move(self, context, event): self.mouse_position(event) if self.handle_right.active: return True if self.handle_left.active: return True else: self.check_hover() return False def draw_callback(self, _self, context, render=False): """ draw on screen feedback using gl. """ # won't render counter if render: return left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) self.origin = left self.line_0.p = left self.line_0.v = right - left self.line_0.z_axis = normal self.label.z_axis = normal value = self.get_value(self.datablock, self.manipulator.prop1_name) self.handle_left.set_pos(context, self.line_0.p, -self.line_0.v, normal=normal) self.handle_right.set_pos(context, self.line_0.lerp(1), self.line_0.v, normal=normal) self.label.set_pos(context, value, self.line_0.lerp(0.5), self.line_0.v, normal=normal) self.label.draw(context, render) self.handle_left.draw(context, render) self.handle_right.draw(context, render) class DumbStringManipulator(Manipulator): """ not a real manipulator, but allow to show a string """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): self.label = GlText(colour=(0, 0, 0, 1)) self.label.unit_mode = 'NONE' self.label.label = manipulator.prop1_name Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) def check_hover(self): return False def mouse_press(self, context, event): return False def mouse_release(self, context, event): return False def mouse_move(self, context, event): return False def draw_callback(self, _self, context, render=False): """ draw on screen feedback using gl. """ # won't render string if render: return left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) pos = left + 0.5 * (right - left) self.label.set_pos(context, None, pos, pos, normal=normal) self.label.draw(context, render) class SizeManipulator(Manipulator): def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): self.handle_left = TriHandle(handle_size, arrow_size) self.handle_right = TriHandle(handle_size, arrow_size, draggable=True) self.line_0 = GlLine() self.line_1 = GlLine() self.line_2 = GlLine() self.label = EditableText(handle_size, arrow_size, draggable=True) # self.label.label = 'S ' Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) def check_hover(self): self.handle_right.check_hover(self.mouse_pos) self.label.check_hover(self.mouse_pos) def mouse_press(self, context, event): global gl_pts3d if self.handle_right.hover: self.active = True self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) self.original_location = self.o.matrix_world.translation.copy() self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [ ('CTRL', 'Snap'), ('SHIFT', 'Round'), ('RIGHTCLICK or ESC', 'cancel') ]) left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) dx = (right - left).normalized() dy = dz.cross(dx) takemat = Matrix([ [dx.x, dy.x, dz.x, right.x], [dx.y, dy.y, dz.y, right.y], [dx.z, dy.z, dz.z, right.z], [0, 0, 0, 1] ]) gl_pts3d = [left, right] snap_point(takemat=takemat, callback=self.sp_callback, constraint_axis=(True, False, False)) self.handle_right.active = True return True if self.label.hover: self.feedback.instructions(context, "Size", "Use keyboard to modify size", [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')]) self.label.active = True self.keyboard_input_active = True return True return False def mouse_release(self, context, event): self.active = False self.check_hover() self.handle_right.active = False if not self.keyboard_input_active: self.feedback.disable() return False def mouse_move(self, context, event): self.mouse_position(event) if self.active: self.update(context, event) return True else: self.check_hover() return False def cancel(self, context, event): if self.active: self.mouse_release(context, event) self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_size) def keyboard_done(self, context, event, value): self.set_value(context, self.datablock, self.manipulator.prop1_name, value) self.label.active = False return True def keyboard_cancel(self, context, event): self.label.active = False return False def update(self, context, event): # 0 1 2 # |_____| # pt = self.get_pos3d(context) pt, t = intersect_point_line(pt, self.line_0.p, self.line_2.p) length = (self.line_0.p - pt).length if event.alt: length = round(length, 1) self.set_value(context, self.datablock, self.manipulator.prop1_name, length) def draw_callback(self, _self, context, render=False): """ draw on screen feedback using gl. """ left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) self.origin = left self.line_1.p = left self.line_1.v = right - left self.line_0.z_axis = normal self.line_1.z_axis = normal self.line_2.z_axis = normal self.label.z_axis = normal self.line_0 = self.line_1.sized_normal(0, side.x * 1.1) self.line_2 = self.line_1.sized_normal(1, side.x * 1.1) self.line_1.offset(side.x * 1.0) self.handle_left.set_pos(context, self.line_1.p, -self.line_1.v, normal=normal) self.handle_right.set_pos(context, self.line_1.lerp(1), self.line_1.v, normal=normal) if not self.keyboard_input_active: self.label_value = self.line_1.length self.label.set_pos(context, self.label_value, self.line_1.lerp(0.5), self.line_1.v, normal=normal) self.line_0.draw(context, render) self.line_1.draw(context, render) self.line_2.draw(context, render) self.handle_left.draw(context, render) self.handle_right.draw(context, render) self.label.draw(context, render) self.feedback.draw(context, render) def sp_callback(self, context, event, state, sp): logger.debug("SizeManipulator.sp_callback") global gl_pts3d p0 = gl_pts3d[0].copy() p1 = gl_pts3d[1].copy() if state != 'CANCEL': p1 += sp.delta length = (p0 - p1).length if state != 'CANCEL' and event.alt: if event.shift: length = round(length, 2) else: length = round(length, 1) self.set_value(context, self.datablock, self.manipulator.prop1_name, length) if state != 'RUNNING': self.mouse_release(context, event) logger.debug("SizeManipulator.sp_callback done") class SizeLocationManipulator(SizeManipulator): """ Handle resizing by any of the boundaries of objects with centered pivots so when size change, object should move of the half of the change in the direction of change. Also take care of moving linked objects too Changing size is not necessary as link does already handle this and childs panels are updated by base object. """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) self.handle_left.draggable = True def check_hover(self): self.handle_right.check_hover(self.mouse_pos) self.handle_left.check_hover(self.mouse_pos) self.label.check_hover(self.mouse_pos) def mouse_press(self, context, event): if self.handle_right.hover: self.active = True self.original_location = self.o.matrix_world.translation.copy() self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) self.feedback.instructions(context, "Size", "Drag to modify size", [ ('ALT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') ]) self.handle_right.active = True return True if self.handle_left.hover: self.active = True self.original_location = self.o.matrix_world.translation.copy() self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) self.feedback.instructions(context, "Size", "Drag to modify size", [ ('ALT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') ]) self.handle_left.active = True return True if self.label.hover: self.feedback.instructions(context, "Size", "Use keyboard to modify size", [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')]) self.label.active = True self.keyboard_input_active = True return True return False def mouse_release(self, context, event): self.active = False self.check_hover() self.handle_right.active = False self.handle_left.active = False if not self.keyboard_input_active: self.feedback.disable() return False def mouse_move(self, context, event): self.mouse_position(event) if self.handle_right.active or self.handle_left.active: self.update(context, event) return True else: self.check_hover() return False def keyboard_done(self, context, event, value): self.set_value(context, self.datablock, self.manipulator.prop1_name, value) # self.move_linked(context, self.manipulator.prop2_name, dl) self.label.active = False self.feedback.disable() return True def cancel(self, context, event): if self.active: self.mouse_release(context, event) # must move back to original location itM = self.o.matrix_world.inverted() dl = self.get_value(itM @ self.original_location, self.manipulator.prop2_name) self.move(context, self.manipulator.prop2_name, dl) self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_size) self.move_linked(context, self.manipulator.prop2_name, dl) def update(self, context, event): # 0 1 2 # |_____| # pt = self.get_pos3d(context) pt, t = intersect_point_line(pt, self.line_0.p, self.line_2.p) len_0 = (pt - self.line_0.p).length len_1 = (pt - self.line_2.p).length length = max(len_0, len_1) if event.alt: length = round(length, 1) dl = length - self.line_1.length if len_0 > len_1: dl = 0.5 * dl else: dl = -0.5 * dl self.move(context, self.manipulator.prop2_name, dl) self.set_value(context, self.datablock, self.manipulator.prop1_name, length) self.move_linked(context, self.manipulator.prop2_name, dl) class SnapSizeLocationManipulator(SizeLocationManipulator): """ Snap aware extension of SizeLocationManipulator Handle resizing by any of the boundaries of objects with centered pivots so when size change, object should move of the half of the change in the direction of change. Also take care of moving linked objects too Changing size is not necessary as link does already handle this and childs panels are updated by base object. """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): SizeLocationManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) def mouse_press(self, context, event): global gl_pts3d if self.handle_right.hover: self.active = True self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) self.original_location = self.o.matrix_world.translation.copy() self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [ ('CTRL', 'Snap'), ('SHIFT', 'Round'), ('RIGHTCLICK or ESC', 'cancel') ]) left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) dx = (right - left).normalized() dy = dz.cross(dx) takemat = Matrix([ [dx.x, dy.x, dz.x, right.x], [dx.y, dy.y, dz.y, right.y], [dx.z, dy.z, dz.z, right.z], [0, 0, 0, 1] ]) gl_pts3d = [left, right] snap_point(takemat=takemat, callback=self.sp_callback, constraint_axis=(True, False, False)) self.handle_right.active = True return True if self.handle_left.hover: self.active = True self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name) self.original_location = self.o.matrix_world.translation.copy() self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [ ('CTRL', 'Snap'), ('SHIFT', 'Round'), ('RIGHTCLICK or ESC', 'cancel') ]) left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) dx = (left - right).normalized() dy = dz.cross(dx) takemat = Matrix([ [dx.x, dy.x, dz.x, left.x], [dx.y, dy.y, dz.y, left.y], [dx.z, dy.z, dz.z, left.z], [0, 0, 0, 1] ]) gl_pts3d = [left, right] snap_point(takemat=takemat, callback=self.sp_callback, constraint_axis=(True, False, False)) self.handle_left.active = True return True if self.label.hover: self.feedback.instructions(context, "Size", "Use keyboard to modify size", [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')]) self.label.active = True self.keyboard_input_active = True return True return False def sp_callback(self, context, event, state, sp): logger.debug("SnapSizeLocationManipulator.sp_callback") global gl_pts3d p0 = gl_pts3d[0].copy() p1 = gl_pts3d[1].copy() if state != 'CANCEL': if self.handle_right.active: p1 += sp.delta else: p0 += sp.delta l0 = self.get_value(self.datablock, self.manipulator.prop1_name) length = (p0 - p1).length if state != 'CANCEL' and event.alt: if event.shift: length = round(length, 2) else: length = round(length, 1) dp = length - l0 if self.handle_left.active: dp = -dp dl = 0.5 * dp # snap_helper = context.object self.move(context, self.manipulator.prop2_name, dl) self.set_value(context, self.datablock, self.manipulator.prop1_name, length) self.move_linked(context, self.manipulator.prop2_name, dl) # snapping child objects may require base object update # eg manipulating windows requiring wall update if self.snap_callback is not None: snap_helper = context.active_object self.snap_callback(context, o=self.o, manipulator=self) snap_helper.select_set(state=True) if state != 'RUNNING': self.mouse_release(context, event) logger.debug("SnapSizeLocationManipulator.sp_callback done") class DeltaLocationManipulator(SizeManipulator): """ Move a child window or door in wall segment not limited to this by the way """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) self.label.label = '' self.feedback.instructions(context, "Move", "Drag to move", [ ('CTRL', 'Snap'), ('SHIFT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') ]) def check_hover(self): self.handle_right.check_hover(self.mouse_pos) def mouse_press(self, context, event): global gl_pts3d if self.handle_right.hover: self.original_location = self.o.matrix_world.translation.copy() self.active = True self.feedback.enable() self.handle_right.active = True left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world) dp = (right - left) dx = dp.normalized() dy = dz.cross(dx) p0 = left + 0.5 * dp takemat = Matrix([ [dx.x, dy.x, dz.x, p0.x], [dx.y, dy.y, dz.y, p0.y], [dx.z, dy.z, dz.z, p0.z], [0, 0, 0, 1] ]) gl_pts3d = [p0] snap_point(takemat=takemat, callback=self.sp_callback, constraint_axis=( self.manipulator.prop1_name == 'x', self.manipulator.prop1_name == 'y', self.manipulator.prop1_name == 'z')) return True return False def mouse_release(self, context, event): self.check_hover() self.feedback.disable() self.active = False self.handle_right.active = False return False def mouse_move(self, context, event): self.mouse_position(event) if self.handle_right.active: # self.update(context, event) return True else: self.check_hover() return False def sp_callback(self, context, event, state, sp): logger.debug("DeltaLocationManipulator.sp_callback") if state == 'CANCEL': self.cancel(context, event) else: global gl_pts3d p0 = gl_pts3d[0].copy() p1 = p0 + sp.delta itM = self.o.matrix_world.inverted() dl = self.get_value(itM @ p1, self.manipulator.prop1_name) self.move(context, self.manipulator.prop1_name, dl) # snapping child objects may require base object update # eg manipulating windows requiring wall update if self.snap_callback is not None: snap_helper = context.active_object self.snap_callback(context, o=self.o, manipulator=self) snap_helper.select_set(state=True) if state == 'SUCCESS': self.mouse_release(context, event) logger.debug("DeltaLocationManipulator.sp_callback done") def cancel(self, context, event): if self.active: self.mouse_release(context, event) # must move back to original location itM = self.o.matrix_world.inverted() dl = self.get_value(itM @ self.original_location, self.manipulator.prop1_name) self.move(context, self.manipulator.prop1_name, dl) def draw_callback(self, _self, context, render=False): """ draw on screen feedback using gl. """ left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world) self.origin = left self.line_1.p = left self.line_1.v = right - left self.line_1.z_axis = normal self.handle_left.set_pos(context, self.line_1.lerp(0.5), -self.line_1.v, normal=normal) self.handle_right.set_pos(context, self.line_1.lerp(0.5), self.line_1.v, normal=normal) self.handle_left.draw(context, render) self.handle_right.draw(context, render) self.feedback.draw(context) class DumbSizeManipulator(SizeManipulator): """ Show a size while not being editable """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) self.handle_right.draggable = False self.label.draggable = False self.label.colour_inactive = (0, 0, 0, 1) # self.label.label = 'Dumb ' def mouse_move(self, context, event): return False class AngleManipulator(Manipulator): """ NOTE: There is a default shortcut to +5 and -5 on angles with left/right arrows Manipulate angle between segments bound to [-pi, pi] """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): # Angle self.handle_right = TriHandle(handle_size, arrow_size, draggable=True) self.handle_center = SquareHandle(handle_size, arrow_size) self.arc = GlArc() self.line_0 = GlLine() self.line_1 = GlLine() self.label_a = EditableText(handle_size, arrow_size, draggable=True) self.label_a.unit_type = 'ANGLE' Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) self.pts_mode = 'RADIUS' def check_hover(self): self.handle_right.check_hover(self.mouse_pos) self.label_a.check_hover(self.mouse_pos) def mouse_press(self, context, event): if self.handle_right.hover: self.active = True self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name) self.feedback.instructions(context, "Angle", "Drag to modify angle", [ ('SHIFT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') ]) self.handle_right.active = True return True if self.label_a.hover: self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle", [('ENTER', 'validate'), ('RIGHTCLICK or ESC', 'cancel')]) self.value_type = 'ROTATION' self.label_a.active = True self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name) self.keyboard_input_active = True return True return False def mouse_release(self, context, event): self.check_hover() self.handle_right.active = False self.active = False return False def mouse_move(self, context, event): self.mouse_position(event) if self.active: # print("AngleManipulator.mouse_move") self.update(context, event) return True else: self.check_hover() return False def keyboard_done(self, context, event, value): self.set_value(context, self.datablock, self.manipulator.prop1_name, value) self.label_a.active = False return True def keyboard_cancel(self, context, event): self.label_a.active = False return False def cancel(self, context, event): if self.active: self.mouse_release(context, event) self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle) def update(self, context, event): pt = self.get_pos3d(context) c = self.arc.c v = 2 * self.arc.r * (pt - c).normalized() v0 = c - v v1 = c + v p0, p1 = intersect_line_sphere(v0, v1, c, self.arc.r) if p0 is not None and p1 is not None: if (p1 - pt).length < (p0 - pt).length: p0, p1 = p1, p0 v = p0 - self.arc.c da = atan2(v.y, v.x) - self.line_0.angle if da > pi: da -= 2 * pi if da < -pi: da += 2 * pi # from there pi > da > -pi # print("a:%.4f da:%.4f a0:%.4f" % (atan2(v.y, v.x), da, self.line_0.angle)) if da > pi: da = pi if da < -pi: da = -pi if event.shift: da = round(da / pi * 180, 0) / 180 * pi self.set_value(context, self.datablock, self.manipulator.prop1_name, da) def draw_callback(self, _self, context, render=False): c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world) self.line_0.z_axis = normal self.line_1.z_axis = normal self.arc.z_axis = normal self.label_a.z_axis = normal self.origin = c self.line_0.p = c self.line_1.p = c self.arc.c = c self.line_0.v = left self.line_0.v = -self.line_0.cross.normalized() self.line_1.v = right self.line_1.v = self.line_1.cross.normalized() self.arc.a0 = self.line_0.angle self.arc.da = self.get_value(self.datablock, self.manipulator.prop1_name) self.arc.r = 1.0 self.handle_right.set_pos(context, self.line_1.lerp(1), self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v) self.handle_center.set_pos(context, self.arc.c, -self.line_0.v) label_value = self.arc.da if self.keyboard_input_active: label_value = self.label_value self.label_a.set_pos(context, label_value, self.arc.lerp(0.5), -self.line_0.v) self.arc.draw(context, render) self.line_0.draw(context, render) self.line_1.draw(context, render) self.handle_right.draw(context, render) self.handle_center.draw(context, render) self.label_a.draw(context, render) self.feedback.draw(context, render) class DumbAngleManipulator(AngleManipulator): def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): AngleManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None) self.handle_right.draggable = False self.label_a.draggable = False def draw_callback(self, _self, context, render=False): c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world) self.line_0.z_axis = normal self.line_1.z_axis = normal self.arc.z_axis = normal self.label_a.z_axis = normal self.origin = c self.line_0.p = c self.line_1.p = c self.arc.c = c self.line_0.v = left self.line_0.v = -self.line_0.cross.normalized() self.line_1.v = right self.line_1.v = self.line_1.cross.normalized() # prevent ValueError in angle_signed if self.line_0.length == 0 or self.line_1.length == 0: return self.arc.a0 = self.line_0.angle self.arc.da = self.line_1.v.to_2d().angle_signed(self.line_0.v.to_2d()) self.arc.r = 1.0 self.handle_right.set_pos(context, self.line_1.lerp(1), self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v) self.handle_center.set_pos(context, self.arc.c, -self.line_0.v) label_value = self.arc.da self.label_a.set_pos(context, label_value, self.arc.lerp(0.5), -self.line_0.v) self.arc.draw(context, render) self.line_0.draw(context, render) self.line_1.draw(context, render) self.handle_right.draw(context, render) self.handle_center.draw(context, render) self.label_a.draw(context, render) self.feedback.draw(context, render) class ArcAngleManipulator(Manipulator): """ Manipulate angle of an arc when angle < 0 the arc center is on the left part of the circle when angle > 0 the arc center is on the right part of the circle bound to [-pi, pi] """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): # Fixed self.handle_left = SquareHandle(handle_size, arrow_size) # Angle self.handle_right = TriHandle(handle_size, arrow_size, draggable=True) self.handle_center = SquareHandle(handle_size, arrow_size) self.arc = GlArc() self.line_0 = GlLine() self.line_1 = GlLine() self.label_a = EditableText(handle_size, arrow_size, draggable=True) self.label_r = EditableText(handle_size, arrow_size, draggable=False) self.label_a.unit_type = 'ANGLE' Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback) self.pts_mode = 'RADIUS' def check_hover(self): self.handle_right.check_hover(self.mouse_pos) self.label_a.check_hover(self.mouse_pos) def mouse_press(self, context, event): if self.handle_right.hover: self.active = True self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name) self.feedback.instructions(context, "Angle (degree)", "Drag to modify angle", [ ('SHIFT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') ]) self.handle_right.active = True return True if self.label_a.hover: self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle", [('ENTER', 'validate'), ('RIGHTCLICK or ESC', 'cancel')]) self.value_type = 'ROTATION' self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name) self.label_a.active = True self.keyboard_input_active = True return True if self.label_r.hover: self.feedback.instructions(context, "Radius", "Use keyboard to modify radius", [('ENTER', 'validate'), ('RIGHTCLICK or ESC', 'cancel')]) self.value_type = 'LENGTH' self.label_r.active = True self.keyboard_input_active = True return True return False def mouse_release(self, context, event): self.check_hover() self.handle_right.active = False self.active = False return False def mouse_move(self, context, event): self.mouse_position(event) if self.handle_right.active: self.update(context, event) return True else: self.check_hover() return False def keyboard_done(self, context, event, value): self.set_value(context, self.datablock, self.manipulator.prop1_name, value) self.label_a.active = False self.label_r.active = False return True def keyboard_cancel(self, context, event): self.label_a.active = False self.label_r.active = False return False def cancel(self, context, event): if self.active: self.mouse_release(context, event) self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle) def update(self, context, event): pt = self.get_pos3d(context) c = self.arc.c v = 2 * self.arc.r * (pt - c).normalized() v0 = c - v v1 = c + v p0, p1 = intersect_line_sphere(v0, v1, c, self.arc.r) if p0 is not None and p1 is not None: # find nearest mouse intersection point if (p1 - pt).length < (p0 - pt).length: p0, p1 = p1, p0 v = p0 - self.arc.c s = self.arc.tangeant(0, 1) res, d, t = s.point_sur_segment(pt) if d > 0: # right side a = self.arc.sized_normal(0, self.arc.r).angle else: a = self.arc.sized_normal(0, -self.arc.r).angle da = atan2(v.y, v.x) - a # bottom side +- pi if t < 0: # right if d > 0: da = pi else: da = -pi # top side bound to +- pi else: if da > pi: da -= 2 * pi if da < -pi: da += 2 * pi if event.shift: da = round(da / pi * 180, 0) / 180 * pi self.set_value(context, self.datablock, self.manipulator.prop1_name, da) def draw_callback(self, _self, context, render=False): # center : 3d points # left : 3d vector pt-c # right : 3d vector pt-c c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world) self.line_0.z_axis = normal self.line_1.z_axis = normal self.arc.z_axis = normal self.label_a.z_axis = normal self.label_r.z_axis = normal self.origin = c self.line_0.p = c self.line_1.p = c self.arc.c = c self.line_0.v = left self.line_1.v = right self.arc.a0 = self.line_0.angle self.arc.da = self.get_value(self.datablock, self.manipulator.prop1_name) self.arc.r = left.length self.handle_left.set_pos(context, self.line_0.lerp(1), self.line_0.v) self.handle_right.set_pos(context, self.line_1.lerp(1), self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v) self.handle_center.set_pos(context, self.arc.c, -self.line_0.v) label_a_value = self.arc.da label_r_value = self.arc.r if self.keyboard_input_active: if self.value_type == 'LENGTH': label_r_value = self.label_value else: label_a_value = self.label_value self.label_a.set_pos(context, label_a_value, self.arc.lerp(0.5), -self.line_0.v) self.label_r.set_pos(context, label_r_value, self.line_0.lerp(0.5), self.line_0.v) self.arc.draw(context, render) self.line_0.draw(context, render) self.line_1.draw(context, render) self.handle_left.draw(context, render) self.handle_right.draw(context, render) self.handle_center.draw(context, render) self.label_r.draw(context, render) self.label_a.draw(context, render) self.feedback.draw(context, render) class ArcAngleRadiusManipulator(ArcAngleManipulator): """ Manipulate angle and radius of an arc when angle < 0 the arc center is on the left part of the circle when angle > 0 the arc center is on the right part of the circle bound to [-pi, pi] """ def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None): ArcAngleManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback) self.handle_center = TriHandle(handle_size, arrow_size, draggable=True) self.label_r.draggable = True def check_hover(self): self.handle_right.check_hover(self.mouse_pos) self.handle_center.check_hover(self.mouse_pos) self.label_a.check_hover(self.mouse_pos) self.label_r.check_hover(self.mouse_pos) def mouse_press(self, context, event): if self.handle_right.hover: self.active = True self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name) self.feedback.instructions(context, "Angle (degree)", "Drag to modify angle", [ ('SHIFT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') ]) self.handle_right.active = True return True if self.handle_center.hover: self.active = True self.original_radius = self.get_value(self.datablock, self.manipulator.prop2_name) self.feedback.instructions(context, "Radius", "Drag to modify radius", [ ('SHIFT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel') ]) self.handle_center.active = True return True if self.label_a.hover: self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle", [('ENTER', 'validate'), ('RIGHTCLICK or ESC', 'cancel')]) self.value_type = 'ROTATION' self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name) self.label_a.active = True self.keyboard_input_active = True return True if self.label_r.hover: self.feedback.instructions(context, "Radius", "Use keyboard to modify radius", [('ENTER', 'validate'), ('RIGHTCLICK or ESC', 'cancel')]) self.value_type = 'LENGTH' self.label_r.active = True self.keyboard_input_active = True return True return False def mouse_release(self, context, event): self.check_hover() self.active = False self.handle_right.active = False self.handle_center.active = False return False def mouse_move(self, context, event): self.mouse_position(event) if self.handle_right.active: self.update(context, event) return True elif self.handle_center.active: self.update_radius(context, event) return True else: self.check_hover() return False def keyboard_done(self, context, event, value): if self.value_type == 'LENGTH': self.set_value(context, self.datablock, self.manipulator.prop2_name, value) self.label_r.active = False else: self.set_value(context, self.datablock, self.manipulator.prop1_name, value) self.label_a.active = False return True def update_radius(self, context, event): pt = self.get_pos3d(context) c = self.arc.c left = self.line_0.lerp(1) p, t = intersect_point_line(pt, c, left) radius = (left - p).length if event.alt: radius = round(radius, 1) self.set_value(context, self.datablock, self.manipulator.prop2_name, radius) def cancel(self, context, event): if self.handle_right.active: self.mouse_release(context, event) self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle) if self.handle_center.active: self.mouse_release(context, event) self.set_value(context, self.datablock, self.manipulator.prop2_name, self.original_radius) # ------------------------------------------------------------------ # Define a single Manipulator Properties to store on object # ------------------------------------------------------------------ # Allow registering manipulators classes manipulators_class_lookup = {} def register_manipulator(type_key, manipulator_class): if type_key in manipulators_class_lookup.keys(): raise RuntimeError("Manipulator of type {} already exists, unable to override".format(type_key)) manipulators_class_lookup[type_key] = manipulator_class class archipack_manipulator(PropertyGroup): """ A property group to add to manipulable objects type_key: type of manipulator prop1_name = the property name of object to modify prop2_name = another property name of object to modify (eg: angle and radius) p0, p1, p2 3d Vectors as base points to represent manipulators on screen normal Vector normal of plane on with draw manipulator """ type_key : StringProperty(default='SIZE') # How 3d points are stored in manipulators ? # SIZE = 2 absolute positioned and a scaling vector # RADIUS = 1 absolute positioned (center) and 2 relatives (sides) # POLYGON = 2 absolute positioned and a relative vector (for rect polygons) pts_mode : StringProperty(default='SIZE') prop1_name : StringProperty() prop2_name : StringProperty() p0 : FloatVectorProperty(subtype='XYZ') p1 : FloatVectorProperty(subtype='XYZ') p2 : FloatVectorProperty(subtype='XYZ') # allow orientation of manipulators by default on xy plane, # but may be used to constrain heights on local object space normal : FloatVectorProperty(subtype='XYZ', default=(0, 0, 1)) def set_pts(self, pts, normal=None): """ set 3d location of gl points (in object space) pts: array of 3 vectors 3d normal: optional vector 3d default to Z axis """ pts = [Vector(p) for p in pts] self.p0, self.p1, self.p2 = pts if normal is not None: self.normal = Vector(normal) def get_pts(self, tM): """ convert points from local to world absolute to draw them at the right place tM : object's world matrix """ rM = tM.to_3x3() if self.pts_mode in ['SIZE', 'POLYGON']: return tM @ self.p0, tM @ self.p1, self.p2, rM @ self.normal else: return tM @ self.p0, rM @ self.p1, rM @ self.p2, rM @ self.normal def get_prefs(self, context): global __name__ global arrow_size global handle_size try: # retrieve addon name from imports addon_name = __name__.split('.')[0] prefs = context.preferences.addons[addon_name].preferences arrow_size = prefs.arrow_size handle_size = prefs.handle_size except: pass def setup(self, context, o, datablock, snap_callback=None): """ Factory return a manipulator object or None o: object datablock: datablock to modify snap_callback: function call y """ self.get_prefs(context) global manipulators_class_lookup if self.type_key not in manipulators_class_lookup.keys() or \ not manipulators_class_lookup[self.type_key].poll(context): # RuntimeError is overkill but may be enabled for debug purposes # Silently ignore allow skipping manipulators if / when deps as not meet # manip stack will simply be filled with None objects # raise RuntimeError("Manipulator of type {} not found".format(self.type_key)) return None m = manipulators_class_lookup[self.type_key](context, o, datablock, self, handle_size, snap_callback) # points storage model as described upside self.pts_mode = m.pts_mode return m # ------------------------------------------------------------------ # Define Manipulable to make a PropertyGroup manipulable # ------------------------------------------------------------------ class ARCHIPACK_OT_manipulate(Operator): bl_idname = "archipack.manipulate" bl_label = "Manipulate" bl_description = "Manipulate" bl_options = {'REGISTER', 'UNDO'} object_name : StringProperty(default="") @classmethod def poll(self, context): return context.active_object is not None def exit_selectmode(self, context, key): """ Hide select area on exit """ global manips if key in manips.keys(): if manips[key].manipulable is not None: manips[key].manipulable.manipulable_exit_selectmode(context) def modal(self, context, event): global manips # Exit on stack change # handle multiple object stack # use object_name property to find manupulated object in stack # select and make object active # and exit when not found if context.area is not None: context.area.tag_redraw() key = self.object_name if check_stack(key): self.exit_selectmode(context, key) remove_manipulable(key) # print("modal exit by check_stack(%s)" % (key)) return {'FINISHED'} res = manips[key].manipulable.manipulable_modal(context, event) if 'FINISHED' in res: self.exit_selectmode(context, key) remove_manipulable(key) # print("modal exit by {FINISHED}") return res def invoke(self, context, event): if context.space_data is not None and context.space_data.type == 'VIEW_3D': context.window_manager.modal_handler_add(self) return {'RUNNING_MODAL'} else: self.report({'WARNING'}, "Active space must be a View3d") return {'CANCELLED'} class ARCHIPACK_OT_disable_manipulate(Operator): bl_idname = "archipack.disable_manipulate" bl_label = "Disable Manipulate" bl_description = "Disable any active manipulator" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(self, context): return True def execute(self, context): empty_stack() return {'FINISHED'} class Manipulable(): """ A class extending PropertyGroup to setup gl manipulators Beware : prevent crash calling manipulable_disable() before changing manipulated data structure """ manipulators : CollectionProperty( type=archipack_manipulator, # options={'SKIP_SAVE'}, # options={'HIDDEN'}, description="store 3d points to draw gl manipulators" ) # TODO: make simple instance vars manipulable_refresh : BoolProperty( default=False, options={'SKIP_SAVE'}, description="Flag enable to rebuild manipulators when data model change" ) manipulate_mode : BoolProperty( default=False, options={'SKIP_SAVE'}, description="Flag manipulation state so we are able to toggle" ) select_mode : BoolProperty( default=False, options={'SKIP_SAVE'}, description="Flag select state so we are able to toggle" ) manipulable_selectable : BoolProperty( default=False, options={'SKIP_SAVE'}, description="Flag make manipulators selectable" ) keymap = None manipulable_area = None manipulable_start_point = None manipulable_end_point = None manipulable_draw_handler = None def setup_manipulators(self): """ Must implement manipulators creation TODO: call from update and manipulable_setup """ raise NotImplementedError def manipulable_draw_callback(self, _self, context): self.manipulable_area.draw(context) def manipulable_disable(self, context): """ disable gl draw handlers """ if self.keymap is None: self.keymap = Keymaps(context) self.manipulable_area = GlCursorArea() self.manipulable_start_point = Vector((0, 0)) self.manipulable_end_point = Vector((0, 0)) o = context.active_object if o is not None: self.manipulable_exit_selectmode(context) remove_manipulable(o.name) self.manip_stack = add_manipulable(o.name, self) self.manipulate_mode = False self.select_mode = False def manipulable_exit_selectmode(self, context): self.manipulable_area.disable() self.select_mode = False # remove select draw handler if self.manipulable_draw_handler is not None: bpy.types.SpaceView3D.draw_handler_remove( self.manipulable_draw_handler, 'WINDOW') self.manipulable_draw_handler = None def manipulable_setup(self, context): """ TODO: Implement the setup part as per parent object basis """ self.manipulable_disable(context) o = context.active_object self.setup_manipulators() for m in self.manipulators: self.manip_stack.append(m.setup(context, o, self)) def _manipulable_invoke(self, context): object_name = context.active_object.name # store a reference to self for operators add_manipulable(object_name, self) # copy context so manipulator always use # invoke time context ctx = context.copy() # take care of context switching # when call from outside of 3d view if context.space_data is not None and context.space_data.type != 'VIEW_3D': for window in bpy.context.window_manager.windows: screen = window.screen for area in screen.areas: if area.type == 'VIEW_3D': ctx['area'] = area for region in area.regions: if region.type == 'WINDOW': ctx['region'] = region break if ctx is not None: bpy.ops.archipack.manipulate(ctx, 'INVOKE_DEFAULT', object_name=object_name) def manipulable_invoke(self, context): """ call this in operator invoke() NB: if override don't forget to call: _manipulable_invoke(context) """ # print("manipulable_invoke self.manipulate_mode:%s" % (self.manipulate_mode)) if self.manipulate_mode: self.manipulable_disable(context) return False # else: # bpy.ops.archipack.disable_manipulate('INVOKE_DEFAULT') # self.manip_stack = [] # kills other's manipulators # self.manipulate_mode = True self.manipulable_setup(context) self.manipulate_mode = True self._manipulable_invoke(context) return True def manipulable_modal(self, context, event): """ call in operator modal() should not be overridden as it provide all needed functionality out of the box """ # setup again when manipulators type change if self.manipulable_refresh: # print("manipulable_refresh") self.manipulable_refresh = False self.manipulable_setup(context) self.manipulate_mode = True if context.area is None: self.manipulable_disable(context) return {'FINISHED'} context.area.tag_redraw() if self.keymap is None: self.keymap = Keymaps(context) if self.keymap.check(event, self.keymap.undo): # user feedback on undo by disabling manipulators self.manipulable_disable(context) return {'FINISHED'} # clean up manipulator on delete if self.keymap.check(event, self.keymap.delete): # {'X'}: # @TODO: # for doors and windows, seek and destroy holes object if any # a dedicated delete method into those objects may be an option ? # A type check is required any way we choose # # Time for a generic archipack's datablock getter / filter into utils # # May also be implemented into nearly hidden "reference point" # to delete / duplicate / link duplicate / unlink of # a complete set of wall, doors and windows at once self.manipulable_disable(context) if bpy.ops.object.delete.poll(): bpy.ops.object.delete('INVOKE_DEFAULT', use_global=False) return {'FINISHED'} """ # handle keyborad for select mode if self.select_mode: if event.type in {'A'} and event.value == 'RELEASE': return {'RUNNING_MODAL'} """ for manipulator in self.manip_stack: # manipulator should return false on left mouse release # so proper release handler is called # and return true to call manipulate when required # print("manipulator:%s" % manipulator) if manipulator is not None and manipulator.modal(context, event): self.manipulable_manipulate(context, event, manipulator) return {'RUNNING_MODAL'} # print("Manipulable %s %s" % (event.type, event.value)) # Manipulators are not active so check for selection if event.type == 'LEFTMOUSE': # either we are starting select mode # user press on area not over maniuplator # Prevent 3 mouse emultation to select when alt pressed if self.manipulable_selectable and event.value == 'PRESS' and not event.alt: self.select_mode = True self.manipulable_area.enable() self.manipulable_start_point = Vector((event.mouse_region_x, event.mouse_region_y)) self.manipulable_area.set_location( context, self.manipulable_start_point, self.manipulable_start_point) # add a select draw handler args = (self, context) self.manipulable_draw_handler = bpy.types.SpaceView3D.draw_handler_add( self.manipulable_draw_callback, args, 'WINDOW', 'POST_PIXEL') # don't keep focus # as this prevent click over ui # return {'RUNNING_MODAL'} elif event.value == 'RELEASE': if self.select_mode: # confirm selection self.manipulable_exit_selectmode(context) # keep focus # return {'RUNNING_MODAL'} else: # allow manipulator action on release for manipulator in self.manip_stack: if manipulator is not None and manipulator.selectable: manipulator.selected = False self.manipulable_release(context) elif self.select_mode and event.type == 'MOUSEMOVE' and event.value == 'PRESS': # update select area size self.manipulable_end_point = Vector((event.mouse_region_x, event.mouse_region_y)) self.manipulable_area.set_location( context, self.manipulable_start_point, self.manipulable_end_point) if event.shift: # deselect for i, manipulator in enumerate(self.manip_stack): if manipulator is not None and manipulator.selectable: manipulator.deselect(self.manipulable_area) else: # select / more for i, manipulator in enumerate(self.manip_stack): if manipulator is not None and manipulator.selectable: manipulator.select(self.manipulable_area) # keep focus to prevent left select mouse to actually move object return {'RUNNING_MODAL'} # event.alt here to prevent 3 button mouse emulation exit while zooming if event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS' and not event.alt: self.manipulable_disable(context) self.manipulable_exit(context) return {'FINISHED'} return {'PASS_THROUGH'} # Callbacks def manipulable_release(self, context): """ Override with action to do on mouse release eg: big update """ return def manipulable_exit(self, context): """ Override with action to do when modal exit """ return def manipulable_manipulate(self, context, event, manipulator): """ Override with action to do when a handle is active (pressed and mousemove) """ return @persistent def cleanup(dummy=None): empty_stack() def register(): # Register default manipulators global manips global manipulators_class_lookup manipulators_class_lookup = {} manips = {} register_manipulator('SIZE', SizeManipulator) register_manipulator('SIZE_LOC', SizeLocationManipulator) register_manipulator('ANGLE', AngleManipulator) register_manipulator('DUMB_ANGLE', DumbAngleManipulator) register_manipulator('ARC_ANGLE_RADIUS', ArcAngleRadiusManipulator) register_manipulator('COUNTER', CounterManipulator) register_manipulator('DUMB_SIZE', DumbSizeManipulator) register_manipulator('DELTA_LOC', DeltaLocationManipulator) register_manipulator('DUMB_STRING', DumbStringManipulator) # snap aware size loc register_manipulator('SNAP_SIZE_LOC', SnapSizeLocationManipulator) # register_manipulator('SNAP_POINT', SnapPointManipulator) # wall's line based object snap register_manipulator('WALL_SNAP', WallSnapManipulator) bpy.utils.register_class(ARCHIPACK_OT_manipulate) bpy.utils.register_class(ARCHIPACK_OT_disable_manipulate) bpy.utils.register_class(archipack_manipulator) bpy.app.handlers.load_pre.append(cleanup) def unregister(): global manips global manipulators_class_lookup empty_stack() del manips manipulators_class_lookup.clear() del manipulators_class_lookup bpy.utils.unregister_class(ARCHIPACK_OT_manipulate) bpy.utils.unregister_class(ARCHIPACK_OT_disable_manipulate) bpy.utils.unregister_class(archipack_manipulator) bpy.app.handlers.load_pre.remove(cleanup)