Skip to content
Snippets Groups Projects
space_view3d_enhanced_3d_cursor.py 179 KiB
Newer Older
  • Learn to ignore specific revisions
  • #  ***** 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 3 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, see <http://www.gnu.org/licenses/>.
    #
    #  ***** END GPL LICENSE BLOCK *****
    
    # <pep8-80 compliant>
    
    bl_info = {
        "name": "Enhanced 3D Cursor",
        "description": "Cursor history and bookmarks; drag/snap cursor.",
        "author": "dairin0d",
    
        "location": "View3D > Action mouse; F10; Properties panel",
        "warning": "",
    
        "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
    
            "Scripts/3D_interaction/Enhanced_3D_Cursor",
    
        "tracker_url": "https://developer.blender.org/T28451",
    
        "category": "3D View"}
    
    """
    
    ATTENTION:
    somewhere around 45447 revision object.ray_cast() starts conflicting with
    mesh.update(calc_tessface=True) -- at least when invoked within one
    operator cycle, object.ray_cast() crashes if object's tessfaces were
    update()d earlier in the code. However, not update()ing the meshes
    seems to work fine -- ray_cast() does its job, and it's possible to
    access tessfaces afterwards.
    
    mesh.calc_tessface() -- ? crashes too
    
    Seems like now axes are stored in columns instead of rows.
    Perhaps it's better to write utility functions to create/decompose
    matrices from/to 3D-vector axes and a translation component
    
    Breakdown:
        Addon registration
        Keymap utils
        Various utils (e.g. find_region)
        OpenGL; drawing utils
        Non-undoable data storage
        Cursor utils
        Stick-object
        Cursor monitor
        Addon's GUI
        Addon's properties
        Addon's operators
        ID Block emulator
        Mesh cache
        Snap utils
        View3D utils
        Transform orientation / coordinate system utils
        Generic transform utils
        Main operator
        ...
    .
    
    First step is to re-make the cursor addon (make something usable first).
    CAD tools should be done without the hassle.
    
    
    TODO:
    
        strip trailing space? (one of campbellbarton's commits did that)
    
        IDEAS:
    
            - implement 'GIMBAL' orientation (euler axes)
            - mini-Z-buffer in the vicinity of mouse coords (using raycasts)
            - an orientation that points towards cursor
              (from current selection to cursor)
            - user coordinate systems (using e.g. empties to store different
              systems; when user switches to such UCS, origin will be set to
              "cursor", cursor will be sticked to the empty, and a custom
              transform orientation will be aligned with the empty)
              - "Stick" transform orientation that is always aligned with the
                object cursor is "sticked" to?
    
            - make 'NORMAL' system also work for bones?
    
            - user preferences? (stored in a file)
    
            - create spline/edge_mesh from history?
            - API to access history/bookmarks/operators from other scripts?
            - Snap selection to bookmark?
            - Optimize
            - Clean up code, move to several files?
        LATER:
        ISSUES:
            Limitations:
                - I need to emulate in Python some things that Blender doesn't
                  currently expose through API:
                  - obtaining matrix of predefined transform orientation
                  - obtaining position of pivot
                  For some kinds of information (e.g. active vertex/edge,
                  selected meta-elements), there is simply no workaround.
                - Snapping to vertices/edges works differently than in Blender.
                  First of all, iteration over all vertices/edges of all
                  objects along the ray is likely to be very slow.
                  Second, it's more human-friendly to snap to visible
                  elements (or at least with approximately known position).
                - In editmode I have to exit-and-enter it to get relevant
                  information about current selection. Thus any operator
                  would automatically get applied when you click on 3D View.
            Mites:
        QUESTIONS:
    ==============================================================================
    Borrowed code/logic:
    - space_view3d_panel_measure.py (Buerbaum Martin "Pontiac"):
      - OpenGL state storing/restoring; working with projection matrices.
    """
    
    import bpy
    import bgl
    import blf
    
    
    from mathutils import Vector, Matrix, Quaternion, Euler
    
    from mathutils.geometry import (intersect_line_sphere,
                                    intersect_ray_tri,
                                    barycentric_transform,
    
                                    intersect_line_line,
                                    intersect_line_plane,
                                    )
    
    
    from bpy_extras.view3d_utils import (region_2d_to_location_3d,
    
                                         location_3d_to_region_2d,
                                         )
    
    import math
    import time
    
    # ====== MODULE GLOBALS / CONSTANTS ====== #
    tmp_name = chr(0x10ffff) # maximal Unicode value
    epsilon = 0.000001
    
    # ====== SET CURSOR OPERATOR ====== #
    class EnhancedSetCursor(bpy.types.Operator):
        """Cursor history and bookmarks; drag/snap cursor."""
        bl_idname = "view3d.cursor3d_enhanced"
        bl_label = "Enhanced Set Cursor"
    
        key_char_map = {
            'PERIOD':".", 'NUMPAD_PERIOD':".",
            'MINUS':"-", 'NUMPAD_MINUS':"-",
            'EQUAL':"+", 'NUMPAD_PLUS':"+",
            #'E':"e", # such big/small numbers aren't useful
            'ONE':"1", 'NUMPAD_1':"1",
            'TWO':"2", 'NUMPAD_2':"2",
            'THREE':"3", 'NUMPAD_3':"3",
            'FOUR':"4", 'NUMPAD_4':"4",
            'FIVE':"5", 'NUMPAD_5':"5",
            'SIX':"6", 'NUMPAD_6':"6",
            'SEVEN':"7", 'NUMPAD_7':"7",
            'EIGHT':"8", 'NUMPAD_8':"8",
            'NINE':"9", 'NUMPAD_9':"9",
            'ZERO':"0", 'NUMPAD_0':"0",
            'SPACE':" ",
            'SLASH':"/", 'NUMPAD_SLASH':"/",
            'NUMPAD_ASTERIX':"*",
        }
    
        key_coordsys_map = {
            'LEFT_BRACKET':-1,
            'RIGHT_BRACKET':1,
            'J':'VIEW',
            'K':"Surface",
            'L':'LOCAL',
            'B':'GLOBAL',
            'N':'NORMAL',
            'M':"Scaled",
        }
    
        key_pivot_map = {
            'H':'ACTIVE',
            'U':'CURSOR',
            'I':'INDIVIDUAL',
            'O':'CENTER',
            'P':'MEDIAN',
        }
    
        key_snap_map = {
            'C':'INCREMENT',
            'V':'VERTEX',
            'E':'EDGE',
            'F':'FACE',
        }
    
        key_tfm_mode_map = {
            'G':'MOVE',
            'R':'ROTATE',
            'S':'SCALE',
        }
    
        key_map = {
            "confirm":{'ACTIONMOUSE'}, # also 'RET' ?
            "cancel":{'SELECTMOUSE', 'ESC'},
            "free_mouse":{'F10'},
            "make_normal_snapshot":{'W'},
            "make_tangential_snapshot":{'Q'},
            "use_absolute_coords":{'A'},
            "snap_to_raw_mesh":{'D'},
            "use_object_centers":{'T'},
            "precision_up":{'PAGE_UP'},
            "precision_down":{'PAGE_DOWN'},
            "move_caret_prev":{'LEFT_ARROW'},
            "move_caret_next":{'RIGHT_ARROW'},
            "move_caret_home":{'HOME'},
            "move_caret_end":{'END'},
            "change_current_axis":{'TAB', 'RET', 'NUMPAD_ENTER'},
            "prev_axis":{'UP_ARROW'},
            "next_axis":{'DOWN_ARROW'},
            "remove_next_character":{'DEL'},
            "remove_last_character":{'BACK_SPACE'},
            "copy_axes":{'C'},
            "paste_axes":{'V'},
            "cut_axes":{'X'},
        }
    
        gizmo_factor = 0.15
        click_period = 0.25
    
        angle_grid_steps = {True:1.0, False:5.0}
        scale_grid_steps = {True:0.01, False:0.1}
    
        # ====== OPERATOR METHOD OVERLOADS ====== #
        @classmethod
        def poll(cls, context):
            area_types = {'VIEW_3D',} # also: IMAGE_EDITOR ?
            return (context.area.type in area_types) and \
                   (context.region.type == "WINDOW")
    
        def modal(self, context, event):
            context.area.tag_redraw()
            return self.try_process_input(context, event)
    
        def invoke(self, context, event):
            # Attempt to launch the monitor
            if bpy.ops.view3d.cursor3d_monitor.poll():
                bpy.ops.view3d.cursor3d_monitor()
    
            # Don't interfere with these modes when only mouse is pressed
    
            if ('SCULPT' in context.mode) or ('PAINT' in context.mode):
    
                if "MOUSE" in event.type:
                    return {'CANCELLED'}
    
            CursorDynamicSettings.active_transform_operator = self
    
            tool_settings = context.tool_settings
    
            settings = find_settings()
            tfm_opts = settings.transform_options
    
            settings_scene = context.scene.cursor_3d_tools_settings
    
            # Coordinate System Utility
            self.particles, self.csu = gather_particles(context=context)
            self.particles = [View3D_Cursor(context)]
    
            self.csu.source_pos = self.particles[0].get_location()
            self.csu.source_rot = self.particles[0].get_rotation()
            self.csu.source_scale = self.particles[0].get_scale()
    
            # View3D Utility
            self.vu = ViewUtility(context.region, context.space_data,
                context.region_data)
    
            # Snap Utility
            self.su = SnapUtility(context)
    
            # turn off view locking for the duration of the operator
            self.view_pos = self.vu.get_position(True)
            self.vu.set_position(self.vu.get_position(), True)
            self.view_locks = self.vu.get_locks()
            self.vu.set_locks({})
    
            # Initialize runtime states
            self.initiated_by_mouse = ("MOUSE" in event.type)
            self.free_mouse = not self.initiated_by_mouse
            self.use_object_centers = False
            self.axes_values = ["", "", ""]
            self.axes_coords = [None, None, None]
            self.axes_eval_success = [True, True, True]
            self.allowed_axes = [True, True, True]
            self.current_axis = 0
            self.caret_pos = 0
            self.coord_format = "{:." + str(settings.free_coord_precision) + "f}"
            self.transform_mode = 'MOVE'
            self.init_xy_angle_distance(context, event)
    
            self.click_start = time.time()
            if not self.initiated_by_mouse:
                self.click_start -= self.click_period
    
            self.stick_obj_name = settings_scene.stick_obj_name
            self.stick_obj_pos = settings_scene.stick_obj_pos
    
            # Initial run
            self.try_process_input(context, event, True)
    
            context.window_manager.modal_handler_add(self)
            return {'RUNNING_MODAL'}
    
        def cancel(self, context):
            for particle in self.particles:
                particle.revert()
    
            set_stick_obj(context.scene, self.stick_obj_name, self.stick_obj_pos)
    
            self.finalize(context)
    
        # ====== CLEANUP/FINALIZE ====== #
        def finalize(self, context):
            # restore view locking
            self.vu.set_locks(self.view_locks)
            self.vu.set_position(self.view_pos, True)
    
            self.cleanup(context)
    
            # This is to avoid "blinking" of
            # between-history-positions line
            settings = find_settings()
            history = settings.history
            # make sure the most recent history entry is displayed
            history.curr_id = 0
            history.last_id = 0
    
            # Ensure there are no leftovers from draw_callback
            context.area.tag_redraw()
    
            return {'FINISHED'}
    
        def cleanup(self, context):
            self.particles = None
            self.csu = None
            self.vu = None
            if self.su is not None:
                self.su.dispose()
            self.su = None
    
            CursorDynamicSettings.active_transform_operator = None
    
        # ====== USER INPUT PROCESSING ====== #
    
        def setup_keymaps(self, context, event=None):
    
            self.key_map = self.key_map.copy()
    
            # There is no such event as 'ACTIONMOUSE',
            # it's always 'LEFTMOUSE' or 'RIGHTMOUSE'
    
            if event:
                if event.type == 'LEFTMOUSE':
                    self.key_map["confirm"] = {'LEFTMOUSE'}
                    self.key_map["cancel"] = {'RIGHTMOUSE', 'ESC'}
                elif event.type == 'RIGHTMOUSE':
                    self.key_map["confirm"] = {'RIGHTMOUSE'}
                    self.key_map["cancel"] = {'LEFTMOUSE', 'ESC'}
                else:
                    event = None
            if event is None:
                select_mouse = context.user_preferences.inputs.select_mouse
                if select_mouse == 'RIGHT':
                    self.key_map["confirm"] = {'LEFTMOUSE'}
                    self.key_map["cancel"] = {'RIGHTMOUSE', 'ESC'}
                else:
                    self.key_map["confirm"] = {'RIGHTMOUSE'}
                    self.key_map["cancel"] = {'LEFTMOUSE', 'ESC'}
    
            # Use user-defined "free mouse" key, if it exists
            wm = context.window_manager
            if '3D View' in wm.keyconfigs.user.keymaps:
                km = wm.keyconfigs.user.keymaps['3D View']
                for kmi in km.keymap_items:
                    if kmi.idname == 'view3d.cursor3d_enhanced':
                        if kmi.map_type == 'KEYBOARD':
                            self.key_map["free_mouse"] = {kmi.type,}
                            break
    
        def try_process_input(self, context, event, initial_run=False):
            try:
                return self.process_input(context, event, initial_run)
            except:
                # If anything fails, at least dispose the resources
                self.cleanup(context)
                raise
    
        def process_input(self, context, event, initial_run=False):
            wm = context.window_manager
            v3d = context.space_data
    
            if event.type in self.key_map["confirm"]:
                if self.free_mouse:
                    finished = (event.value == 'PRESS')
                else:
                    finished = (event.value == 'RELEASE')
    
                if finished:
                    return self.finalize(context)
    
            if event.type in self.key_map["cancel"]:
                return self.cancel(context)
    
            tool_settings = context.tool_settings
    
            settings = find_settings()
            tfm_opts = settings.transform_options
    
            make_snapshot = False
            tangential_snapshot = False
    
            if event.value == 'PRESS':
                if event.type in self.key_map["free_mouse"]:
    
                        # confirm if pressed second time
                        return self.finalize(context)
                    else:
                        self.free_mouse = True
    
                if event.type in self.key_tfm_mode_map:
                    new_mode = self.key_tfm_mode_map[event.type]
    
                    if self.transform_mode != new_mode:
                        # snap cursor to its initial state
                        if new_mode != 'MOVE':
                            for particle in self.particles:
                                initial_matrix = particle.get_initial_matrix()
                                particle.set_matrix(initial_matrix)
                        # reset intial mouse position
                        self.init_xy_angle_distance(context, event)
    
                    self.transform_mode = new_mode
    
                if event.type in self.key_map["make_normal_snapshot"]:
                    make_snapshot = True
                    tangential_snapshot = False
    
                if event.type in self.key_map["make_tangential_snapshot"]:
                    make_snapshot = True
                    tangential_snapshot = True
    
                if event.type in self.key_map["snap_to_raw_mesh"]:
                    tool_settings.use_snap_self = \
                        not tool_settings.use_snap_self
    
                if (not event.alt) and (event.type in {'X', 'Y', 'Z'}):
                    axis_lock = [(event.type == 'X') != event.shift,
                                 (event.type == 'Y') != event.shift,
                                 (event.type == 'Z') != event.shift]
    
                    if self.allowed_axes != axis_lock:
                        self.allowed_axes = axis_lock
                    else:
                        self.allowed_axes = [True, True, True]
    
                if event.type in self.key_map["use_absolute_coords"]:
                    tfm_opts.use_relative_coords = \
                        not tfm_opts.use_relative_coords
    
                    self.update_origin_projection(context)
    
                incr = 0
                if event.type in self.key_map["change_current_axis"]:
                    incr = (-1 if event.shift else 1)
                elif event.type in self.key_map["next_axis"]:
                    incr = 1
                elif event.type in self.key_map["prev_axis"]:
                    incr = -1
    
                if incr != 0:
                    self.current_axis = (self.current_axis + incr) % 3
                    self.caret_pos = len(self.axes_values[self.current_axis])
    
                incr = 0
                if event.type in self.key_map["precision_up"]:
                    incr = 1
                elif event.type in self.key_map["precision_down"]:
                    incr = -1
    
                if incr != 0:
                    settings.free_coord_precision += incr
                    self.coord_format = "{:." + \
                        str(settings.free_coord_precision) + "f}"
    
                if (event.type == 'ZERO') and event.ctrl:
                    self.snap_to_system_origin()
                else:
                    self.process_axis_input(event)
    
                if event.alt:
    
                    jc = (", " if tfm_opts.use_comma_separator else "\t")
    
                    if event.type in self.key_map["copy_axes"]:
    
                        wm.clipboard = jc.join(self.get_axes_text(True))
    
                    elif event.type in self.key_map["cut_axes"]:
    
                        wm.clipboard = jc.join(self.get_axes_text(True))
    
                        self.set_axes_text("\t\t\t")
                    elif event.type in self.key_map["paste_axes"]:
    
                        if jc == "\t":
                            self.set_axes_text(wm.clipboard, True)
                        else:
                            jc = jc.strip()
                            ttext = ""
                            brackets = 0
                            for c in wm.clipboard:
                                if c in "[{(":
                                    brackets += 1
                                elif c in "]})":
                                    brackets -= 1
                                if (brackets == 0) and (c == jc):
                                    c = "\t"
                                ttext += c
                            self.set_axes_text(ttext, True)
    
                if event.type in self.key_coordsys_map:
                    new_orientation = self.key_coordsys_map[event.type]
                    self.csu.set_orientation(new_orientation)
    
                    self.update_origin_projection(context)
    
                    if event.ctrl:
                        self.snap_to_system_origin()
    
                if event.type in self.key_map["use_object_centers"]:
                    v3d.use_pivot_point_align = not v3d.use_pivot_point_align
    
                if event.type in self.key_pivot_map:
                    self.csu.set_pivot(self.key_pivot_map[event.type])
    
                    self.update_origin_projection(context)
    
                    if event.ctrl:
                        self.snap_to_system_origin(force_pivot=True)
    
                if (not event.alt) and (event.type in self.key_snap_map):
                    snap_element = self.key_snap_map[event.type]
                    if tool_settings.snap_element == snap_element:
                        if snap_element == 'VERTEX':
                            snap_element = 'VOLUME'
                        elif snap_element == 'VOLUME':
                            snap_element = 'VERTEX'
                    tool_settings.snap_element = snap_element
            # end if
    
            use_snap = (tool_settings.use_snap != event.ctrl)
            if use_snap:
                snap_type = tool_settings.snap_element
            else:
                userprefs_view = context.user_preferences.view
                if userprefs_view.use_mouse_depth_cursor:
                    # Suggested by Lissanro in the forum
                    use_snap = True
                    snap_type = 'FACE'
    
                else:
                    snap_type = None
    
            axes_coords = [None, None, None]
            if self.transform_mode == 'MOVE':
                for i in range(3):
                    if self.axes_coords[i] is not None:
                        axes_coords[i] = self.axes_coords[i]
                    elif not self.allowed_axes[i]:
                        axes_coords[i] = 0.0
    
            self.su.set_modes(
                interpolation=tfm_opts.snap_interpolate_normals_mode,
                use_relative_coords=tfm_opts.use_relative_coords,
                editmode=tool_settings.use_snap_self,
                snap_type=snap_type,
                snap_align=tool_settings.use_snap_align_rotation,
                axes_coords=axes_coords,
                )
    
            self.do_raycast = ("MOUSE" in event.type)
            self.grid_substep = event.shift
            self.modify_surface_orientation = (len(self.particles) == 1)
            self.xy = Vector((event.mouse_region_x, event.mouse_region_y))
    
            self.use_object_centers = v3d.use_pivot_point_align
    
            if event.type == 'MOUSEMOVE':
                self.update_transform_mousemove()
    
            if self.transform_mode == 'MOVE':
                transform_func = self.transform_move
            elif self.transform_mode == 'ROTATE':
                transform_func = self.transform_rotate
            elif self.transform_mode == 'SCALE':
                transform_func = self.transform_scale
    
            for particle in self.particles:
                transform_func(particle)
    
            if make_snapshot:
                self.make_normal_snapshot(context.scene, tangential_snapshot)
    
            return {'RUNNING_MODAL'}
    
        def update_origin_projection(self, context):
            r = context.region
            rv3d = context.region_data
    
            origin = self.csu.get_origin()
            # prehaps not projection, but intersection with plane?
            self.origin_xy = location_3d_to_region_2d(r, rv3d, origin)
            if self.origin_xy is None:
                self.origin_xy = Vector((r.width / 2, r.height / 2))
    
            self.delta_xy = (self.start_xy - self.origin_xy).to_3d()
            self.prev_delta_xy = self.delta_xy
    
        def init_xy_angle_distance(self, context, event):
            self.start_xy = Vector((event.mouse_region_x, event.mouse_region_y))
    
            self.update_origin_projection(context)
    
            # Distinction between angles has to be made because
            # angles can go beyond 360 degrees (we cannot snap
            # to increment the original ones).
            self.raw_angles = [0.0, 0.0, 0.0]
            self.angles = [0.0, 0.0, 0.0]
            self.scales = [1.0, 1.0, 1.0]
    
        def update_transform_mousemove(self):
            delta_xy = (self.xy - self.origin_xy).to_3d()
    
            n_axes = sum(int(v) for v in self.allowed_axes)
            if n_axes == 1:
                # rotate using angle as value
                rd = self.prev_delta_xy.rotation_difference(delta_xy)
                offset = -rd.angle * round(rd.axis[2])
    
                sys_matrix = self.csu.get_matrix()
    
                i_allowed = 0
                for i in range(3):
                    if self.allowed_axes[i]:
                        i_allowed = i
    
                view_dir = self.vu.get_direction()
                if view_dir.dot(sys_matrix[i_allowed][:3]) < 0:
                    offset = -offset
    
                for i in range(3):
                    if self.allowed_axes[i]:
                        self.raw_angles[i] += offset
            elif n_axes == 2:
                # rotate using XY coords as two values
                offset = (delta_xy - self.prev_delta_xy) * (math.pi / 180.0)
    
                if self.grid_substep:
                    offset *= 0.1
                else:
                    offset *= 0.5
    
                j = 0
                for i in range(3):
                    if self.allowed_axes[i]:
                        self.raw_angles[i] += offset[1 - j]
                        j += 1
            elif n_axes == 3:
                # rotate around view direction
                rd = self.prev_delta_xy.rotation_difference(delta_xy)
                offset = -rd.angle * round(rd.axis[2])
    
                view_dir = self.vu.get_direction()
    
                sys_matrix = self.csu.get_matrix()
    
                try:
                    view_dir = sys_matrix.inverted().to_3x3() * view_dir
                except:
                    # this is some degenerate system
                    pass
    
                view_dir.normalize()
    
                rot = Matrix.Rotation(offset, 3, view_dir)
    
                matrix = Euler(self.raw_angles, 'XYZ').to_matrix()
                matrix.rotate(rot)
    
                euler = matrix.to_euler('XYZ')
                self.raw_angles[0] += clamp_angle(euler.x - self.raw_angles[0])
                self.raw_angles[1] += clamp_angle(euler.y - self.raw_angles[1])
                self.raw_angles[2] += clamp_angle(euler.z - self.raw_angles[2])
    
            scale = delta_xy.length / self.delta_xy.length
            if self.delta_xy.dot(delta_xy) < 0:
                scale *= -1
            for i in range(3):
                if self.allowed_axes[i]:
                    self.scales[i] = scale
    
            self.prev_delta_xy = delta_xy
    
        def transform_move(self, particle):
    
            global set_cursor_location__reset_stick
    
            src_matrix = particle.get_matrix()
            initial_matrix = particle.get_initial_matrix()
    
            matrix = self.su.snap(
                self.xy, src_matrix, initial_matrix,
                self.do_raycast, self.grid_substep,
                self.vu, self.csu,
                self.modify_surface_orientation,
                self.use_object_centers)
    
            set_cursor_location__reset_stick = False
    
            particle.set_matrix(matrix)
    
            set_cursor_location__reset_stick = True
    
        def rotate_matrix(self, matrix):
            sys_matrix = self.csu.get_matrix()
    
            try:
                matrix = sys_matrix.inverted() * matrix
            except:
                # this is some degenerate system
                pass
    
            # Blender's order of rotation [in local axes]
            rotation_order = [2, 1, 0]
    
            # Seems that 4x4 matrix cannot be rotated using rotate() ?
            sys_matrix3 = sys_matrix.to_3x3()
    
            for i in range(3):
                j = rotation_order[i]
                axis = sys_matrix3[j]
                angle = self.angles[j]
    
                rot = angle_axis_to_quat(angle, axis)
                # this seems to be buggy too
                #rot = Matrix.Rotation(angle, 3, axis)
    
                sys_matrix3 = rot.to_matrix() * sys_matrix3
                # sys_matrix3.rotate has a bug? or I don't understand how it works?
                #sys_matrix3.rotate(rot)
    
            for i in range(3):
                sys_matrix[i][:3] = sys_matrix3[i]
    
            matrix = sys_matrix * matrix
    
            return matrix
    
        def transform_rotate(self, particle):
            grid_step = self.angle_grid_steps[self.grid_substep]
            grid_step *= (math.pi / 180.0)
    
            for i in range(3):
                if self.axes_values[i] and self.axes_eval_success[i]:
                    self.raw_angles[i] = self.axes_coords[i] * (math.pi / 180.0)
    
                self.angles[i] = self.raw_angles[i]
    
            if self.su.implementation.snap_type == 'INCREMENT':
                for i in range(3):
                    self.angles[i] = round_step(self.angles[i], grid_step)
    
            initial_matrix = particle.get_initial_matrix()
            matrix = self.rotate_matrix(initial_matrix)
    
            particle.set_matrix(matrix)
    
        def scale_matrix(self, matrix):
            sys_matrix = self.csu.get_matrix()
    
            try:
                matrix = sys_matrix.inverted() * matrix
            except:
                # this is some degenerate system
                pass
    
            for i in range(3):
                sys_matrix[i] *= self.scales[i]
    
            matrix = sys_matrix * matrix
    
            return matrix
    
        def transform_scale(self, particle):
            grid_step = self.scale_grid_steps[self.grid_substep]
    
            for i in range(3):
                if self.axes_values[i] and self.axes_eval_success[i]:
                    self.scales[i] = self.axes_coords[i]
    
            if self.su.implementation.snap_type == 'INCREMENT':
                for i in range(3):
                    self.scales[i] = round_step(self.scales[i], grid_step)
    
            initial_matrix = particle.get_initial_matrix()
            matrix = self.scale_matrix(initial_matrix)
    
            particle.set_matrix(matrix)
    
        def set_axis_input(self, axis_id, axis_val):
            if axis_val == self.axes_values[axis_id]:
                return
    
            self.axes_values[axis_id] = axis_val
    
            if len(axis_val) == 0:
                self.axes_coords[axis_id] = None
                self.axes_eval_success[axis_id] = True
            else:
                try:
                    #self.axes_coords[axis_id] = float(eval(axis_val, {}, {}))
                    self.axes_coords[axis_id] = \
                        float(eval(axis_val, math.__dict__))
                    self.axes_eval_success[axis_id] = True
                except:
                    self.axes_eval_success[axis_id] = False
    
        def snap_to_system_origin(self, force_pivot=False):
            if self.transform_mode == 'MOVE':
                pivot = self.csu.get_pivot_name(raw=force_pivot)
                p = self.csu.get_origin(relative=False, pivot=pivot)
                m = self.csu.get_matrix()
    
                try:
                    p = m.inverted() * p
                except:
                    # this is some degenerate system
                    pass
    
                for i in range(3):
                    self.set_axis_input(i, str(p[i]))
            elif self.transform_mode == 'ROTATE':
                for i in range(3):
                    self.set_axis_input(i, "0")
            elif self.transform_mode == 'SCALE':
                for i in range(3):
                    self.set_axis_input(i, "1")
    
        def get_axes_values(self, as_string=False):
            if self.transform_mode == 'MOVE':
                localmat = CursorDynamicSettings.local_matrix
    
                raw_axes = localmat.translation
    
            elif self.transform_mode == 'ROTATE':
                raw_axes = Vector(self.angles) * (180.0 / math.pi)
            elif self.transform_mode == 'SCALE':
                raw_axes = Vector(self.scales)
    
            axes_values = []
            for i in range(3):
                if as_string and self.axes_values[i]:
                    value = self.axes_values[i]
                elif self.axes_eval_success[i] and \
                        (self.axes_coords[i] is not None):
                    value = self.axes_coords[i]
                else:
                    value = raw_axes[i]
                    if as_string:
                        value = self.coord_format.format(value)
                axes_values.append(value)
    
            return axes_values
    
        def get_axes_text(self, offset=False):
            axes_values = self.get_axes_values(as_string=True)
    
            axes_text = []
            for i in range(3):
                j = i
                if offset:
                    j = (i + self.current_axis) % 3
    
                axes_text.append(axes_values[j])
    
            return axes_text
    
        def set_axes_text(self, text, offset=False):
            if "\n" in text:
                text = text.replace("\r", "")
            else:
                text = text.replace("\r", "\n")
            text = text.replace("\n", "\t")
            #text = text.replace(",", ".") # ???
    
            axes_text = text.split("\t")
            for i in range(min(len(axes_text), 3)):
                j = i
                if offset:
                    j = (i + self.current_axis) % 3
                self.set_axis_input(j, axes_text[i])
    
        def process_axis_input(self, event):
            axis_id = self.current_axis
            axis_val = self.axes_values[axis_id]
    
            if event.type in self.key_map["remove_next_character"]:
                if event.ctrl:
                    # clear all
                    for i in range(3):
                        self.set_axis_input(i, "")
                    self.caret_pos = 0
                    return
                else:
                    axis_val = axis_val[0:self.caret_pos] + \
                               axis_val[self.caret_pos + 1:len(axis_val)]
            elif event.type in self.key_map["remove_last_character"]:
                if event.ctrl:
                    # clear current
                    axis_val = ""
                else:
                    axis_val = axis_val[0:self.caret_pos - 1] + \
                               axis_val[self.caret_pos:len(axis_val)]
                    self.caret_pos -= 1
            elif event.type in self.key_map["move_caret_next"]:
                self.caret_pos += 1
                if event.ctrl:
                    snap_chars = ".-+*/%()"
                    i = self.caret_pos
                    while axis_val[i:i + 1] not in snap_chars:
                        i += 1
                    self.caret_pos = i
            elif event.type in self.key_map["move_caret_prev"]:
                self.caret_pos -= 1
                if event.ctrl:
                    snap_chars = ".-+*/%()"
                    i = self.caret_pos
                    while axis_val[i - 1:i] not in snap_chars:
                        i -= 1
                    self.caret_pos = i
            elif event.type in self.key_map["move_caret_home"]:
                self.caret_pos = 0
            elif event.type in self.key_map["move_caret_end"]:
                self.caret_pos = len(axis_val)
            elif event.type in self.key_char_map:
                # Currently accessing event.ascii seems to crash Blender
                c = self.key_char_map[event.type]
                if event.shift:
                    if c == "8":
                        c = "*"
                    elif c == "5":
                        c = "%"
                    elif c == "9":
                        c = "("
                    elif c == "0":
                        c = ")"
                axis_val = axis_val[0:self.caret_pos] + c + \
                           axis_val[self.caret_pos:len(axis_val)]
                self.caret_pos += 1
    
            self.caret_pos = min(max(self.caret_pos, 0), len(axis_val))
    
            self.set_axis_input(axis_id, axis_val)
    
        # ====== DRAWING ====== #
        def gizmo_distance(self, pos):
            rv3d = self.vu.region_data
            if rv3d.view_perspective == 'ORTHO':
                dist = rv3d.view_distance
            else:
                view_pos = self.vu.get_viewpoint()
                view_dir = self.vu.get_direction()
                dist = (pos - view_pos).dot(view_dir)
            return dist
    
        def gizmo_scale(self, pos):
            return self.gizmo_distance(pos) * self.gizmo_factor
    
        def check_v3d_local(self, context):
            csu_v3d = self.csu.space_data
            v3d = context.space_data
            if csu_v3d.local_view:
                return csu_v3d != v3d
            return v3d.local_view
    
        def draw_3d(self, context):
            if self.check_v3d_local(context):
                return
    
            if time.time() < (self.click_start + self.click_period):
                return
    
            settings = find_settings()
            tfm_opts = settings.transform_options
    
            initial_matrix = self.particles[0].get_initial_matrix()
    
            sys_matrix = self.csu.get_matrix()
            if tfm_opts.use_relative_coords:
    
                sys_matrix.translation = initial_matrix.translation.copy()
    
            sys_origin = sys_matrix.to_translation()
            dest_point = self.particles[0].get_location()
    
            if self.is_normal_visible():
                p0, x, y, z, _x, _z = \
                    self.get_normal_params(tfm_opts, dest_point)
    
                # use theme colors?
                #ThemeView3D.normal
                #ThemeView3D.vertex_normal