Sun position: remove unused prop in HDRI mode
[blender-addons.git] / archipack / archipack_manipulator.py
blobf1d91cad13601a3138c7df79572d627488d0362b
1 # -*- coding:utf-8 -*-
3 # ##### BEGIN GPL LICENSE BLOCK #####
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ##### END GPL LICENSE BLOCK #####
21 # <pep8 compliant>
23 # ----------------------------------------------------------
24 # Author: Stephen Leger (s-leger)
26 # ----------------------------------------------------------
27 import bpy
28 from math import atan2, pi
29 from mathutils import Vector, Matrix
30 from mathutils.geometry import intersect_line_plane, intersect_point_line, intersect_line_sphere
31 from bpy_extras import view3d_utils
32 from bpy.types import PropertyGroup, Operator
33 from bpy.props import (
34 FloatVectorProperty,
35 StringProperty,
36 CollectionProperty,
37 BoolProperty
40 from bpy.app.handlers import persistent
41 from .archipack_snap import snap_point
42 from .archipack_keymaps import Keymaps
43 from .archipack_gl import (
44 GlLine, GlArc, GlText,
45 GlPolyline, GlPolygon,
46 TriHandle, SquareHandle, EditableText,
47 FeedbackPanel, GlCursorArea
51 # NOTE:
52 # Snap aware manipulators use a dirty hack :
53 # draw() as a callback to update values in realtime
54 # as transform.translate in use to allow snap
55 # does catch all events.
56 # This however has a wanted side effect:
57 # the manipulator take precedence over already running
58 # ones, and prevent select mode to start.
60 # TODO:
61 # Other manipulators should use same technique to take
62 # precedence over already running ones when active
64 # NOTE:
65 # Select mode does suffer from this stack effect:
66 # the last running wins. The point is left mouse select mode
67 # requiring left drag to be RUNNING_MODAL to prevent real
68 # objects select and move during manipulators selection.
70 # TODO:
71 # First run a separate modal dedicated to select mode.
72 # Selecting in whole manips stack when required
73 # (manips[key].manipulable.manip_stack)
74 # Must investigate for a way to handle unselect after drag done.
77 import logging
78 logger = logging.getLogger("archipack")
81 """
82 Change object location when moving 1 point
83 When False, change data.origin instead
84 """
85 USE_MOVE_OBJECT = True
86 # Arrow sizes (world units)
87 arrow_size = 0.05
88 # Handle area size (pixels)
89 handle_size = 10
92 # a global manipulator stack reference
93 # prevent Blender "ACCESS_VIOLATION" crashes
94 # use a dict to prevent collisions
95 # between many objects being in manipulate mode
96 # use object names as loose keys
97 # NOTE : use app.drivers to reset before file load
98 manips = {}
101 class ArchipackActiveManip:
103 Store manipulated object
104 - object_name: manipulated object name
105 - stack: array of Manipulators instances
106 - manipulable: Manipulable instance
108 def __init__(self, object_name):
109 self.object_name = object_name
110 # manipulators stack for object
111 self.stack = []
112 # reference to object manipulable instance
113 self.manipulable = None
115 def is_snapping(self, ctx):
117 Check if snap is active
119 return ctx.active_object and ctx.active_object.name.startswith("Archipack_")
121 @property
122 def dirty(self):
124 Check for manipulable validity
125 to disable modal when required
127 o = bpy.data.objects.get(self.object_name)
128 return (
129 self.manipulable is None or
130 o is None or
131 # The object is not selected and snap is not active
132 not (self.is_snapping(bpy.context) or o.select_get())
135 def exit(self):
137 Exit manipulation mode
138 - exit from all running manipulators
139 - empty manipulators stack
140 - set manipulable.manipulate_mode to False
141 - remove reference to manipulable
143 for m in self.stack:
144 if m is not None:
145 m.exit()
146 if self.manipulable is not None:
147 self.manipulable.manipulate_mode = False
148 self.manipulable = None
149 self.object_name = ""
150 self.stack.clear()
153 def remove_manipulable(key):
155 disable and remove a manipulable from stack
157 global manips
158 # print("remove_manipulable key:%s" % (key))
159 if key in manips.keys():
160 manips[key].exit()
161 manips.pop(key)
164 def check_stack(key):
166 check for stack item validity
167 use in modal to destroy invalid modals
168 return true when invalid / not found
169 false when valid
171 global manips
172 if key not in manips.keys():
173 # print("check_stack : key not found %s" % (key))
174 return True
175 elif manips[key].dirty:
176 # print("check_stack : key.dirty %s" % (key))
177 remove_manipulable(key)
178 return True
179 return False
182 def empty_stack():
183 # print("empty_stack()")
185 kill every manipulators in stack
186 and cleanup stack
188 global manips
189 for key in manips.keys():
190 manips[key].exit()
191 manips.clear()
194 def add_manipulable(key, manipulable):
196 add a ArchipackActiveManip into the stack
197 if not already present
198 setup reference to manipulable
199 return manipulators stack
201 global manips
202 if key not in manips.keys():
203 # print("add_manipulable() key:%s not found create new" % (key))
204 manips[key] = ArchipackActiveManip(key)
206 manips[key].manipulable = manipulable
207 return manips[key].stack
210 # ------------------------------------------------------------------
211 # Define Manipulators
212 # ------------------------------------------------------------------
215 class Manipulator():
217 Manipulator base class to derive other
218 handle keyboard and modal events
219 provide convenient funcs including getter and setter for datablock values
220 store reference of base object, datablock and manipulator
222 keyboard_ascii = {
223 ".", ",", "-", "+", "1", "2", "3",
224 "4", "5", "6", "7", "8", "9", "0",
225 "c", "m", "d", "k", "h", "a",
226 " ", "/", "*", "'", "\""
227 # "="
229 keyboard_type = {
230 'BACK_SPACE', 'DEL',
231 'LEFT_ARROW', 'RIGHT_ARROW'
234 def __init__(self, context, o, datablock, manipulator, snap_callback=None):
236 o : object to manipulate
237 datablock : object data to manipulate
238 manipulator: object archipack_manipulator datablock
239 snap_callback: on snap enabled manipulators, will be called when drag occurs
241 self.keymap = Keymaps(context)
242 self.feedback = FeedbackPanel()
243 self.active = False
244 self.selectable = False
245 self.selected = False
246 # active text input value for manipulator
247 self.keyboard_input_active = False
248 self.label_value = 0
249 # unit for keyboard input value
250 self.value_type = 'LENGTH'
251 self.pts_mode = 'SIZE'
252 self.o = o
253 self.datablock = datablock
254 self.manipulator = manipulator
255 self.snap_callback = snap_callback
256 self.origin = Vector((0, 0, 1))
257 self.mouse_pos = Vector((0, 0))
258 self.length_entered = ""
259 self.line_pos = 0
260 args = (self, context)
261 self._handle = bpy.types.SpaceView3D.draw_handler_add(self.draw_callback, args, 'WINDOW', 'POST_PIXEL')
263 @classmethod
264 def poll(cls, context):
266 Allow manipulator enable/disable
267 in given context
268 handles will not show
270 return True
272 def exit(self):
274 Modal exit, DON'T EVEN TRY TO OVERRIDE
276 if self._handle is not None:
277 bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
278 self._handle = None
279 else:
280 print("Manipulator.exit() handle not found %s" % (type(self).__name__))
282 # Mouse event handlers, MUST be overridden
283 def mouse_press(self, context, event):
285 Manipulators must implement
286 mouse press event handler
287 return True to callback manipulable_manipulate
289 raise NotImplementedError
291 def mouse_release(self, context, event):
293 Manipulators must implement
294 mouse mouse_release event handler
295 return False to callback manipulable_release
297 raise NotImplementedError
299 def mouse_move(self, context, event):
301 Manipulators must implement
302 mouse move event handler
303 return True to callback manipulable_manipulate
305 raise NotImplementedError
307 # Keyboard event handlers, MAY be overridden
308 def keyboard_done(self, context, event, value):
310 Manipulators may implement
311 keyboard value validated event handler
312 value: changed by keyboard
313 return True to callback manipulable_manipulate
315 return False
317 def keyboard_editing(self, context, event, value):
319 Manipulators may implement
320 keyboard value changed event handler
321 value: string changed by keyboard
322 allow realtime update of label
323 return False to show edited value on window header
324 return True when feedback show right on screen
326 self.label_value = value
327 return True
329 def keyboard_cancel(self, context, event):
331 Manipulators may implement
332 keyboard entry cancelled
334 return
336 def cancel(self, context, event):
338 Manipulators may implement
339 cancelled event (ESC RIGHTCLICK)
341 self.active = False
342 return
344 def undo(self, context, event):
346 Manipulators may implement
347 undo event (CTRL+Z)
349 return False
351 # Internal, do not override unless you really
352 # really really deeply know what you are doing
353 def keyboard_eval(self, context, event):
355 evaluate keyboard entry while typing
356 do not override this one
358 c = event.ascii
359 if c:
360 if c == ",":
361 c = "."
362 self.length_entered = self.length_entered[:self.line_pos] + c + self.length_entered[self.line_pos:]
363 self.line_pos += 1
365 if self.length_entered:
366 if event.type == 'BACK_SPACE':
367 self.length_entered = self.length_entered[:self.line_pos - 1] + self.length_entered[self.line_pos:]
368 self.line_pos -= 1
370 elif event.type == 'DEL':
371 self.length_entered = self.length_entered[:self.line_pos] + self.length_entered[self.line_pos + 1:]
373 elif event.type == 'LEFT_ARROW':
374 self.line_pos = (self.line_pos - 1) % (len(self.length_entered) + 1)
376 elif event.type == 'RIGHT_ARROW':
377 self.line_pos = (self.line_pos + 1) % (len(self.length_entered) + 1)
379 try:
380 value = bpy.utils.units.to_value(context.scene.unit_settings.system, self.value_type, self.length_entered)
381 draw_on_header = self.keyboard_editing(context, event, value)
382 except: # ValueError:
383 draw_on_header = True
384 pass
386 if draw_on_header:
387 a = ""
388 if self.length_entered:
389 pos = self.line_pos
390 a = self.length_entered[:pos] + '|' + self.length_entered[pos:]
391 context.area.header_text_set("%s" % (a))
393 # modal mode: do not let event bubble up
394 return True
396 def modal(self, context, event):
398 Modal handler
399 handle mouse, and keyboard events
400 enable and disable feedback
402 # print("Manipulator modal:%s %s" % (event.value, event.type))
404 if event.type == 'MOUSEMOVE':
405 return self.mouse_move(context, event)
407 elif event.value == 'PRESS':
409 if event.type == 'LEFTMOUSE':
410 active = self.mouse_press(context, event)
411 if active:
412 self.feedback.enable()
413 return active
415 elif self.keymap.check(event, self.keymap.undo):
416 if self.keyboard_input_active:
417 self.keyboard_input_active = False
418 self.keyboard_cancel(context, event)
419 self.feedback.disable()
420 # prevent undo CRASH
421 return True
423 elif self.keyboard_input_active and (
424 event.ascii in self.keyboard_ascii or
425 event.type in self.keyboard_type
427 # get keyboard input
428 return self.keyboard_eval(context, event)
430 elif event.type in {'ESC', 'RIGHTMOUSE'}:
431 self.feedback.disable()
432 if self.keyboard_input_active:
433 # allow keyboard exit without setting value
434 self.length_entered = ""
435 self.line_pos = 0
436 self.keyboard_input_active = False
437 self.keyboard_cancel(context, event)
438 return True
439 elif self.active:
440 self.cancel(context, event)
441 return True
442 return False
444 elif event.value == 'RELEASE':
446 if event.type == 'LEFTMOUSE':
447 if not self.keyboard_input_active:
448 self.feedback.disable()
449 return self.mouse_release(context, event)
451 elif self.keyboard_input_active and event.type in {'RET', 'NUMPAD_ENTER'}:
452 # validate keyboard input
453 if self.length_entered != "":
454 try:
455 value = bpy.utils.units.to_value(
456 context.scene.unit_settings.system,
457 self.value_type, self.length_entered)
458 self.length_entered = ""
459 ret = self.keyboard_done(context, event, value)
460 except: # ValueError:
461 ret = False
462 self.keyboard_cancel(context, event)
463 pass
464 context.area.header_text_set(None)
465 self.keyboard_input_active = False
466 self.feedback.disable()
467 return ret
469 return False
471 def mouse_position(self, event):
473 store mouse position in a 2d Vector
475 self.mouse_pos.x, self.mouse_pos.y = event.mouse_region_x, event.mouse_region_y
477 def get_pos3d(self, context):
479 convert mouse pos to 3d point over plane defined by origin and normal
480 pt is in world space
482 region = context.region
483 rv3d = context.region_data
484 rM = context.active_object.matrix_world.to_3x3()
485 view_vector_mouse = view3d_utils.region_2d_to_vector_3d(region, rv3d, self.mouse_pos)
486 ray_origin_mouse = view3d_utils.region_2d_to_origin_3d(region, rv3d, self.mouse_pos)
487 pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse,
488 self.origin, rM @ self.manipulator.normal, False)
489 # fix issue with parallel plane
490 if pt is None:
491 pt = intersect_line_plane(ray_origin_mouse, ray_origin_mouse + view_vector_mouse,
492 self.origin, view_vector_mouse, False)
493 return pt
495 def get_value(self, data, attr, index=-1):
497 Datablock value getter with index support
499 try:
500 if index > -1:
501 return getattr(data, attr)[index]
502 else:
503 return getattr(data, attr)
504 except:
505 print("get_value of %s %s failed" % (data, attr))
506 return 0
508 def set_value(self, context, data, attr, value, index=-1):
510 Datablock value setter with index support
512 try:
513 if self.get_value(data, attr, index) != value:
514 o = self.o
515 # switch context so unselected object may be manipulable too
516 old = context.object
517 state = o.select_get()
518 o.select_set(state=True)
519 context.view_layer.objects.active = o
520 if index > -1:
521 getattr(data, attr)[index] = value
522 else:
523 setattr(data, attr, value)
524 o.select_set(state=state)
525 old.select_set(state=True)
526 context.view_layer.objects.active = old
527 except:
528 pass
530 def preTranslate(self, tM, vec):
532 return a preTranslated Matrix
533 tM Matrix source
534 vec Vector translation
536 return tM @ Matrix.Translation(vec)
538 def _move(self, o, axis, value):
539 if axis == 'x':
540 vec = Vector((value, 0, 0))
541 elif axis == 'y':
542 vec = Vector((0, value, 0))
543 else:
544 vec = Vector((0, 0, value))
545 o.matrix_world = self.preTranslate(o.matrix_world, vec)
547 def move_linked(self, context, axis, value):
549 Move an object along local axis
550 takes care of linked too, fix issue #8
552 old = context.active_object
553 bpy.ops.object.select_all(action='DESELECT')
554 self.o.select_set(state=True)
555 context.view_layer.objects.active = self.o
556 bpy.ops.object.select_linked(type='OBDATA')
557 for o in context.selected_objects:
558 if o != self.o:
559 self._move(o, axis, value)
560 bpy.ops.object.select_all(action='DESELECT')
561 old.select_set(state=True)
562 context.view_layer.objects.active = old
564 def move(self, context, axis, value):
566 Move an object along local axis
568 self._move(self.o, axis, value)
571 # Generic snap tool for line based archipack objects (fence, wall, maybe stair too)
572 gl_pts3d = []
575 class WallSnapManipulator(Manipulator):
577 np_station snap inspired manipulator
578 Use prop1_name as string part index
579 Use prop2_name as string identifier height property for placeholders
581 Misnamed as it work for all line based archipack's
582 primitives, currently wall and fences,
583 but may also work with stairs (sharing same data structure)
585 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
586 self.placeholder_area = GlPolygon((0.5, 0, 0, 0.2))
587 self.placeholder_line = GlPolyline((0.5, 0, 0, 0.8))
588 self.placeholder_line.closed = True
589 self.label = GlText()
590 self.line = GlLine()
591 self.handle = SquareHandle(handle_size, 1.2 * arrow_size, draggable=True, selectable=True)
592 Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback)
593 self.selectable = True
595 def select(self, cursor_area):
596 self.selected = self.selected or cursor_area.in_area(self.handle.pos_2d)
597 self.handle.selected = self.selected
599 def deselect(self, cursor_area):
600 self.selected = not cursor_area.in_area(self.handle.pos_2d)
601 self.handle.selected = self.selected
603 def check_hover(self):
604 self.handle.check_hover(self.mouse_pos)
606 def mouse_press(self, context, event):
607 global gl_pts3d
608 global manips
609 if self.handle.hover:
610 self.active = True
611 self.handle.active = True
612 gl_pts3d = []
613 idx = int(self.manipulator.prop1_name)
615 # get selected manipulators idx
616 selection = []
617 for m in manips[self.o.name].stack:
618 if m is not None and m.selected:
619 selection.append(int(m.manipulator.prop1_name))
621 # store all points of wall
622 for i, part in enumerate(self.datablock.parts):
623 p0, p1, side, normal = part.manipulators[2].get_pts(self.o.matrix_world)
624 # if selected p0 will move and require placeholder
625 gl_pts3d.append((p0, p1, i in selection or i == idx))
627 self.feedback.instructions(context, "Move / Snap", "Drag to move, use keyboard to input values", [
628 ('CTRL', 'Snap'),
629 ('X Y', 'Constraint to axis (toggle Global Local None)'),
630 ('SHIFT+Z', 'Constraint to xy plane'),
631 ('MMBTN', 'Constraint to axis'),
632 ('RIGHTCLICK or ESC', 'exit without change')
634 self.feedback.enable()
635 self.handle.hover = False
636 self.o.select_set(state=True)
637 takeloc, right, side, dz = self.manipulator.get_pts(self.o.matrix_world)
638 dx = (right - takeloc).normalized()
639 dy = dz.cross(dx)
640 takemat = Matrix([
641 [dx.x, dy.x, dz.x, takeloc.x],
642 [dx.y, dy.y, dz.y, takeloc.y],
643 [dx.z, dy.z, dz.z, takeloc.z],
644 [0, 0, 0, 1]
646 snap_point(takemat=takemat, draw=self.sp_draw, callback=self.sp_callback,
647 constraint_axis=(True, True, False))
648 # this prevent other selected to run
649 return True
651 return False
653 def mouse_release(self, context, event):
654 self.check_hover()
655 self.handle.active = False
656 self.active = False
657 self.feedback.disable()
658 # False to callback manipulable_release
659 return False
661 def sp_callback(self, context, event, state, sp):
663 np station callback on moving, place, or cancel
665 global gl_pts3d
666 logger.debug("WallSnapManipulator.sp_callback")
668 if state == 'SUCCESS':
669 o = self.o
670 o.select_set(state=True)
671 context.view_layer.objects.active = o
672 # apply changes to wall
673 d = self.datablock
674 g = d.get_generator()
676 # rotation relative to object
677 rM = o.matrix_world.inverted().to_3x3()
678 delta =rM @ sp.delta
679 # x_axis = (rM @ Vector((1, 0, 0))).to_2d()
681 # update generator
682 idx = 0
683 for p0, p1, selected in gl_pts3d:
685 if selected:
687 # new location in object space
688 pt = g.segs[idx].lerp(0) + delta.to_2d()
690 # move last point of segment before current
691 if idx > 0:
692 g.segs[idx - 1].p1 = pt
694 # move first point of current segment
695 g.segs[idx].p0 = pt
697 idx += 1
699 # update properties from generator
700 idx = 0
701 d.auto_update = False
702 for p0, p1, selected in gl_pts3d:
704 if selected:
706 # adjust segment before current
707 if idx > 0:
708 w = g.segs[idx - 1]
709 part = d.parts[idx - 1]
711 if idx > 1:
712 part.a0 = w.delta_angle(g.segs[idx - 2])
713 else:
714 part.a0 = w.a0
716 if "C_" in part.type:
717 part.radius = w.r
718 else:
719 part.length = w.length
721 # adjust current segment
722 w = g.segs[idx]
723 part = d.parts[idx]
725 if idx > 0:
726 part.a0 = w.delta_angle(g.segs[idx - 1])
727 else:
728 part.a0 = w.a0
729 # move object when point 0
730 if USE_MOVE_OBJECT:
731 d.move_object(o, o.matrix_world.translation + sp.delta)
732 # self.o.location += sp.delta
733 # self.o.matrix_world.translation += sp.delta
734 else:
735 d.origin += sp.delta
737 if "C_" in part.type:
738 part.radius = w.r
739 else:
740 part.length = w.length
742 # adjust next one
743 if idx + 1 < d.n_parts:
744 d.parts[idx + 1].a0 = g.segs[idx + 1].delta_angle(w)
746 idx += 1
748 self.mouse_release(context, event)
749 if hasattr(d, "relocate_childs"):
750 d.relocate_childs(context, o, g)
751 d.auto_update = True
752 d.update(context)
754 if state == 'CANCEL':
755 self.mouse_release(context, event)
756 logger.debug("WallSnapManipulator.sp_callback done")
758 return
760 def sp_draw(self, sp, context):
761 # draw wall placeholders
762 logger.debug("WallSnapManipulator.sp_draw")
764 global gl_pts3d
766 if self.o is None:
767 return
769 z = self.get_value(self.datablock, self.manipulator.prop2_name)
771 placeholders = []
772 for p0, p1, selected in gl_pts3d:
773 pt = p0.copy()
774 if selected:
775 # when selected, p0 is moving
776 # last one p1 should move too
777 # last one require a placeholder too
778 pt += sp.delta
779 if len(placeholders) > 0:
780 placeholders[-1][1] = pt
781 placeholders[-1][2] = True
782 placeholders.append([pt, p1, selected])
784 # first selected and closed -> should move last p1 too
785 if gl_pts3d[0][2] and self.datablock.closed:
786 placeholders[-1][1] = placeholders[0][0].copy()
787 placeholders[-1][2] = True
789 # last one not visible when not closed
790 if not self.datablock.closed:
791 placeholders[-1][2] = False
793 for p0, p1, selected in placeholders:
794 if selected:
795 self.placeholder_area.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))])
796 self.placeholder_line.set_pos([p0, p1, Vector((p1.x, p1.y, p1.z + z)), Vector((p0.x, p0.y, p0.z + z))])
797 self.placeholder_area.draw(context, render=False)
798 self.placeholder_line.draw(context, render=False)
800 p0, p1, side, normal = self.manipulator.get_pts(self.o.matrix_world)
801 self.line.p = p0
802 self.line.v = sp.delta
803 self.label.set_pos(context, self.line.length, self.line.lerp(0.5), self.line.v, normal=Vector((0, 0, 1)))
804 self.line.draw(context, render=False)
805 self.label.draw(context, render=False)
806 logger.debug("WallSnapManipulator.sp_draw done")
808 def mouse_move(self, context, event):
809 self.mouse_position(event)
810 if self.handle.active:
811 # False here to pass_through
812 # print("i'm able to pick up mouse move event while transform running")
813 return False
814 else:
815 self.check_hover()
816 return False
818 def draw_callback(self, _self, context, render=False):
819 left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world)
820 self.handle.set_pos(context, left, (left - right).normalized(), normal=normal)
821 self.handle.draw(context, render)
822 self.feedback.draw(context, render)
825 class CounterManipulator(Manipulator):
827 increase or decrease an integer step by step
828 right on click to prevent misuse
830 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
831 self.handle_left = TriHandle(handle_size, arrow_size, draggable=True)
832 self.handle_right = TriHandle(handle_size, arrow_size, draggable=True)
833 self.line_0 = GlLine()
834 self.label = GlText()
835 self.label.unit_mode = 'NONE'
836 self.label.precision = 0
837 Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback)
839 def check_hover(self):
840 self.handle_right.check_hover(self.mouse_pos)
841 self.handle_left.check_hover(self.mouse_pos)
843 def mouse_press(self, context, event):
844 if self.handle_right.hover:
845 value = self.get_value(self.datablock, self.manipulator.prop1_name)
846 self.set_value(context, self.datablock, self.manipulator.prop1_name, value + 1)
847 self.handle_right.active = True
848 return True
849 if self.handle_left.hover:
850 value = self.get_value(self.datablock, self.manipulator.prop1_name)
851 self.set_value(context, self.datablock, self.manipulator.prop1_name, value - 1)
852 self.handle_left.active = True
853 return True
854 return False
856 def mouse_release(self, context, event):
857 self.check_hover()
858 self.handle_right.active = False
859 self.handle_left.active = False
860 return False
862 def mouse_move(self, context, event):
863 self.mouse_position(event)
864 if self.handle_right.active:
865 return True
866 if self.handle_left.active:
867 return True
868 else:
869 self.check_hover()
870 return False
872 def draw_callback(self, _self, context, render=False):
874 draw on screen feedback using gl.
876 # won't render counter
877 if render:
878 return
879 left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world)
880 self.origin = left
881 self.line_0.p = left
882 self.line_0.v = right - left
883 self.line_0.z_axis = normal
884 self.label.z_axis = normal
885 value = self.get_value(self.datablock, self.manipulator.prop1_name)
886 self.handle_left.set_pos(context, self.line_0.p, -self.line_0.v, normal=normal)
887 self.handle_right.set_pos(context, self.line_0.lerp(1), self.line_0.v, normal=normal)
888 self.label.set_pos(context, value, self.line_0.lerp(0.5), self.line_0.v, normal=normal)
889 self.label.draw(context, render)
890 self.handle_left.draw(context, render)
891 self.handle_right.draw(context, render)
894 class DumbStringManipulator(Manipulator):
896 not a real manipulator, but allow to show a string
898 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
899 self.label = GlText(colour=(0, 0, 0, 1))
900 self.label.unit_mode = 'NONE'
901 self.label.label = manipulator.prop1_name
902 Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback)
904 def check_hover(self):
905 return False
907 def mouse_press(self, context, event):
908 return False
910 def mouse_release(self, context, event):
911 return False
913 def mouse_move(self, context, event):
914 return False
916 def draw_callback(self, _self, context, render=False):
918 draw on screen feedback using gl.
920 # won't render string
921 if render:
922 return
923 left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world)
924 pos = left + 0.5 * (right - left)
925 self.label.set_pos(context, None, pos, pos, normal=normal)
926 self.label.draw(context, render)
929 class SizeManipulator(Manipulator):
931 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
932 self.handle_left = TriHandle(handle_size, arrow_size)
933 self.handle_right = TriHandle(handle_size, arrow_size, draggable=True)
934 self.line_0 = GlLine()
935 self.line_1 = GlLine()
936 self.line_2 = GlLine()
937 self.label = EditableText(handle_size, arrow_size, draggable=True)
938 # self.label.label = 'S '
939 Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback)
941 def check_hover(self):
942 self.handle_right.check_hover(self.mouse_pos)
943 self.label.check_hover(self.mouse_pos)
945 def mouse_press(self, context, event):
946 global gl_pts3d
947 if self.handle_right.hover:
948 self.active = True
949 self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name)
950 self.original_location = self.o.matrix_world.translation.copy()
951 self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [
952 ('CTRL', 'Snap'),
953 ('SHIFT', 'Round'),
954 ('RIGHTCLICK or ESC', 'cancel')
956 left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world)
957 dx = (right - left).normalized()
958 dy = dz.cross(dx)
959 takemat = Matrix([
960 [dx.x, dy.x, dz.x, right.x],
961 [dx.y, dy.y, dz.y, right.y],
962 [dx.z, dy.z, dz.z, right.z],
963 [0, 0, 0, 1]
965 gl_pts3d = [left, right]
966 snap_point(takemat=takemat,
967 callback=self.sp_callback,
968 constraint_axis=(True, False, False))
969 self.handle_right.active = True
970 return True
971 if self.label.hover:
972 self.feedback.instructions(context, "Size", "Use keyboard to modify size",
973 [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')])
974 self.label.active = True
975 self.keyboard_input_active = True
976 return True
977 return False
979 def mouse_release(self, context, event):
980 self.active = False
981 self.check_hover()
982 self.handle_right.active = False
983 if not self.keyboard_input_active:
984 self.feedback.disable()
985 return False
987 def mouse_move(self, context, event):
988 self.mouse_position(event)
989 if self.active:
990 self.update(context, event)
991 return True
992 else:
993 self.check_hover()
994 return False
996 def cancel(self, context, event):
997 if self.active:
998 self.mouse_release(context, event)
999 self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_size)
1001 def keyboard_done(self, context, event, value):
1002 self.set_value(context, self.datablock, self.manipulator.prop1_name, value)
1003 self.label.active = False
1004 return True
1006 def keyboard_cancel(self, context, event):
1007 self.label.active = False
1008 return False
1010 def update(self, context, event):
1011 # 0 1 2
1012 # |_____|
1014 pt = self.get_pos3d(context)
1015 pt, t = intersect_point_line(pt, self.line_0.p, self.line_2.p)
1016 length = (self.line_0.p - pt).length
1017 if event.alt:
1018 length = round(length, 1)
1019 self.set_value(context, self.datablock, self.manipulator.prop1_name, length)
1021 def draw_callback(self, _self, context, render=False):
1023 draw on screen feedback using gl.
1025 left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world)
1026 self.origin = left
1027 self.line_1.p = left
1028 self.line_1.v = right - left
1029 self.line_0.z_axis = normal
1030 self.line_1.z_axis = normal
1031 self.line_2.z_axis = normal
1032 self.label.z_axis = normal
1033 self.line_0 = self.line_1.sized_normal(0, side.x * 1.1)
1034 self.line_2 = self.line_1.sized_normal(1, side.x * 1.1)
1035 self.line_1.offset(side.x * 1.0)
1036 self.handle_left.set_pos(context, self.line_1.p, -self.line_1.v, normal=normal)
1037 self.handle_right.set_pos(context, self.line_1.lerp(1), self.line_1.v, normal=normal)
1038 if not self.keyboard_input_active:
1039 self.label_value = self.line_1.length
1040 self.label.set_pos(context, self.label_value, self.line_1.lerp(0.5), self.line_1.v, normal=normal)
1041 self.line_0.draw(context, render)
1042 self.line_1.draw(context, render)
1043 self.line_2.draw(context, render)
1044 self.handle_left.draw(context, render)
1045 self.handle_right.draw(context, render)
1046 self.label.draw(context, render)
1047 self.feedback.draw(context, render)
1049 def sp_callback(self, context, event, state, sp):
1050 logger.debug("SizeManipulator.sp_callback")
1051 global gl_pts3d
1053 p0 = gl_pts3d[0].copy()
1054 p1 = gl_pts3d[1].copy()
1056 if state != 'CANCEL':
1057 p1 += sp.delta
1059 length = (p0 - p1).length
1061 if state != 'CANCEL' and event.alt:
1062 if event.shift:
1063 length = round(length, 2)
1064 else:
1065 length = round(length, 1)
1067 self.set_value(context, self.datablock, self.manipulator.prop1_name, length)
1069 if state != 'RUNNING':
1070 self.mouse_release(context, event)
1072 logger.debug("SizeManipulator.sp_callback done")
1075 class SizeLocationManipulator(SizeManipulator):
1077 Handle resizing by any of the boundaries
1078 of objects with centered pivots
1079 so when size change, object should move of the
1080 half of the change in the direction of change.
1082 Also take care of moving linked objects too
1083 Changing size is not necessary as link does
1084 already handle this and childs panels are
1085 updated by base object.
1087 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
1088 SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback)
1089 self.handle_left.draggable = True
1091 def check_hover(self):
1092 self.handle_right.check_hover(self.mouse_pos)
1093 self.handle_left.check_hover(self.mouse_pos)
1094 self.label.check_hover(self.mouse_pos)
1096 def mouse_press(self, context, event):
1097 if self.handle_right.hover:
1098 self.active = True
1099 self.original_location = self.o.matrix_world.translation.copy()
1100 self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name)
1101 self.feedback.instructions(context, "Size", "Drag to modify size", [
1102 ('ALT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel')
1104 self.handle_right.active = True
1105 return True
1106 if self.handle_left.hover:
1107 self.active = True
1108 self.original_location = self.o.matrix_world.translation.copy()
1109 self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name)
1110 self.feedback.instructions(context, "Size", "Drag to modify size", [
1111 ('ALT', 'Round value'), ('RIGHTCLICK or ESC', 'cancel')
1113 self.handle_left.active = True
1114 return True
1115 if self.label.hover:
1116 self.feedback.instructions(context, "Size", "Use keyboard to modify size",
1117 [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')])
1118 self.label.active = True
1119 self.keyboard_input_active = True
1120 return True
1121 return False
1123 def mouse_release(self, context, event):
1124 self.active = False
1125 self.check_hover()
1126 self.handle_right.active = False
1127 self.handle_left.active = False
1128 if not self.keyboard_input_active:
1129 self.feedback.disable()
1130 return False
1132 def mouse_move(self, context, event):
1133 self.mouse_position(event)
1134 if self.handle_right.active or self.handle_left.active:
1135 self.update(context, event)
1136 return True
1137 else:
1138 self.check_hover()
1139 return False
1141 def keyboard_done(self, context, event, value):
1142 self.set_value(context, self.datablock, self.manipulator.prop1_name, value)
1143 # self.move_linked(context, self.manipulator.prop2_name, dl)
1144 self.label.active = False
1145 self.feedback.disable()
1146 return True
1148 def cancel(self, context, event):
1149 if self.active:
1150 self.mouse_release(context, event)
1151 # must move back to original location
1152 itM = self.o.matrix_world.inverted()
1153 dl = self.get_value(itM @ self.original_location, self.manipulator.prop2_name)
1155 self.move(context, self.manipulator.prop2_name, dl)
1156 self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_size)
1157 self.move_linked(context, self.manipulator.prop2_name, dl)
1159 def update(self, context, event):
1160 # 0 1 2
1161 # |_____|
1163 pt = self.get_pos3d(context)
1164 pt, t = intersect_point_line(pt, self.line_0.p, self.line_2.p)
1166 len_0 = (pt - self.line_0.p).length
1167 len_1 = (pt - self.line_2.p).length
1169 length = max(len_0, len_1)
1171 if event.alt:
1172 length = round(length, 1)
1174 dl = length - self.line_1.length
1176 if len_0 > len_1:
1177 dl = 0.5 * dl
1178 else:
1179 dl = -0.5 * dl
1181 self.move(context, self.manipulator.prop2_name, dl)
1182 self.set_value(context, self.datablock, self.manipulator.prop1_name, length)
1183 self.move_linked(context, self.manipulator.prop2_name, dl)
1186 class SnapSizeLocationManipulator(SizeLocationManipulator):
1188 Snap aware extension of SizeLocationManipulator
1189 Handle resizing by any of the boundaries
1190 of objects with centered pivots
1191 so when size change, object should move of the
1192 half of the change in the direction of change.
1194 Also take care of moving linked objects too
1195 Changing size is not necessary as link does
1196 already handle this and childs panels are
1197 updated by base object.
1201 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
1202 SizeLocationManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback)
1204 def mouse_press(self, context, event):
1205 global gl_pts3d
1206 if self.handle_right.hover:
1207 self.active = True
1208 self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name)
1209 self.original_location = self.o.matrix_world.translation.copy()
1210 self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [
1211 ('CTRL', 'Snap'),
1212 ('SHIFT', 'Round'),
1213 ('RIGHTCLICK or ESC', 'cancel')
1215 left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world)
1216 dx = (right - left).normalized()
1217 dy = dz.cross(dx)
1218 takemat = Matrix([
1219 [dx.x, dy.x, dz.x, right.x],
1220 [dx.y, dy.y, dz.y, right.y],
1221 [dx.z, dy.z, dz.z, right.z],
1222 [0, 0, 0, 1]
1224 gl_pts3d = [left, right]
1225 snap_point(takemat=takemat,
1226 callback=self.sp_callback,
1227 constraint_axis=(True, False, False))
1229 self.handle_right.active = True
1230 return True
1232 if self.handle_left.hover:
1233 self.active = True
1234 self.original_size = self.get_value(self.datablock, self.manipulator.prop1_name)
1235 self.original_location = self.o.matrix_world.translation.copy()
1236 self.feedback.instructions(context, "Size", "Drag or Keyboard to modify size", [
1237 ('CTRL', 'Snap'),
1238 ('SHIFT', 'Round'),
1239 ('RIGHTCLICK or ESC', 'cancel')
1241 left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world)
1242 dx = (left - right).normalized()
1243 dy = dz.cross(dx)
1244 takemat = Matrix([
1245 [dx.x, dy.x, dz.x, left.x],
1246 [dx.y, dy.y, dz.y, left.y],
1247 [dx.z, dy.z, dz.z, left.z],
1248 [0, 0, 0, 1]
1250 gl_pts3d = [left, right]
1251 snap_point(takemat=takemat,
1252 callback=self.sp_callback,
1253 constraint_axis=(True, False, False))
1254 self.handle_left.active = True
1255 return True
1257 if self.label.hover:
1258 self.feedback.instructions(context, "Size", "Use keyboard to modify size",
1259 [('ENTER', 'Validate'), ('RIGHTCLICK or ESC', 'cancel')])
1260 self.label.active = True
1261 self.keyboard_input_active = True
1262 return True
1264 return False
1266 def sp_callback(self, context, event, state, sp):
1267 logger.debug("SnapSizeLocationManipulator.sp_callback")
1268 global gl_pts3d
1269 p0 = gl_pts3d[0].copy()
1270 p1 = gl_pts3d[1].copy()
1272 if state != 'CANCEL':
1273 if self.handle_right.active:
1274 p1 += sp.delta
1275 else:
1276 p0 += sp.delta
1278 l0 = self.get_value(self.datablock, self.manipulator.prop1_name)
1279 length = (p0 - p1).length
1281 if state != 'CANCEL' and event.alt:
1282 if event.shift:
1283 length = round(length, 2)
1284 else:
1285 length = round(length, 1)
1287 dp = length - l0
1289 if self.handle_left.active:
1290 dp = -dp
1291 dl = 0.5 * dp
1293 # snap_helper = context.object
1294 self.move(context, self.manipulator.prop2_name, dl)
1295 self.set_value(context, self.datablock, self.manipulator.prop1_name, length)
1296 self.move_linked(context, self.manipulator.prop2_name, dl)
1298 # snapping child objects may require base object update
1299 # eg manipulating windows requiring wall update
1300 if self.snap_callback is not None:
1301 snap_helper = context.active_object
1302 self.snap_callback(context, o=self.o, manipulator=self)
1303 snap_helper.select_set(state=True)
1305 if state != 'RUNNING':
1306 self.mouse_release(context, event)
1308 logger.debug("SnapSizeLocationManipulator.sp_callback done")
1311 class DeltaLocationManipulator(SizeManipulator):
1313 Move a child window or door in wall segment
1314 not limited to this by the way
1316 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
1317 SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback)
1318 self.label.label = ''
1319 self.feedback.instructions(context, "Move", "Drag to move", [
1320 ('CTRL', 'Snap'),
1321 ('SHIFT', 'Round value'),
1322 ('RIGHTCLICK or ESC', 'cancel')
1325 def check_hover(self):
1326 self.handle_right.check_hover(self.mouse_pos)
1328 def mouse_press(self, context, event):
1329 global gl_pts3d
1330 if self.handle_right.hover:
1331 self.original_location = self.o.matrix_world.translation.copy()
1332 self.active = True
1333 self.feedback.enable()
1334 self.handle_right.active = True
1336 left, right, side, dz = self.manipulator.get_pts(self.o.matrix_world)
1337 dp = (right - left)
1338 dx = dp.normalized()
1339 dy = dz.cross(dx)
1340 p0 = left + 0.5 * dp
1341 takemat = Matrix([
1342 [dx.x, dy.x, dz.x, p0.x],
1343 [dx.y, dy.y, dz.y, p0.y],
1344 [dx.z, dy.z, dz.z, p0.z],
1345 [0, 0, 0, 1]
1347 gl_pts3d = [p0]
1348 snap_point(takemat=takemat,
1349 callback=self.sp_callback,
1350 constraint_axis=(
1351 self.manipulator.prop1_name == 'x',
1352 self.manipulator.prop1_name == 'y',
1353 self.manipulator.prop1_name == 'z'))
1354 return True
1355 return False
1357 def mouse_release(self, context, event):
1358 self.check_hover()
1359 self.feedback.disable()
1360 self.active = False
1361 self.handle_right.active = False
1362 return False
1364 def mouse_move(self, context, event):
1365 self.mouse_position(event)
1366 if self.handle_right.active:
1367 # self.update(context, event)
1368 return True
1369 else:
1370 self.check_hover()
1371 return False
1373 def sp_callback(self, context, event, state, sp):
1374 logger.debug("DeltaLocationManipulator.sp_callback")
1376 if state == 'CANCEL':
1377 self.cancel(context, event)
1378 else:
1379 global gl_pts3d
1380 p0 = gl_pts3d[0].copy()
1381 p1 = p0 + sp.delta
1382 itM = self.o.matrix_world.inverted()
1383 dl = self.get_value(itM @ p1, self.manipulator.prop1_name)
1384 self.move(context, self.manipulator.prop1_name, dl)
1386 # snapping child objects may require base object update
1387 # eg manipulating windows requiring wall update
1388 if self.snap_callback is not None:
1389 snap_helper = context.active_object
1390 self.snap_callback(context, o=self.o, manipulator=self)
1391 snap_helper.select_set(state=True)
1393 if state == 'SUCCESS':
1394 self.mouse_release(context, event)
1396 logger.debug("DeltaLocationManipulator.sp_callback done")
1398 def cancel(self, context, event):
1399 if self.active:
1400 self.mouse_release(context, event)
1401 # must move back to original location
1402 itM = self.o.matrix_world.inverted()
1403 dl = self.get_value(itM @ self.original_location, self.manipulator.prop1_name)
1404 self.move(context, self.manipulator.prop1_name, dl)
1406 def draw_callback(self, _self, context, render=False):
1408 draw on screen feedback using gl.
1410 left, right, side, normal = self.manipulator.get_pts(self.o.matrix_world)
1411 self.origin = left
1412 self.line_1.p = left
1413 self.line_1.v = right - left
1414 self.line_1.z_axis = normal
1415 self.handle_left.set_pos(context, self.line_1.lerp(0.5), -self.line_1.v, normal=normal)
1416 self.handle_right.set_pos(context, self.line_1.lerp(0.5), self.line_1.v, normal=normal)
1417 self.handle_left.draw(context, render)
1418 self.handle_right.draw(context, render)
1419 self.feedback.draw(context)
1422 class DumbSizeManipulator(SizeManipulator):
1424 Show a size while not being editable
1426 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
1427 SizeManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback)
1428 self.handle_right.draggable = False
1429 self.label.draggable = False
1430 self.label.colour_inactive = (0, 0, 0, 1)
1431 # self.label.label = 'Dumb '
1433 def mouse_move(self, context, event):
1434 return False
1437 class AngleManipulator(Manipulator):
1439 NOTE:
1440 There is a default shortcut to +5 and -5 on angles with left/right arrows
1442 Manipulate angle between segments
1443 bound to [-pi, pi]
1446 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
1447 # Angle
1448 self.handle_right = TriHandle(handle_size, arrow_size, draggable=True)
1449 self.handle_center = SquareHandle(handle_size, arrow_size)
1450 self.arc = GlArc()
1451 self.line_0 = GlLine()
1452 self.line_1 = GlLine()
1453 self.label_a = EditableText(handle_size, arrow_size, draggable=True)
1454 self.label_a.unit_type = 'ANGLE'
1455 Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback)
1456 self.pts_mode = 'RADIUS'
1458 def check_hover(self):
1459 self.handle_right.check_hover(self.mouse_pos)
1460 self.label_a.check_hover(self.mouse_pos)
1462 def mouse_press(self, context, event):
1463 if self.handle_right.hover:
1464 self.active = True
1465 self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name)
1466 self.feedback.instructions(context, "Angle", "Drag to modify angle", [
1467 ('SHIFT', 'Round value'),
1468 ('RIGHTCLICK or ESC', 'cancel')
1470 self.handle_right.active = True
1471 return True
1472 if self.label_a.hover:
1473 self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle",
1474 [('ENTER', 'validate'),
1475 ('RIGHTCLICK or ESC', 'cancel')])
1476 self.value_type = 'ROTATION'
1477 self.label_a.active = True
1478 self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name)
1479 self.keyboard_input_active = True
1480 return True
1481 return False
1483 def mouse_release(self, context, event):
1484 self.check_hover()
1485 self.handle_right.active = False
1486 self.active = False
1487 return False
1489 def mouse_move(self, context, event):
1490 self.mouse_position(event)
1491 if self.active:
1492 # print("AngleManipulator.mouse_move")
1493 self.update(context, event)
1494 return True
1495 else:
1496 self.check_hover()
1497 return False
1499 def keyboard_done(self, context, event, value):
1500 self.set_value(context, self.datablock, self.manipulator.prop1_name, value)
1501 self.label_a.active = False
1502 return True
1504 def keyboard_cancel(self, context, event):
1505 self.label_a.active = False
1506 return False
1508 def cancel(self, context, event):
1509 if self.active:
1510 self.mouse_release(context, event)
1511 self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle)
1513 def update(self, context, event):
1514 pt = self.get_pos3d(context)
1515 c = self.arc.c
1516 v = 2 * self.arc.r * (pt - c).normalized()
1517 v0 = c - v
1518 v1 = c + v
1519 p0, p1 = intersect_line_sphere(v0, v1, c, self.arc.r)
1520 if p0 is not None and p1 is not None:
1522 if (p1 - pt).length < (p0 - pt).length:
1523 p0, p1 = p1, p0
1525 v = p0 - self.arc.c
1526 da = atan2(v.y, v.x) - self.line_0.angle
1527 if da > pi:
1528 da -= 2 * pi
1529 if da < -pi:
1530 da += 2 * pi
1531 # from there pi > da > -pi
1532 # print("a:%.4f da:%.4f a0:%.4f" % (atan2(v.y, v.x), da, self.line_0.angle))
1533 if da > pi:
1534 da = pi
1535 if da < -pi:
1536 da = -pi
1537 if event.shift:
1538 da = round(da / pi * 180, 0) / 180 * pi
1539 self.set_value(context, self.datablock, self.manipulator.prop1_name, da)
1541 def draw_callback(self, _self, context, render=False):
1542 c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world)
1543 self.line_0.z_axis = normal
1544 self.line_1.z_axis = normal
1545 self.arc.z_axis = normal
1546 self.label_a.z_axis = normal
1547 self.origin = c
1548 self.line_0.p = c
1549 self.line_1.p = c
1550 self.arc.c = c
1551 self.line_0.v = left
1552 self.line_0.v = -self.line_0.cross.normalized()
1553 self.line_1.v = right
1554 self.line_1.v = self.line_1.cross.normalized()
1555 self.arc.a0 = self.line_0.angle
1556 self.arc.da = self.get_value(self.datablock, self.manipulator.prop1_name)
1557 self.arc.r = 1.0
1558 self.handle_right.set_pos(context, self.line_1.lerp(1),
1559 self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v)
1560 self.handle_center.set_pos(context, self.arc.c, -self.line_0.v)
1561 label_value = self.arc.da
1562 if self.keyboard_input_active:
1563 label_value = self.label_value
1564 self.label_a.set_pos(context, label_value, self.arc.lerp(0.5), -self.line_0.v)
1565 self.arc.draw(context, render)
1566 self.line_0.draw(context, render)
1567 self.line_1.draw(context, render)
1568 self.handle_right.draw(context, render)
1569 self.handle_center.draw(context, render)
1570 self.label_a.draw(context, render)
1571 self.feedback.draw(context, render)
1574 class DumbAngleManipulator(AngleManipulator):
1575 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
1576 AngleManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None)
1577 self.handle_right.draggable = False
1578 self.label_a.draggable = False
1580 def draw_callback(self, _self, context, render=False):
1581 c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world)
1582 self.line_0.z_axis = normal
1583 self.line_1.z_axis = normal
1584 self.arc.z_axis = normal
1585 self.label_a.z_axis = normal
1586 self.origin = c
1587 self.line_0.p = c
1588 self.line_1.p = c
1589 self.arc.c = c
1590 self.line_0.v = left
1591 self.line_0.v = -self.line_0.cross.normalized()
1592 self.line_1.v = right
1593 self.line_1.v = self.line_1.cross.normalized()
1595 # prevent ValueError in angle_signed
1596 if self.line_0.length == 0 or self.line_1.length == 0:
1597 return
1599 self.arc.a0 = self.line_0.angle
1600 self.arc.da = self.line_1.v.to_2d().angle_signed(self.line_0.v.to_2d())
1601 self.arc.r = 1.0
1602 self.handle_right.set_pos(context, self.line_1.lerp(1),
1603 self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v)
1604 self.handle_center.set_pos(context, self.arc.c, -self.line_0.v)
1605 label_value = self.arc.da
1606 self.label_a.set_pos(context, label_value, self.arc.lerp(0.5), -self.line_0.v)
1607 self.arc.draw(context, render)
1608 self.line_0.draw(context, render)
1609 self.line_1.draw(context, render)
1610 self.handle_right.draw(context, render)
1611 self.handle_center.draw(context, render)
1612 self.label_a.draw(context, render)
1613 self.feedback.draw(context, render)
1616 class ArcAngleManipulator(Manipulator):
1618 Manipulate angle of an arc
1619 when angle < 0 the arc center is on the left part of the circle
1620 when angle > 0 the arc center is on the right part of the circle
1621 bound to [-pi, pi]
1624 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
1626 # Fixed
1627 self.handle_left = SquareHandle(handle_size, arrow_size)
1628 # Angle
1629 self.handle_right = TriHandle(handle_size, arrow_size, draggable=True)
1630 self.handle_center = SquareHandle(handle_size, arrow_size)
1631 self.arc = GlArc()
1632 self.line_0 = GlLine()
1633 self.line_1 = GlLine()
1634 self.label_a = EditableText(handle_size, arrow_size, draggable=True)
1635 self.label_r = EditableText(handle_size, arrow_size, draggable=False)
1636 self.label_a.unit_type = 'ANGLE'
1637 Manipulator.__init__(self, context, o, datablock, manipulator, snap_callback)
1638 self.pts_mode = 'RADIUS'
1640 def check_hover(self):
1641 self.handle_right.check_hover(self.mouse_pos)
1642 self.label_a.check_hover(self.mouse_pos)
1644 def mouse_press(self, context, event):
1645 if self.handle_right.hover:
1646 self.active = True
1647 self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name)
1648 self.feedback.instructions(context, "Angle (degree)", "Drag to modify angle", [
1649 ('SHIFT', 'Round value'),
1650 ('RIGHTCLICK or ESC', 'cancel')
1652 self.handle_right.active = True
1653 return True
1654 if self.label_a.hover:
1655 self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle",
1656 [('ENTER', 'validate'),
1657 ('RIGHTCLICK or ESC', 'cancel')])
1658 self.value_type = 'ROTATION'
1659 self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name)
1660 self.label_a.active = True
1661 self.keyboard_input_active = True
1662 return True
1663 if self.label_r.hover:
1664 self.feedback.instructions(context, "Radius", "Use keyboard to modify radius",
1665 [('ENTER', 'validate'),
1666 ('RIGHTCLICK or ESC', 'cancel')])
1667 self.value_type = 'LENGTH'
1668 self.label_r.active = True
1669 self.keyboard_input_active = True
1670 return True
1671 return False
1673 def mouse_release(self, context, event):
1674 self.check_hover()
1675 self.handle_right.active = False
1676 self.active = False
1677 return False
1679 def mouse_move(self, context, event):
1680 self.mouse_position(event)
1681 if self.handle_right.active:
1682 self.update(context, event)
1683 return True
1684 else:
1685 self.check_hover()
1686 return False
1688 def keyboard_done(self, context, event, value):
1689 self.set_value(context, self.datablock, self.manipulator.prop1_name, value)
1690 self.label_a.active = False
1691 self.label_r.active = False
1692 return True
1694 def keyboard_cancel(self, context, event):
1695 self.label_a.active = False
1696 self.label_r.active = False
1697 return False
1699 def cancel(self, context, event):
1700 if self.active:
1701 self.mouse_release(context, event)
1702 self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle)
1704 def update(self, context, event):
1706 pt = self.get_pos3d(context)
1707 c = self.arc.c
1709 v = 2 * self.arc.r * (pt - c).normalized()
1710 v0 = c - v
1711 v1 = c + v
1712 p0, p1 = intersect_line_sphere(v0, v1, c, self.arc.r)
1714 if p0 is not None and p1 is not None:
1715 # find nearest mouse intersection point
1716 if (p1 - pt).length < (p0 - pt).length:
1717 p0, p1 = p1, p0
1719 v = p0 - self.arc.c
1721 s = self.arc.tangeant(0, 1)
1722 res, d, t = s.point_sur_segment(pt)
1723 if d > 0:
1724 # right side
1725 a = self.arc.sized_normal(0, self.arc.r).angle
1726 else:
1727 a = self.arc.sized_normal(0, -self.arc.r).angle
1729 da = atan2(v.y, v.x) - a
1731 # bottom side +- pi
1732 if t < 0:
1733 # right
1734 if d > 0:
1735 da = pi
1736 else:
1737 da = -pi
1738 # top side bound to +- pi
1739 else:
1740 if da > pi:
1741 da -= 2 * pi
1742 if da < -pi:
1743 da += 2 * pi
1745 if event.shift:
1746 da = round(da / pi * 180, 0) / 180 * pi
1747 self.set_value(context, self.datablock, self.manipulator.prop1_name, da)
1749 def draw_callback(self, _self, context, render=False):
1750 # center : 3d points
1751 # left : 3d vector pt-c
1752 # right : 3d vector pt-c
1753 c, left, right, normal = self.manipulator.get_pts(self.o.matrix_world)
1754 self.line_0.z_axis = normal
1755 self.line_1.z_axis = normal
1756 self.arc.z_axis = normal
1757 self.label_a.z_axis = normal
1758 self.label_r.z_axis = normal
1759 self.origin = c
1760 self.line_0.p = c
1761 self.line_1.p = c
1762 self.arc.c = c
1763 self.line_0.v = left
1764 self.line_1.v = right
1765 self.arc.a0 = self.line_0.angle
1766 self.arc.da = self.get_value(self.datablock, self.manipulator.prop1_name)
1767 self.arc.r = left.length
1768 self.handle_left.set_pos(context, self.line_0.lerp(1), self.line_0.v)
1769 self.handle_right.set_pos(context, self.line_1.lerp(1),
1770 self.line_1.sized_normal(1, -1 if self.arc.da > 0 else 1).v)
1771 self.handle_center.set_pos(context, self.arc.c, -self.line_0.v)
1772 label_a_value = self.arc.da
1773 label_r_value = self.arc.r
1774 if self.keyboard_input_active:
1775 if self.value_type == 'LENGTH':
1776 label_r_value = self.label_value
1777 else:
1778 label_a_value = self.label_value
1779 self.label_a.set_pos(context, label_a_value, self.arc.lerp(0.5), -self.line_0.v)
1780 self.label_r.set_pos(context, label_r_value, self.line_0.lerp(0.5), self.line_0.v)
1781 self.arc.draw(context, render)
1782 self.line_0.draw(context, render)
1783 self.line_1.draw(context, render)
1784 self.handle_left.draw(context, render)
1785 self.handle_right.draw(context, render)
1786 self.handle_center.draw(context, render)
1787 self.label_r.draw(context, render)
1788 self.label_a.draw(context, render)
1789 self.feedback.draw(context, render)
1792 class ArcAngleRadiusManipulator(ArcAngleManipulator):
1794 Manipulate angle and radius of an arc
1795 when angle < 0 the arc center is on the left part of the circle
1796 when angle > 0 the arc center is on the right part of the circle
1797 bound to [-pi, pi]
1800 def __init__(self, context, o, datablock, manipulator, handle_size, snap_callback=None):
1801 ArcAngleManipulator.__init__(self, context, o, datablock, manipulator, handle_size, snap_callback)
1802 self.handle_center = TriHandle(handle_size, arrow_size, draggable=True)
1803 self.label_r.draggable = True
1805 def check_hover(self):
1806 self.handle_right.check_hover(self.mouse_pos)
1807 self.handle_center.check_hover(self.mouse_pos)
1808 self.label_a.check_hover(self.mouse_pos)
1809 self.label_r.check_hover(self.mouse_pos)
1811 def mouse_press(self, context, event):
1812 if self.handle_right.hover:
1813 self.active = True
1814 self.original_angle = self.get_value(self.datablock, self.manipulator.prop1_name)
1815 self.feedback.instructions(context, "Angle (degree)", "Drag to modify angle", [
1816 ('SHIFT', 'Round value'),
1817 ('RIGHTCLICK or ESC', 'cancel')
1819 self.handle_right.active = True
1820 return True
1821 if self.handle_center.hover:
1822 self.active = True
1823 self.original_radius = self.get_value(self.datablock, self.manipulator.prop2_name)
1824 self.feedback.instructions(context, "Radius", "Drag to modify radius", [
1825 ('SHIFT', 'Round value'),
1826 ('RIGHTCLICK or ESC', 'cancel')
1828 self.handle_center.active = True
1829 return True
1830 if self.label_a.hover:
1831 self.feedback.instructions(context, "Angle (degree)", "Use keyboard to modify angle",
1832 [('ENTER', 'validate'),
1833 ('RIGHTCLICK or ESC', 'cancel')])
1834 self.value_type = 'ROTATION'
1835 self.label_value = self.get_value(self.datablock, self.manipulator.prop1_name)
1836 self.label_a.active = True
1837 self.keyboard_input_active = True
1838 return True
1839 if self.label_r.hover:
1840 self.feedback.instructions(context, "Radius", "Use keyboard to modify radius",
1841 [('ENTER', 'validate'),
1842 ('RIGHTCLICK or ESC', 'cancel')])
1843 self.value_type = 'LENGTH'
1844 self.label_r.active = True
1845 self.keyboard_input_active = True
1846 return True
1847 return False
1849 def mouse_release(self, context, event):
1850 self.check_hover()
1851 self.active = False
1852 self.handle_right.active = False
1853 self.handle_center.active = False
1854 return False
1856 def mouse_move(self, context, event):
1857 self.mouse_position(event)
1858 if self.handle_right.active:
1859 self.update(context, event)
1860 return True
1861 elif self.handle_center.active:
1862 self.update_radius(context, event)
1863 return True
1864 else:
1865 self.check_hover()
1866 return False
1868 def keyboard_done(self, context, event, value):
1869 if self.value_type == 'LENGTH':
1870 self.set_value(context, self.datablock, self.manipulator.prop2_name, value)
1871 self.label_r.active = False
1872 else:
1873 self.set_value(context, self.datablock, self.manipulator.prop1_name, value)
1874 self.label_a.active = False
1875 return True
1877 def update_radius(self, context, event):
1878 pt = self.get_pos3d(context)
1879 c = self.arc.c
1880 left = self.line_0.lerp(1)
1881 p, t = intersect_point_line(pt, c, left)
1882 radius = (left - p).length
1883 if event.alt:
1884 radius = round(radius, 1)
1885 self.set_value(context, self.datablock, self.manipulator.prop2_name, radius)
1887 def cancel(self, context, event):
1888 if self.handle_right.active:
1889 self.mouse_release(context, event)
1890 self.set_value(context, self.datablock, self.manipulator.prop1_name, self.original_angle)
1891 if self.handle_center.active:
1892 self.mouse_release(context, event)
1893 self.set_value(context, self.datablock, self.manipulator.prop2_name, self.original_radius)
1896 # ------------------------------------------------------------------
1897 # Define a single Manipulator Properties to store on object
1898 # ------------------------------------------------------------------
1901 # Allow registering manipulators classes
1902 manipulators_class_lookup = {}
1905 def register_manipulator(type_key, manipulator_class):
1906 if type_key in manipulators_class_lookup.keys():
1907 raise RuntimeError("Manipulator of type {} already exists, unable to override".format(type_key))
1908 manipulators_class_lookup[type_key] = manipulator_class
1911 class archipack_manipulator(PropertyGroup):
1913 A property group to add to manipulable objects
1914 type_key: type of manipulator
1915 prop1_name = the property name of object to modify
1916 prop2_name = another property name of object to modify (eg: angle and radius)
1917 p0, p1, p2 3d Vectors as base points to represent manipulators on screen
1918 normal Vector normal of plane on with draw manipulator
1920 type_key : StringProperty(default='SIZE')
1922 # How 3d points are stored in manipulators ?
1923 # SIZE = 2 absolute positioned and a scaling vector
1924 # RADIUS = 1 absolute positioned (center) and 2 relatives (sides)
1925 # POLYGON = 2 absolute positioned and a relative vector (for rect polygons)
1927 pts_mode : StringProperty(default='SIZE')
1928 prop1_name : StringProperty()
1929 prop2_name : StringProperty()
1930 p0 : FloatVectorProperty(subtype='XYZ')
1931 p1 : FloatVectorProperty(subtype='XYZ')
1932 p2 : FloatVectorProperty(subtype='XYZ')
1933 # allow orientation of manipulators by default on xy plane,
1934 # but may be used to constrain heights on local object space
1935 normal : FloatVectorProperty(subtype='XYZ', default=(0, 0, 1))
1937 def set_pts(self, pts, normal=None):
1939 set 3d location of gl points (in object space)
1940 pts: array of 3 vectors 3d
1941 normal: optional vector 3d default to Z axis
1943 pts = [Vector(p) for p in pts]
1944 self.p0, self.p1, self.p2 = pts
1945 if normal is not None:
1946 self.normal = Vector(normal)
1948 def get_pts(self, tM):
1950 convert points from local to world absolute
1951 to draw them at the right place
1952 tM : object's world matrix
1954 rM = tM.to_3x3()
1955 if self.pts_mode in ['SIZE', 'POLYGON']:
1956 return tM @ self.p0, tM @ self.p1, self.p2, rM @ self.normal
1957 else:
1958 return tM @ self.p0, rM @ self.p1, rM @ self.p2, rM @ self.normal
1960 def get_prefs(self, context):
1961 global __name__
1962 global arrow_size
1963 global handle_size
1964 try:
1965 # retrieve addon name from imports
1966 addon_name = __name__.split('.')[0]
1967 prefs = context.preferences.addons[addon_name].preferences
1968 arrow_size = prefs.arrow_size
1969 handle_size = prefs.handle_size
1970 except:
1971 pass
1973 def setup(self, context, o, datablock, snap_callback=None):
1975 Factory return a manipulator object or None
1976 o: object
1977 datablock: datablock to modify
1978 snap_callback: function call y
1981 self.get_prefs(context)
1983 global manipulators_class_lookup
1985 if self.type_key not in manipulators_class_lookup.keys() or \
1986 not manipulators_class_lookup[self.type_key].poll(context):
1987 # RuntimeError is overkill but may be enabled for debug purposes
1988 # Silently ignore allow skipping manipulators if / when deps as not meet
1989 # manip stack will simply be filled with None objects
1990 # raise RuntimeError("Manipulator of type {} not found".format(self.type_key))
1991 return None
1993 m = manipulators_class_lookup[self.type_key](context, o, datablock, self, handle_size, snap_callback)
1994 # points storage model as described upside
1995 self.pts_mode = m.pts_mode
1996 return m
1999 # ------------------------------------------------------------------
2000 # Define Manipulable to make a PropertyGroup manipulable
2001 # ------------------------------------------------------------------
2004 class ARCHIPACK_OT_manipulate(Operator):
2005 bl_idname = "archipack.manipulate"
2006 bl_label = "Manipulate"
2007 bl_description = "Manipulate"
2008 bl_options = {'REGISTER', 'UNDO'}
2010 object_name : StringProperty(default="")
2012 @classmethod
2013 def poll(self, context):
2014 return context.active_object is not None
2016 def exit_selectmode(self, context, key):
2018 Hide select area on exit
2020 global manips
2021 if key in manips.keys():
2022 if manips[key].manipulable is not None:
2023 manips[key].manipulable.manipulable_exit_selectmode(context)
2025 def modal(self, context, event):
2026 global manips
2027 # Exit on stack change
2028 # handle multiple object stack
2029 # use object_name property to find manupulated object in stack
2030 # select and make object active
2031 # and exit when not found
2032 if context.area is not None:
2033 context.area.tag_redraw()
2034 key = self.object_name
2035 if check_stack(key):
2036 self.exit_selectmode(context, key)
2037 remove_manipulable(key)
2038 # print("modal exit by check_stack(%s)" % (key))
2039 return {'FINISHED'}
2041 res = manips[key].manipulable.manipulable_modal(context, event)
2043 if 'FINISHED' in res:
2044 self.exit_selectmode(context, key)
2045 remove_manipulable(key)
2046 # print("modal exit by {FINISHED}")
2048 return res
2050 def invoke(self, context, event):
2051 if context.space_data is not None and context.space_data.type == 'VIEW_3D':
2052 context.window_manager.modal_handler_add(self)
2053 return {'RUNNING_MODAL'}
2054 else:
2055 self.report({'WARNING'}, "Active space must be a View3d")
2056 return {'CANCELLED'}
2059 class ARCHIPACK_OT_disable_manipulate(Operator):
2060 bl_idname = "archipack.disable_manipulate"
2061 bl_label = "Disable Manipulate"
2062 bl_description = "Disable any active manipulator"
2063 bl_options = {'REGISTER', 'UNDO'}
2065 @classmethod
2066 def poll(self, context):
2067 return True
2069 def execute(self, context):
2070 empty_stack()
2071 return {'FINISHED'}
2074 class Manipulable():
2076 A class extending PropertyGroup to setup gl manipulators
2077 Beware : prevent crash calling manipulable_disable()
2078 before changing manipulated data structure
2080 manipulators : CollectionProperty(
2081 type=archipack_manipulator,
2082 # options={'SKIP_SAVE'},
2083 # options={'HIDDEN'},
2084 description="store 3d points to draw gl manipulators"
2087 # TODO: make simple instance vars
2088 manipulable_refresh : BoolProperty(
2089 default=False,
2090 options={'SKIP_SAVE'},
2091 description="Flag enable to rebuild manipulators when data model change"
2093 manipulate_mode : BoolProperty(
2094 default=False,
2095 options={'SKIP_SAVE'},
2096 description="Flag manipulation state so we are able to toggle"
2098 select_mode : BoolProperty(
2099 default=False,
2100 options={'SKIP_SAVE'},
2101 description="Flag select state so we are able to toggle"
2103 manipulable_selectable : BoolProperty(
2104 default=False,
2105 options={'SKIP_SAVE'},
2106 description="Flag make manipulators selectable"
2109 keymap = None
2110 manipulable_area = None
2111 manipulable_start_point = None
2112 manipulable_end_point = None
2113 manipulable_draw_handler = None
2115 def setup_manipulators(self):
2117 Must implement manipulators creation
2118 TODO: call from update and manipulable_setup
2120 raise NotImplementedError
2122 def manipulable_draw_callback(self, _self, context):
2123 self.manipulable_area.draw(context)
2125 def manipulable_disable(self, context):
2127 disable gl draw handlers
2130 if self.keymap is None:
2131 self.keymap = Keymaps(context)
2132 self.manipulable_area = GlCursorArea()
2133 self.manipulable_start_point = Vector((0, 0))
2134 self.manipulable_end_point = Vector((0, 0))
2136 o = context.active_object
2137 if o is not None:
2138 self.manipulable_exit_selectmode(context)
2139 remove_manipulable(o.name)
2140 self.manip_stack = add_manipulable(o.name, self)
2142 self.manipulate_mode = False
2143 self.select_mode = False
2145 def manipulable_exit_selectmode(self, context):
2146 self.manipulable_area.disable()
2147 self.select_mode = False
2148 # remove select draw handler
2149 if self.manipulable_draw_handler is not None:
2150 bpy.types.SpaceView3D.draw_handler_remove(
2151 self.manipulable_draw_handler,
2152 'WINDOW')
2153 self.manipulable_draw_handler = None
2155 def manipulable_setup(self, context):
2157 TODO: Implement the setup part as per parent object basis
2159 self.manipulable_disable(context)
2160 o = context.active_object
2161 self.setup_manipulators()
2162 for m in self.manipulators:
2163 self.manip_stack.append(m.setup(context, o, self))
2165 def _manipulable_invoke(self, context):
2167 object_name = context.active_object.name
2169 # store a reference to self for operators
2170 add_manipulable(object_name, self)
2172 # copy context so manipulator always use
2173 # invoke time context
2174 ctx = context.copy()
2176 # take care of context switching
2177 # when call from outside of 3d view
2178 if context.space_data is not None and context.space_data.type != 'VIEW_3D':
2179 for window in bpy.context.window_manager.windows:
2180 screen = window.screen
2181 for area in screen.areas:
2182 if area.type == 'VIEW_3D':
2183 ctx['area'] = area
2184 for region in area.regions:
2185 if region.type == 'WINDOW':
2186 ctx['region'] = region
2187 break
2188 if ctx is not None:
2189 bpy.ops.archipack.manipulate(ctx, 'INVOKE_DEFAULT', object_name=object_name)
2191 def manipulable_invoke(self, context):
2193 call this in operator invoke()
2195 if override don't forget to call:
2196 _manipulable_invoke(context)
2199 # print("manipulable_invoke self.manipulate_mode:%s" % (self.manipulate_mode))
2201 if self.manipulate_mode:
2202 self.manipulable_disable(context)
2203 return False
2204 # else:
2205 # bpy.ops.archipack.disable_manipulate('INVOKE_DEFAULT')
2207 # self.manip_stack = []
2208 # kills other's manipulators
2209 # self.manipulate_mode = True
2210 self.manipulable_setup(context)
2211 self.manipulate_mode = True
2213 self._manipulable_invoke(context)
2215 return True
2217 def manipulable_modal(self, context, event):
2219 call in operator modal()
2220 should not be overridden
2221 as it provide all needed
2222 functionality out of the box
2225 # setup again when manipulators type change
2226 if self.manipulable_refresh:
2227 # print("manipulable_refresh")
2228 self.manipulable_refresh = False
2229 self.manipulable_setup(context)
2230 self.manipulate_mode = True
2232 if context.area is None:
2233 self.manipulable_disable(context)
2234 return {'FINISHED'}
2236 context.area.tag_redraw()
2238 if self.keymap is None:
2239 self.keymap = Keymaps(context)
2241 if self.keymap.check(event, self.keymap.undo):
2242 # user feedback on undo by disabling manipulators
2243 self.manipulable_disable(context)
2244 return {'FINISHED'}
2246 # clean up manipulator on delete
2247 if self.keymap.check(event, self.keymap.delete): # {'X'}:
2248 # @TODO:
2249 # for doors and windows, seek and destroy holes object if any
2250 # a dedicated delete method into those objects may be an option ?
2251 # A type check is required any way we choose
2253 # Time for a generic archipack's datablock getter / filter into utils
2255 # May also be implemented into nearly hidden "reference point"
2256 # to delete / duplicate / link duplicate / unlink of
2257 # a complete set of wall, doors and windows at once
2258 self.manipulable_disable(context)
2260 if bpy.ops.object.delete.poll():
2261 bpy.ops.object.delete('INVOKE_DEFAULT', use_global=False)
2263 return {'FINISHED'}
2266 # handle keyborad for select mode
2267 if self.select_mode:
2268 if event.type in {'A'} and event.value == 'RELEASE':
2269 return {'RUNNING_MODAL'}
2272 for manipulator in self.manip_stack:
2273 # manipulator should return false on left mouse release
2274 # so proper release handler is called
2275 # and return true to call manipulate when required
2276 # print("manipulator:%s" % manipulator)
2277 if manipulator is not None and manipulator.modal(context, event):
2278 self.manipulable_manipulate(context, event, manipulator)
2279 return {'RUNNING_MODAL'}
2281 # print("Manipulable %s %s" % (event.type, event.value))
2283 # Manipulators are not active so check for selection
2284 if event.type == 'LEFTMOUSE':
2286 # either we are starting select mode
2287 # user press on area not over maniuplator
2288 # Prevent 3 mouse emultation to select when alt pressed
2289 if self.manipulable_selectable and event.value == 'PRESS' and not event.alt:
2290 self.select_mode = True
2291 self.manipulable_area.enable()
2292 self.manipulable_start_point = Vector((event.mouse_region_x, event.mouse_region_y))
2293 self.manipulable_area.set_location(
2294 context,
2295 self.manipulable_start_point,
2296 self.manipulable_start_point)
2297 # add a select draw handler
2298 args = (self, context)
2299 self.manipulable_draw_handler = bpy.types.SpaceView3D.draw_handler_add(
2300 self.manipulable_draw_callback,
2301 args,
2302 'WINDOW',
2303 'POST_PIXEL')
2304 # don't keep focus
2305 # as this prevent click over ui
2306 # return {'RUNNING_MODAL'}
2308 elif event.value == 'RELEASE':
2309 if self.select_mode:
2310 # confirm selection
2312 self.manipulable_exit_selectmode(context)
2314 # keep focus
2315 # return {'RUNNING_MODAL'}
2317 else:
2318 # allow manipulator action on release
2319 for manipulator in self.manip_stack:
2320 if manipulator is not None and manipulator.selectable:
2321 manipulator.selected = False
2322 self.manipulable_release(context)
2324 elif self.select_mode and event.type == 'MOUSEMOVE' and event.value == 'PRESS':
2325 # update select area size
2326 self.manipulable_end_point = Vector((event.mouse_region_x, event.mouse_region_y))
2327 self.manipulable_area.set_location(
2328 context,
2329 self.manipulable_start_point,
2330 self.manipulable_end_point)
2331 if event.shift:
2332 # deselect
2333 for i, manipulator in enumerate(self.manip_stack):
2334 if manipulator is not None and manipulator.selectable:
2335 manipulator.deselect(self.manipulable_area)
2336 else:
2337 # select / more
2338 for i, manipulator in enumerate(self.manip_stack):
2339 if manipulator is not None and manipulator.selectable:
2340 manipulator.select(self.manipulable_area)
2341 # keep focus to prevent left select mouse to actually move object
2342 return {'RUNNING_MODAL'}
2344 # event.alt here to prevent 3 button mouse emulation exit while zooming
2345 if event.type in {'RIGHTMOUSE', 'ESC'} and event.value == 'PRESS' and not event.alt:
2346 self.manipulable_disable(context)
2347 self.manipulable_exit(context)
2348 return {'FINISHED'}
2350 return {'PASS_THROUGH'}
2352 # Callbacks
2353 def manipulable_release(self, context):
2355 Override with action to do on mouse release
2356 eg: big update
2358 return
2360 def manipulable_exit(self, context):
2362 Override with action to do when modal exit
2364 return
2366 def manipulable_manipulate(self, context, event, manipulator):
2368 Override with action to do when a handle is active (pressed and mousemove)
2370 return
2373 @persistent
2374 def cleanup(dummy=None):
2375 empty_stack()
2378 def register():
2379 # Register default manipulators
2380 global manips
2381 global manipulators_class_lookup
2382 manipulators_class_lookup = {}
2383 manips = {}
2384 register_manipulator('SIZE', SizeManipulator)
2385 register_manipulator('SIZE_LOC', SizeLocationManipulator)
2386 register_manipulator('ANGLE', AngleManipulator)
2387 register_manipulator('DUMB_ANGLE', DumbAngleManipulator)
2388 register_manipulator('ARC_ANGLE_RADIUS', ArcAngleRadiusManipulator)
2389 register_manipulator('COUNTER', CounterManipulator)
2390 register_manipulator('DUMB_SIZE', DumbSizeManipulator)
2391 register_manipulator('DELTA_LOC', DeltaLocationManipulator)
2392 register_manipulator('DUMB_STRING', DumbStringManipulator)
2394 # snap aware size loc
2395 register_manipulator('SNAP_SIZE_LOC', SnapSizeLocationManipulator)
2396 # register_manipulator('SNAP_POINT', SnapPointManipulator)
2397 # wall's line based object snap
2398 register_manipulator('WALL_SNAP', WallSnapManipulator)
2399 bpy.utils.register_class(ARCHIPACK_OT_manipulate)
2400 bpy.utils.register_class(ARCHIPACK_OT_disable_manipulate)
2401 bpy.utils.register_class(archipack_manipulator)
2402 bpy.app.handlers.load_pre.append(cleanup)
2405 def unregister():
2406 global manips
2407 global manipulators_class_lookup
2408 empty_stack()
2409 del manips
2410 manipulators_class_lookup.clear()
2411 del manipulators_class_lookup
2412 bpy.utils.unregister_class(ARCHIPACK_OT_manipulate)
2413 bpy.utils.unregister_class(ARCHIPACK_OT_disable_manipulate)
2414 bpy.utils.unregister_class(archipack_manipulator)
2415 bpy.app.handlers.load_pre.remove(cleanup)