diff options
author | Thomas Dinges <blender@dingto.org> | 2021-11-23 11:24:55 +0300 |
---|---|---|
committer | Thomas Dinges <blender@dingto.org> | 2021-11-23 11:24:55 +0300 |
commit | d7517a6f2a69071eab53c02a645f7651ccfffd45 (patch) | |
tree | 7a496b8ada3e764b7e6c438e30d8c2be49fb95cf /archipack/archipack_manipulator.py | |
parent | 162cba016c8c11bcebea4d8d3cf80da9faf4ce76 (diff) |
Remove Archipack to reflect new key requirements.
https://wiki.blender.org/wiki/Process/Addons
Diffstat (limited to 'archipack/archipack_manipulator.py')
-rw-r--r-- | archipack/archipack_manipulator.py | 2415 |
1 files changed, 0 insertions, 2415 deletions
diff --git a/archipack/archipack_manipulator.py b/archipack/archipack_manipulator.py deleted file mode 100644 index f1d91cad..00000000 --- a/archipack/archipack_manipulator.py +++ /dev/null @@ -1,2415 +0,0 @@ -# -*- 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 ##### - -# <pep8 compliant> - -# ---------------------------------------------------------- -# 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) |