Skip to content
Snippets Groups Projects
space_view3d_enhanced_3d_cursor.py 185 KiB
Newer Older
  • Learn to ignore specific revisions
  •     op_new = "scene.cursor_3d_new_bookmark_library"
        op_delete = "scene.cursor_3d_delete_bookmark_library"
        icon = 'BOOKMARKS'
    
        collection, active, enum = PseudoIDBlockBase.create_props(
            BookmarkLibraryProp, "Bookmark Library")
    
        def on_item_select(self):
            library = self.get_item()
            library.bookmarks.update_enum()
    
    class NewCursor3DBookmarkLibrary(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_new_bookmark_library"
        bl_label = "New Library"
        bl_description = "Add a new bookmark library"
    
        name: bpy.props.StringProperty(
    
            name="Name",
            description="Name of the new library",
            default="Lib")
    
        def execute(self, context):
            settings = find_settings()
    
            settings.libraries.add(name=self.name)
    
            return {'FINISHED'}
    
    class DeleteCursor3DBookmarkLibrary(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_delete_bookmark_library"
        bl_label = "Delete Library"
        bl_description = "Delete active bookmark library"
    
        def execute(self, context):
            settings = find_settings()
    
            name = settings.libraries.active
    
            settings.libraries.remove(key=name)
    
            return {'FINISHED'}
    
    # ===== MAIN PROPERTIES ===== #
    # TODO: ~a bug? Somewhy tooltip shows "Cursor3DToolsSettings.foo"
    # instead of "bpy.types.Screen.cursor_3d_tools_settings.foo"
    class Cursor3DToolsSettings(bpy.types.PropertyGroup):
        transform_options = bpy.props.PointerProperty(
            type=TransformExtraOptionsProp,
            options={'HIDDEN'})
    
        cursor_visible: bpy.props.BoolProperty(
    
            name="Cursor visibility",
            description="Show/hide cursor. When hidden, "\
    "Blender continuously redraws itself (eats CPU like crazy, "\
    "and becomes the less responsive the more complex scene you have)!",
            default=True)
    
    
        cursor_lock = bpy.props.BoolProperty(
            name="Lock cursor location",
            description="Prevent accidental cursor movement",
            default=False)
    
    
        draw_guides = bpy.props.BoolProperty(
            name="Guides",
            description="Display guides",
            default=True)
    
        draw_snap_elements = bpy.props.BoolProperty(
            name="Snap elements",
            description="Display snap elements",
            default=True)
    
        draw_N = bpy.props.BoolProperty(
            name="Surface normal",
            description="Display surface normal",
            default=True)
    
        draw_T1 = bpy.props.BoolProperty(
            name="Surface 1st tangential",
            description="Display 1st surface tangential",
            default=True)
    
        draw_T2 = bpy.props.BoolProperty(
            name="Surface 2nd tangential",
            description="Display 2nd surface tangential",
            default=True)
    
        stick_to_obj = bpy.props.BoolProperty(
            name="Stick to objects",
            description="Move cursor along with object it was snapped to",
            default=True)
    
        # HISTORY-RELATED
        history = bpy.props.PointerProperty(
            type=CursorHistoryProp,
            options={'HIDDEN'})
    
        # BOOKMARK-RELATED
        libraries = bpy.props.PointerProperty(
            type=BookmarkLibraryIDBlock,
            options={'HIDDEN'})
    
        show_bookmarks = bpy.props.BoolProperty(
            name="Show bookmarks",
            description="Show active bookmark in 3D view",
            default=True,
            options={'HIDDEN'})
    
        free_coord_precision = bpy.props.IntProperty(
            name="Coord precision",
            description="Numer of digits afer comma "\
                        "for displayed coordinate values",
            default=4,
            min=0,
            max=10,
            options={'HIDDEN'})
    
        auto_register_keymaps = bpy.props.BoolProperty(
            name="Auto Register Keymaps",
            default=True)
    
    
    class Cursor3DToolsSceneSettings(bpy.types.PropertyGroup):
    
        stick_obj_name: bpy.props.StringProperty(
    
            name="Stick-to-object name",
            description="Name of the object to stick cursor to",
            options={'HIDDEN'})
    
        stick_obj_pos: bpy.props.FloatVectorProperty(
    
            default=(0.0, 0.0, 0.0),
            options={'HIDDEN'},
            subtype='XYZ')
    
    # ===== CURSOR RUNTIME PROPERTIES ===== #
    class CursorRuntimeSettings(bpy.types.PropertyGroup):
    
        current_monitor_id: bpy.props.IntProperty(
    
            default=0,
            options={'HIDDEN'})
    
        surface_pos: bpy.props.FloatVectorProperty(
    
            default=(0.0, 0.0, 0.0),
            options={'HIDDEN'},
            subtype='XYZ')
    
    
        use_cursor_monitor: bpy.props.BoolProperty(
    
    dairin0d's avatar
    dairin0d committed
            name="Enable Cursor Monitor",
            description="Record 3D cursor history "\
                "(uses a background modal operator)",
            default=True)
    
    
    class CursorDynamicSettings:
        local_matrix = Matrix()
    
        active_transform_operator = None
    
        csu = None
    
        active_scene_hash = 0
    
        @classmethod
        def recalc_csu(cls, context, event_value=None):
            scene_hash_changed = (cls.active_scene_hash != hash(context.scene))
            cls.active_scene_hash = hash(context.scene)
    
            # Don't recalc if mouse is over some UI panel!
            # (otherwise, this may lead to applying operator
            # (e.g. Subdivide) in Edit Mode, even if user
            # just wants to change some operator setting)
            clicked = (event_value in {'PRESS', 'RELEASE'}) and \
                (context.region.type == 'WINDOW')
    
            if clicked or scene_hash_changed:
                particles, cls.csu = gather_particles()
    
    #============================================================================#
    # ===== PANELS AND DIALOGS ===== #
    class TransformExtraOptions(bpy.types.Panel):
        bl_label = "Transform Extra Options"
        bl_idname = "OBJECT_PT_transform_extra_options"
        bl_space_type = "VIEW_3D"
        bl_region_type = "UI"
        #bl_context = "object"
    
        def draw(self, context):
            layout = self.layout
    
            settings = find_settings()
            tfm_opts = settings.transform_options
    
            layout.prop(tfm_opts, "use_relative_coords")
            layout.prop(tfm_opts, "snap_only_to_solid")
            layout.prop(tfm_opts, "snap_interpolate_normals_mode", text="")
    
            layout.prop(tfm_opts, "use_comma_separator")
    
            #layout.prop(tfm_opts, "snap_element_screen_size")
    
    class Cursor3DTools(bpy.types.Panel):
        bl_label = "3D Cursor Tools"
        bl_idname = "OBJECT_PT_cursor_3d_tools"
        bl_space_type = "VIEW_3D"
        bl_region_type = "UI"
    
    Brendon Murphy's avatar
    Brendon Murphy committed
        bl_options = {'DEFAULT_CLOSED'}
    
        def draw(self, context):
            layout = self.layout
    
            # Attempt to launch the monitor
            if bpy.ops.view3d.cursor3d_monitor.poll():
                bpy.ops.view3d.cursor3d_monitor()
            #=============================================#
    
    dairin0d's avatar
    dairin0d committed
            wm = context.window_manager
    
            settings = find_settings()
    
            row = layout.split(0.5)
            #row = layout.row()
            row.operator("view3d.set_cursor3d_dialog",
                "Set", 'CURSOR')
            row = row.split(1 / 3, align=True)
            #row = row.row(align=True)
            row.prop(settings, "draw_guides",
                text="", icon='MANIPUL', toggle=True)
            row.prop(settings, "draw_snap_elements",
                text="", icon='EDITMODE_HLT', toggle=True)
            row.prop(settings, "stick_to_obj",
                text="", icon='SNAP_ON', toggle=True)
    
            subrow = row.split(0.5)
    
            subrow.prop(settings, "cursor_lock", text="", toggle=True,
                     icon=('LOCKED' if settings.cursor_lock else 'UNLOCKED'))
    
            subrow = subrow.split(1)
            subrow.alert = True
            subrow.prop(settings, "cursor_visible", text="", toggle=True,
                     icon=('RESTRICT_VIEW_OFF' if settings.cursor_visible
                           else 'RESTRICT_VIEW_ON'))
    
            row = row.split(1 / 3, align=True)
            row.prop(settings, "draw_N",
                text="N", toggle=True, index=0)
            row.prop(settings, "draw_T1",
                text="T1", toggle=True, index=1)
            row.prop(settings, "draw_T2",
                text="T2", toggle=True, index=2)
    
            # === HISTORY === #
            history = settings.history
            row = layout.row(align=True)
    
    dairin0d's avatar
    dairin0d committed
            row.prop(wm.cursor_3d_runtime_settings, "use_cursor_monitor",
                text="", toggle=True, icon='REC')
    
            row.prop(history, "show_trace", text="", icon='SORTTIME')
            row = row.split(0.35, True)
            row.prop(history, "max_size", text="")
            row.prop(history, "current_id", text="")
    
            # === BOOKMARK LIBRARIES === #
            settings.libraries.draw(context, layout)
    
            library = settings.libraries.get_item()
    
            if library is None:
                return
    
            row = layout.row()
            row.prop(settings, "show_bookmarks",
                text="", icon='RESTRICT_VIEW_OFF')
            row = row.row(align=True)
            row.prop(library, "system", text="")
            row.prop(library, "offset", text="",
                icon='ARROW_LEFTRIGHT')
    
            # === BOOKMARKS === #
            library.bookmarks.draw(context, layout)
    
            if len(library.bookmarks.collection) == 0:
                return
    
            row = layout.row()
            row = row.split(align=True)
            # PASTEDOWN
            # COPYDOWN
            row.operator("scene.cursor_3d_overwrite_bookmark",
                text="", icon='REC')
            row.operator("scene.cursor_3d_swap_bookmark",
                text="", icon='FILE_REFRESH')
            row.operator("scene.cursor_3d_recall_bookmark",
                text="", icon='FILE_TICK')
            row.operator("scene.cursor_3d_add_empty_at_bookmark",
                text="", icon='EMPTY_DATA')
            # Not implemented (and maybe shouldn't)
            #row.operator("scene.cursor_3d_snap_selection_to_bookmark",
            #    text="", icon='SNAP_ON')
    
    class SetCursorDialog(bpy.types.Operator):
        bl_idname = "view3d.set_cursor3d_dialog"
        bl_label = "Set 3D Cursor"
        bl_description = "Set 3D Cursor XYZ values"
    
        pos: bpy.props.FloatVectorProperty(
    
            name="Location",
            description="3D Cursor location in current coordinate system",
            subtype='XYZ',
            )
    
        @classmethod
        def poll(cls, context):
            return context.area.type == 'VIEW_3D'
    
        def execute(self, context):
            scene = context.scene
    
            # "current system" / "relative" could have changed
            self.matrix = self.csu.get_matrix()
    
            pos = self.matrix * self.pos
    
            set_cursor_location(pos, v3d=context.space_data)
    
            return {'FINISHED'}
    
        def invoke(self, context, event):
            scene = context.scene
    
            cursor_pos = get_cursor_location(v3d=context.space_data)
    
            particles, self.csu = gather_particles(context=context)
            self.csu.source_pos = cursor_pos
    
            self.matrix = self.csu.get_matrix()
    
            try:
                self.pos = self.matrix.inverted() * cursor_pos
            except:
                # this is some degenerate system
                self.pos = Vector()
    
            wm = context.window_manager
            return wm.invoke_props_dialog(self, width=160)
    
        def draw(self, context):
            layout = self.layout
    
            settings = find_settings()
            tfm_opts = settings.transform_options
    
            v3d = context.space_data
    
            col = layout.column()
            col.prop(self, "pos", text="")
    
            row = layout.row()
            row.prop(tfm_opts, "use_relative_coords", text="Relative")
            row.prop(v3d, "transform_orientation", text="")
    
    
    # Adapted from Chromoly's lock_cursor3d
    def selection_global_positions(context):
        if context.mode == 'EDIT_MESH':
            ob = context.active_object
            mat = ob.matrix_world
            bm = bmesh.from_edit_mesh(ob.data)
            verts = [v for v in bm.verts if v.select]
            return [mat * v.co for v in verts]
        elif context.mode == 'OBJECT':
            return [ob.matrix_world.to_translation()
                    for ob in context.selected_objects]
    
    # Adapted from Chromoly's lock_cursor3d
    def center_of_circumscribed_circle(vecs):
        if len(vecs) == 1:
            return vecs[0]
        elif len(vecs) == 2:
            return (vecs[0] + vecs[1]) / 2
        elif len(vecs) == 3:
            v1, v2, v3 = vecs
            if v1 != v2 and v2 != v3 and v3 != v1:
                v12 = v2 - v1
                v13 = v3 - v1
                med12 = (v1 + v2) / 2
                med13 = (v1 + v3) / 2
                per12 = v13 - v13.project(v12)
                per13 = v12 - v12.project(v13)
                inter = intersect_line_line(med12, med12 + per12,
                                            med13, med13 + per13)
                if inter:
                    return (inter[0] + inter[1]) / 2
            return (v1 + v2 + v3) / 3
        return None
    
    def center_of_inscribed_circle(vecs):
        if len(vecs) == 1:
            return vecs[0]
        elif len(vecs) == 2:
            return (vecs[0] + vecs[1]) / 2
        elif len(vecs) == 3:
            v1, v2, v3 = vecs
            L1 = (v3 - v2).magnitude
            L2 = (v3 - v1).magnitude
            L3 = (v2 - v1).magnitude
            return (L1*v1 + L2*v2 + L3*v3) / (L1 + L2 + L3)
        return None
    
    # Adapted from Chromoly's lock_cursor3d
    class SnapCursor_Circumscribed(bpy.types.Operator):
        bl_idname = "view3d.snap_cursor_to_circumscribed"
        bl_label = "Cursor to Circumscribed"
        bl_description = "Snap cursor to the center of the circumscribed circle"
    
        def execute(self, context):
            vecs = selection_global_positions(context)
            if vecs is None:
                self.report({'WARNING'}, 'Not implemented \
                            for %s mode' % context.mode)
                return {'CANCELLED'}
    
            pos = center_of_circumscribed_circle(vecs)
            if pos is None:
                self.report({'WARNING'}, 'Select 3 objects/elements')
                return {'CANCELLED'}
    
            set_cursor_location(pos, v3d=context.space_data)
    
            return {'FINISHED'}
    
    class SnapCursor_Inscribed(bpy.types.Operator):
        bl_idname = "view3d.snap_cursor_to_inscribed"
        bl_label = "Cursor to Inscribed"
        bl_description = "Snap cursor to the center of the inscribed circle"
    
        def execute(self, context):
            vecs = selection_global_positions(context)
            if vecs is None:
                self.report({'WARNING'}, 'Not implemented \
                            for %s mode' % context.mode)
                return {'CANCELLED'}
    
            pos = center_of_inscribed_circle(vecs)
            if pos is None:
                self.report({'WARNING'}, 'Select 3 objects/elements')
                return {'CANCELLED'}
    
            set_cursor_location(pos, v3d=context.space_data)
    
            return {'FINISHED'}
    
    
    class AlignOrientationProperties(bpy.types.PropertyGroup):
        axes_items = [
            ('X', 'X', 'X axis'),
            ('Y', 'Y', 'Y axis'),
            ('Z', 'Z', 'Z axis'),
            ('-X', '-X', '-X axis'),
            ('-Y', '-Y', '-Y axis'),
            ('-Z', '-Z', '-Z axis'),
        ]
    
        axes_items_ = [
            ('X', 'X', 'X axis'),
            ('Y', 'Y', 'Y axis'),
            ('Z', 'Z', 'Z axis'),
            (' ', ' ', 'Same as source axis'),
        ]
    
        def get_orients(self, context):
            orients = []
            orients.append(('GLOBAL', "Global", ""))
            orients.append(('LOCAL', "Local", ""))
            orients.append(('GIMBAL', "Gimbal", ""))
            orients.append(('NORMAL', "Normal", ""))
            orients.append(('VIEW', "View", ""))
    
            if context is not None:
                for orientation in context.scene.orientations:
                    name = orientation.name
                    orients.append((name, name, ""))
    
            return orients
    
        src_axis: bpy.props.EnumProperty(default='Z', items=axes_items,
    
                                          name="Initial axis")
        #src_orient = bpy.props.EnumProperty(default='GLOBAL', items=get_orients)
    
        dest_axis: bpy.props.EnumProperty(default=' ', items=axes_items_,
    
                                           name="Final axis")
    
        dest_orient: bpy.props.EnumProperty(items=get_orients,
    
                                             name="Final orientation")
    
    
    Dima Glib's avatar
    Dima Glib committed
    class AlignOrientation(bpy.types.Operator):
        bl_idname = "view3d.align_orientation"
        bl_label = "Align Orientation"
    
        bl_description = "Rotates active object to match axis of current "\
            "orientation to axis of another orientation"
    
        bl_options = {'REGISTER', 'UNDO'}
    
        axes_items = [
            ('X', 'X', 'X axis'),
            ('Y', 'Y', 'Y axis'),
            ('Z', 'Z', 'Z axis'),
            ('-X', '-X', '-X axis'),
            ('-Y', '-Y', '-Y axis'),
            ('-Z', '-Z', '-Z axis'),
        ]
    
        axes_items_ = [
            ('X', 'X', 'X axis'),
            ('Y', 'Y', 'Y axis'),
            ('Z', 'Z', 'Z axis'),
            (' ', ' ', 'Same as source axis'),
        ]
    
        axes_ids = {'X':0, 'Y':1, 'Z':2}
    
        def get_orients(self, context):
            orients = []
            orients.append(('GLOBAL', "Global", ""))
            orients.append(('LOCAL', "Local", ""))
            orients.append(('GIMBAL', "Gimbal", ""))
            orients.append(('NORMAL', "Normal", ""))
            orients.append(('VIEW', "View", ""))
    
            if context is not None:
                for orientation in context.scene.orientations:
                    name = orientation.name
                    orients.append((name, name, ""))
    
            return orients
    
        src_axis: bpy.props.EnumProperty(default='Z', items=axes_items,
    
                                          name="Initial axis")
        #src_orient = bpy.props.EnumProperty(default='GLOBAL', items=get_orients)
    
        dest_axis: bpy.props.EnumProperty(default=' ', items=axes_items_,
    
                                           name="Final axis")
    
        dest_orient: bpy.props.EnumProperty(items=get_orients,
    
                                             name="Final orientation")
    
        @classmethod
        def poll(cls, context):
            return (context.area.type == 'VIEW_3D') and context.object
    
        def execute(self, context):
    
            wm = context.window_manager
    
            obj = context.object
            scene = context.scene
            v3d = context.space_data
            rv3d = context.region_data
    
            particles, csu = gather_particles(context=context)
            tou = csu.tou
            #tou = TransformOrientationUtility(scene, v3d, rv3d)
    
            aop = wm.align_orientation_properties # self
    
            src_axes = MatrixDecompose(src_matrix)
    
            src_axis_name = aop.src_axis
    
            if src_axis_name.startswith("-"):
                src_axis_name = src_axis_name[1:]
                src_axis = -src_axes[self.axes_ids[src_axis_name]]
            else:
                src_axis = src_axes[self.axes_ids[src_axis_name]]
    
            tou.set(aop.dest_orient, False)
    
            dest_axes = MatrixDecompose(dest_matrix)
            if self.dest_axis != ' ':
    
                dest_axis_name = aop.dest_axis
    
            else:
                dest_axis_name = src_axis_name
            dest_axis = dest_axes[self.axes_ids[dest_axis_name]]
    
            q = src_axis.rotation_difference(dest_axis)
    
            m = obj.matrix_world.to_3x3()
            m.rotate(q)
            m.resize_4x4()
            m.translation = obj.matrix_world.translation.copy()
    
            obj.matrix_world = m
    
            #bpy.ops.ed.undo_push(message="Align Orientation")
    
            return {'FINISHED'}
    
        # ATTENTION!
        # This _must_ be a dialog, because with 'UNDO' option
        # the last selected orientation may revert to the previous state
    
        def invoke(self, context, event):
            wm = context.window_manager
    
            return wm.invoke_props_dialog(self, width=200)
    
        def draw(self, context):
            layout = self.layout
            wm = context.window_manager
            aop = wm.align_orientation_properties # self
            layout.prop(aop, "src_axis")
            layout.prop(aop, "dest_axis")
            layout.prop(aop, "dest_orient")
    
    class CopyOrientation(bpy.types.Operator):
        bl_idname = "view3d.copy_orientation"
        bl_label = "Copy Orientation"
        bl_description = "Makes a copy of current orientation"
    
        def execute(self, context):
            scene = context.scene
            v3d = context.space_data
            rv3d = context.region_data
    
            particles, csu = gather_particles(context=context)
            tou = csu.tou
            #tou = TransformOrientationUtility(scene, v3d, rv3d)
    
    Dima Glib's avatar
    Dima Glib committed
            orient = create_transform_orientation(scene,
    
                name=tou.get()+".copy", matrix=tou.get_matrix())
    
    Dima Glib's avatar
    Dima Glib committed
            tou.set(orient.name)
    
    Dima Glib's avatar
    Dima Glib committed
    
    def transform_orientations_panel_extension(self, context):
        row = self.layout.row()
        row.operator("view3d.align_orientation", text="Align")
        row.operator("view3d.copy_orientation", text="Copy")
    
    # ===== CURSOR MONITOR ===== #
    class CursorMonitor(bpy.types.Operator):
    
        """Monitor changes in cursor location and write to history"""
    
        bl_idname = "view3d.cursor3d_monitor"
        bl_label = "Cursor Monitor"
    
        # A class-level variable (it must be accessed from poll())
        is_running = False
    
        storage = {}
    
        _handle_view = None
        _handle_px = None
        _handle_header_px = None
    
        @staticmethod
        def handle_add(self, context):
            CursorMonitor._handle_view = bpy.types.SpaceView3D.draw_handler_add(
                draw_callback_view, (self, context), 'WINDOW', 'POST_VIEW')
            CursorMonitor._handle_px = bpy.types.SpaceView3D.draw_handler_add(
                draw_callback_px, (self, context), 'WINDOW', 'POST_PIXEL')
            CursorMonitor._handle_header_px = bpy.types.SpaceView3D.draw_handler_add(
                draw_callback_header_px, (self, context), 'HEADER', 'POST_PIXEL')
    
        @staticmethod
        def handle_remove(context):
            if CursorMonitor._handle_view is not None:
                bpy.types.SpaceView3D.draw_handler_remove(CursorMonitor._handle_view, 'WINDOW')
            if CursorMonitor._handle_px is not None:
                bpy.types.SpaceView3D.draw_handler_remove(CursorMonitor._handle_px, 'WINDOW')
            if CursorMonitor._handle_header_px is not None:
                bpy.types.SpaceView3D.draw_handler_remove(CursorMonitor._handle_header_px, 'HEADER')
            CursorMonitor._handle_view = None
            CursorMonitor._handle_px = None
            CursorMonitor._handle_header_px = None
    
        @classmethod
        def poll(cls, context):
            try:
    
    dairin0d's avatar
    dairin0d committed
                wm = context.window_manager
                if not wm.cursor_3d_runtime_settings.use_cursor_monitor:
                    return False
    
    
                runtime_settings = find_runtime_settings()
                if not runtime_settings:
                    return False
    
                # When addon is enabled by default and
                # user started another new scene, is_running
                # would still be True
                return (not CursorMonitor.is_running) or \
                    (runtime_settings.current_monitor_id == 0)
            except Exception as e:
    
                print("Cursor monitor exeption in poll:\n" + repr(e))
    
                return False
    
        def modal(self, context, event):
    
    dairin0d's avatar
    dairin0d committed
            wm = context.window_manager
            if not wm.cursor_3d_runtime_settings.use_cursor_monitor:
                self.cancel(context)
                return {'CANCELLED'}
    
    
            # Scripts cannot be reloaded while modal operators are running
            # Intercept the corresponding event and shut down CursorMonitor
            # (it would be relaunched automatically afterwards)
            for kmi in CursorMonitor.script_reload_kmis:
                if IsKeyMapItemEvent(kmi, event):
    
    dairin0d's avatar
    dairin0d committed
                    self.cancel(context)
    
            try:
                return self._modal(context, event)
            except Exception as e:
    
                print("Cursor monitor exeption in modal:\n" + repr(e))
    
                # Remove callbacks at any cost
                self.cancel(context)
                #raise
                return {'CANCELLED'}
    
        def _modal(self, context, event):
            runtime_settings = find_runtime_settings()
    
            # ATTENTION: will this work correctly when another
            # blend is loaded? (it should, since all scripts
            # seem to be reloaded in such case)
            if (runtime_settings is None) or \
                    (self.id != runtime_settings.current_monitor_id):
                # Another (newer) monitor was launched;
                # this one should stop.
                # (OR addon was disabled)
    
            # Somewhy after addon re-registration
            # this permanently becomes False
    
            CursorMonitor.is_running = True
    
            if self.update_storage(runtime_settings):
                # hmm... can this cause flickering of menus?
                context.area.tag_redraw()
    
            settings = find_settings()
    
            propagate_settings_to_all_screens(settings)
    
            # ================== #
            # Update bookmark enums when addon is initialized.
            # Since CursorMonitor operator can be called from draw(),
            # we have to postpone all re-registration-related tasks
            # (such as redefining the enums).
            if self.just_initialized:
                # update the relevant enums, bounds and other options
                # (is_running becomes False once another scene is loaded,
                # so this operator gets restarted)
                settings.history.update_max_size()
                settings.libraries.update_enum()
                library = settings.libraries.get_item()
                if library:
                    library.bookmarks.update_enum()
    
                self.just_initialized = False
            # ================== #
    
            # Seems like recalc_csu() in this place causes trouble
            # if space type is switched from 3D to e.g. UV
            '''
    
            tfm_operator = CursorDynamicSettings.active_transform_operator
            if tfm_operator:
                CursorDynamicSettings.csu = tfm_operator.csu
            else:
                CursorDynamicSettings.recalc_csu(context, event.value)
    
            return {'PASS_THROUGH'}
    
        def update_storage(self, runtime_settings):
            if CursorDynamicSettings.active_transform_operator:
                # Don't add to history while operator is running
                return False
    
            new_pos = None
    
            last_locations = {}
    
            for scene in bpy.data.scenes:
    
                # History doesn't depend on view (?)
    
                curr_pos = get_cursor_location(scene=scene)
    
                last_locations[scene.name] = curr_pos
    
                # Ignore newly-created or some renamed scenes
                if scene.name in self.last_locations:
                    if curr_pos != self.last_locations[scene.name]:
                        new_pos = curr_pos
                elif runtime_settings.current_monitor_id == 0:
                    # startup location should be added
                    new_pos = curr_pos
    
            # Seems like scene.cursor_location is fast enough here
            # -> no need to resort to v3d.cursor_location.
            """
            screen = bpy.context.screen
            scene = screen.scene
            v3d = None
            for area in screen.areas:
                for space in area.spaces:
                    if space.type == 'VIEW_3D':
                        v3d = space
                        break
    
            if v3d is not None:
    
                curr_pos = get_cursor_location(v3d=v3d)
    
                last_locations[scene.name] = curr_pos
    
                # Ignore newly-created or some renamed scenes
                if scene.name in self.last_locations:
                    if curr_pos != self.last_locations[scene.name]:
                        new_pos = curr_pos
            """
    
            self.last_locations = last_locations
    
            if new_pos is not None:
                settings = find_settings()
                history = settings.history
    
                pos = history.get_pos()
                if (pos is not None):# and (history.current_id != 0): # ?
                    if pos == new_pos:
                        return False # self.just_initialized ?
    
                entry = history.entries.add()
                entry.pos = new_pos
    
                last_id = len(history.entries) - 1
                history.entries.move(last_id, 0)
    
                if last_id > int(history.max_size):
                    history.entries.remove(last_id)
    
                # make sure the most recent history entry is displayed
    
                CursorHistoryProp.update_cursor_on_id_change = False
    
                history.current_id = 0
    
                CursorHistoryProp.update_cursor_on_id_change = True
    
                history.curr_id = history.current_id
                history.last_id = 1
    
                return True
    
            return False # self.just_initialized ?
    
        def execute(self, context):
            print("Cursor monitor: launched")
    
            CursorMonitor.script_reload_kmis = list(KeyMapItemSearch('script.reload'))
    
    
            runtime_settings = find_runtime_settings()
    
            self.just_initialized = True
    
            self.id = 0
    
            self.last_locations = {}
    
            # Important! Call update_storage() before assigning
            # current_monitor_id (used to add startup cursor location)
            self.update_storage(runtime_settings)
    
            # Indicate that this is the most recent monitor.
            # All others should shut down.
            self.id = runtime_settings.current_monitor_id + 1
            runtime_settings.current_monitor_id = self.id
    
            CursorMonitor.is_running = True
    
            CursorDynamicSettings.recalc_csu(context, 'PRESS')
    
            # I suppose that cursor position would change
            # only with user interaction.
            #self._timer = context.window_manager. \
            #    event_timer_add(0.1, context.window)
    
            # Here we cannot return 'PASS_THROUGH',
            # or Blender will crash!
    
    Campbell Barton's avatar
    Campbell Barton committed
    
            # Currently there seems to be only one window
            context.window_manager.modal_handler_add(self)
    
            return {'RUNNING_MODAL'}
    
        def cancel(self, context):
            CursorMonitor.is_running = False
            #type(self).is_running = False
    
            # Unregister callbacks...
    
    
    
    # ===== MATH / GEOMETRY UTILITIES ===== #
    def to_matrix4x4(orient, pos):
        if not isinstance(orient, Matrix):
            orient = orient.to_matrix()
        m = orient.to_4x4()
    
        m.translation = pos.to_3d()
    
    def MatrixCompose(*args):
        size = len(args)
        m = Matrix.Identity(size)
        axes = m.col # m.row
    
        if size == 2:
            for i in (0, 1):
                c = args[i]
                if isinstance(c, Vector):
                    axes[i] = c.to_2d()
                elif hasattr(c, "__iter__"):
                    axes[i] = Vector(c).to_2d()
                else:
                    axes[i][i] = c
        else:
            for i in (0, 1, 2):
                c = args[i]
                if isinstance(c, Vector):
                    axes[i][:3] = c.to_3d()
                elif hasattr(c, "__iter__"):
                    axes[i][:3] = Vector(c).to_3d()
                else:
                    axes[i][i] = c
    
            if size == 4:
                c = args[3]
                if isinstance(c, Vector):
                    m.translation = c.to_3d()
                elif hasattr(c, "__iter__"):
                    m.translation = Vector(c).to_3d()
    
        return m
    
    def MatrixDecompose(m, res_size=None):
        size = len(m)
        axes = m.col # m.row
        if res_size is None:
            res_size = size
    
        if res_size == 2:
            return (axes[0].to_2d(), axes[1].to_2d())
        else:
            x = axes[0].to_3d()
            y = axes[1].to_3d()
            z = (axes[2].to_3d() if size > 2 else Vector())
            if res_size == 3:
                return (x, y, z)
    
            t = (m.translation.to_3d() if size == 4 else Vector())
            if res_size == 4:
                return (x, y, z, t)
    
    
    def angle_axis_to_quat(angle, axis):
        w = math.cos(angle / 2.0)
        xyz = axis.normalized() * math.sin(angle / 2.0)
        return Quaternion((w, xyz.x, xyz.y, xyz.z))
    
    def round_step(x, s=1.0):
        #return math.floor(x * s + 0.5) / s
        return math.floor(x / s + 0.5) * s
    
    twoPi = 2.0 * math.pi
    def clamp_angle(ang):
    
    Campbell Barton's avatar
    Campbell Barton committed
        # Attention! In Python the behaviour is:
        # -359.0 % 180.0 == 1.0
        # -359.0 % -180.0 == -179.0
        ang = (ang % twoPi)
        return ((ang - twoPi) if (ang > math.pi) else ang)
    
    def prepare_grid_mesh(bm, nx=1, ny=1, sx=1.0, sy=1.0,
                          z=0.0, xyz_indices=(0,1,2)):
    
        vertices = []
        for i in range(nx + 1):
            x = 2 * (i / nx) - 1
            x *= sx
            for j in range(ny + 1):
                y = 2 * (j / ny) - 1
                y *= sy
                pos = (x, y, z)
    
                vert = bm.verts.new((pos[xyz_indices[0]],
                                     pos[xyz_indices[1]],
                                     pos[xyz_indices[2]]))
                vertices.append(vert)
    
        nxmax = nx + 1
        for i in range(nx):
            i1 = i + 1
            for j in range(ny):
                j1 = j + 1
    
                verts = [vertices[j + i * nxmax],
                         vertices[j1 + i * nxmax],
                         vertices[j1 + i1 * nxmax],
                         vertices[j + i1 * nxmax]]
                bm.faces.new(verts)
        #return
    
    
    def prepare_gridbox_mesh(subdiv=1):
    
        sides = [
            (-1, (0,1,2)), # -Z
            (1, (1,0,2)), # +Z
            (-1, (1,2,0)), # -Y
            (1, (0,2,1)), # +Y
            (-1, (2,0,1)), # -X
            (1, (2,1,0)), # +X
            ]
    
        for side in sides: