Skip to content
Snippets Groups Projects
space_view3d_enhanced_3d_cursor.py 185 KiB
Newer Older
  • Learn to ignore specific revisions
  •     variants_enum = {'RAW', 'PREVIEW', 'RENDER'}
        variants_normalization = {
            'MESH':{},
            'CURVE':{},
            'SURFACE':{},
            'FONT':{},
            'META':{'RAW':'PREVIEW'},
            'ARMATURE':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
            'LATTICE':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
            'EMPTY':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
            'CAMERA':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
            'LAMP':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
            'SPEAKER':{'RAW':'PREVIEW', 'RENDER':'PREVIEW'},
        }
        conversible_types = {'MESH', 'CURVE', 'SURFACE', 'FONT',
                             'META', 'ARMATURE', 'LATTICE'}
        convert_types = conversible_types
    
        def __init__(self, context, convert_types=None):
            self.collection = context.collection
            self.scene = context.scene
    
            if convert_types:
                self.convert_types = convert_types
            self.cached = {}
    
        def clear(self, expect_zero_users=False):
            for cache_item in self.cached.values():
                if cache_item:
                    try:
                        cache_item.dispose()
                    except RuntimeError:
                        if expect_zero_users:
                            raise
            self.cached.clear()
    
        def __delitem__(self, obj):
            cache_item = self.cached.pop(obj, None)
            if cache_item:
                cache_item.dispose()
    
        def __contains__(self, obj):
            return obj in self.cached
    
        def __getitem__(self, obj):
            if isinstance(obj, tuple):
                return self.get(*obj)
            return self.get(obj)
    
        def get(self, obj, variant='PREVIEW', reuse=True):
            if variant not in self.variants_enum:
                raise ValueError("Mesh variant must be one of %s" %
                                 self.variants_enum)
    
            # Make sure the variant is proper for this type of object
            variant = (self.variants_normalization[obj.type].
                       get(variant, variant))
    
            if obj in self.cached:
                cache_item = self.cached[obj]
                try:
                    # cache_item is None if object isn't conversible to mesh
                    return (None if (cache_item is None)
                            else cache_item[variant])
                except KeyError:
                    pass
            else:
                cache_item = None
    
            if obj.type not in self.conversible_types:
                self.cached[obj] = None
                return None
    
            if not cache_item:
                cache_item = MeshCacheItem()
                self.cached[obj] = cache_item
    
            conversion = self._convert(obj, variant, reuse)
            cache_item[variant] = conversion
    
        def _convert(self, obj, variant, reuse=True):
            obj_type = obj.type
            obj_mode = obj.mode
            data = obj.data
    
            if obj_type == 'MESH':
                if reuse and ((variant == 'RAW') or (len(obj.modifiers) == 0)):
                    return (obj, False)
                else:
    
                    force_objectmode = (obj_mode in {'EDIT', 'SCULPT'})
    
                    return (self._to_mesh(obj, variant, force_objectmode), True)
    
            elif obj_type in {'CURVE', 'SURFACE', 'FONT'}:
    
                if variant == 'RAW':
                    bm = bmesh.new()
                    for spline in data.splines:
                        for point in spline.bezier_points:
                            bm.verts.new(point.co)
                            bm.verts.new(point.handle_left)
                            bm.verts.new(point.handle_right)
                        for point in spline.points:
                            bm.verts.new(point.co[:3])
                    return (self._make_obj(bm, obj), True)
                else:
                    if variant == 'RENDER':
                        resolution_u = data.resolution_u
                        resolution_v = data.resolution_v
                        if data.render_resolution_u != 0:
                            data.resolution_u = data.render_resolution_u
                        if data.render_resolution_v != 0:
                            data.resolution_v = data.render_resolution_v
    
                    result = (self._to_mesh(obj, variant), True)
    
                    if variant == 'RENDER':
                        data.resolution_u = resolution_u
                        data.resolution_v = resolution_v
    
                    return result
            elif obj_type == 'META':
                if variant == 'RAW':
                    # To avoid the hassle of snapping metaelements
                    # to themselves, we just create an empty mesh
                    bm = bmesh.new()
                    return (self._make_obj(bm, obj), True)
                else:
                    if variant == 'RENDER':
                        resolution = data.resolution
                        data.resolution = data.render_resolution
    
                    result = (self._to_mesh(obj, variant), True)
    
                    if variant == 'RENDER':
                        data.resolution = resolution
    
                    return result
            elif obj_type == 'ARMATURE':
                bm = bmesh.new()
                if obj_mode == 'EDIT':
                    for bone in data.edit_bones:
                        head = bm.verts.new(bone.head)
                        tail = bm.verts.new(bone.tail)
                        bm.edges.new((head, tail))
                elif obj_mode == 'POSE':
                    for bone in obj.pose.bones:
                        head = bm.verts.new(bone.head)
                        tail = bm.verts.new(bone.tail)
                        bm.edges.new((head, tail))
                else:
                    for bone in data.bones:
                        head = bm.verts.new(bone.head_local)
                        tail = bm.verts.new(bone.tail_local)
                        bm.edges.new((head, tail))
                return (self._make_obj(bm, obj), True)
            elif obj_type == 'LATTICE':
                bm = bmesh.new()
                for point in data.points:
                    bm.verts.new(point.co_deform)
                return (self._make_obj(bm, obj), True)
    
        def _to_mesh(self, obj, variant, force_objectmode=False):
            tmp_name = chr(0x10ffff) # maximal Unicode value
    
            with ToggleObjectMode(force_objectmode):
                if variant == 'RAW':
                    mesh = obj.to_mesh(self.scene, False, 'PREVIEW')
                else:
                    mesh = obj.to_mesh(self.scene, True, variant)
                mesh.name = tmp_name
    
        def _make_obj(self, mesh, src_obj):
            tmp_name = chr(0x10ffff) # maximal Unicode value
    
            if isinstance(mesh, bmesh.types.BMesh):
                bm = mesh
                mesh = bpy.data.meshes.new(tmp_name)
                bm.to_mesh(mesh)
    
            tmp_obj = bpy.data.objects.new(tmp_name, mesh)
    
            if src_obj:
                tmp_obj.matrix_world = src_obj.matrix_world
    
                # This is necessary for correct bbox display # TODO
                # (though it'd be better to change the logic in the raycasting)
    
                tmp_obj.show_in_front = src_obj.show_in_front
    
                tmp_obj.instance_faces_scale = src_obj.instance_faces_scale
                tmp_obj.instance_collection = src_obj.instance_collection
    
                #tmp_obj.dupli_list = src_obj.dupli_list
    
                tmp_obj.instance_type = src_obj.instance_type
    
            # Make Blender recognize object as having geometry
            # (is there a simpler way to do this?)
    
            self.collection.objects.link(tmp_obj)
    
            self.scene.update()
            # We don't need this object in scene
    
            self.collection.objects.unlink(tmp_obj)
    
    
    #============================================================================#
    
    # A base class for emulating ID-datablock behavior
    class PseudoIDBlockBase(bpy.types.PropertyGroup):
        # TODO: use normal metaprogramming?
    
        @staticmethod
        def create_props(type, name, options={'ANIMATABLE'}):
            def active_update(self, context):
                # necessary to avoid recursive calls
                if self._self_update[0]:
                    return
    
                if self._dont_rename[0]:
                    return
    
                if len(self.collection) == 0:
                    return
    
                # prepare data for renaming...
                old_key = (self.enum if self.enum else self.collection[0].name)
                new_key = (self.active if self.active else "Untitled")
    
                if old_key == new_key:
                    return
    
                old_item = None
                new_item = None
                existing_names = []
    
                for item in self.collection:
                    if (item.name == old_key) and (not new_item):
                        new_item = item
                    elif (item.name == new_key) and (not old_item):
                        old_item = item
                    else:
                        existing_names.append(item.name)
                existing_names.append(new_key)
    
                # rename current item
                new_item.name = new_key
    
                if old_item:
                    # rename other item if it has that name
                    name = new_key
                    i = 1
                    while name in existing_names:
                        name = "{}.{:0>3}".format(new_key, i)
                        i += 1
                    old_item.name = name
    
                # update the enum
                self._self_update[0] += 1
                self.update_enum()
                self._self_update[0] -= 1
            # end def
    
            def enum_update(self, context):
                # necessary to avoid recursive calls
                if self._self_update[0]:
                    return
    
                self._dont_rename[0] = True
                self.active = self.enum
                self._dont_rename[0] = False
    
                self.on_item_select()
            # end def
    
            collection = bpy.props.CollectionProperty(
    
                type=type)
    
            active = bpy.props.StringProperty(
    
                name="Name",
                description="Name of the active {}".format(name),
                options=options,
                update=active_update)
    
            enum = bpy.props.EnumProperty(
    
                items=[],
                name="Choose",
                description="Choose {}".format(name),
                default=set(),
                options={'ENUM_FLAG'},
                update=enum_update)
    
            return collection, active, enum
        # end def
    
        def add(self, name="", **kwargs):
            if not name:
                name = 'Untitled'
            _name = name
    
            existing_names = [item.name for item in self.collection]
            i = 1
            while name in existing_names:
                name = "{}.{:0>3}".format(_name, i)
                i += 1
    
            instance = self.collection.add()
            instance.name = name
    
            for key, value in kwargs.items():
                setattr(instance, key, value)
    
            self._self_update[0] += 1
            self.active = name
            self.update_enum()
            self._self_update[0] -= 1
    
            return instance
    
        def remove(self, key):
            if isinstance(key, int):
                i = key
            else:
                i = self.indexof(key)
    
            # Currently remove() ignores non-existing indices...
            # In the case this behavior changes, we have the try block.
            try:
                self.collection.remove(i)
            except:
                pass
    
            self._self_update[0] += 1
            if len(self.collection) != 0:
                i = min(i, len(self.collection) - 1)
                self.active = self.collection[i].name
            else:
                self.active = ""
            self.update_enum()
            self._self_update[0] -= 1
    
        def get_item(self, key=None):
            if key is None:
                i = self.indexof(self.active)
            elif isinstance(key, int):
                i = key
            else:
                i = self.indexof(key)
    
            try:
                return self.collection[i]
            except:
                return None
    
        def indexof(self, key):
            return next((i for i, v in enumerate(self.collection) \
                if v.name == key), -1)
    
            # Which is more Pythonic?
    
            #for i, item in enumerate(self.collection):
            #    if item.name == key:
            #        return i
            #return -1 # non-existing index
    
        def update_enum(self):
            names = []
            items = []
            for item in self.collection:
                names.append(item.name)
                items.append((item.name, item.name, ""))
    
            prop_class, prop_params = type(self).enum
            prop_params["items"] = items
            if len(items) == 0:
                prop_params["default"] = set()
                prop_params["options"] = {'ENUM_FLAG'}
            else:
                # Somewhy active may be left from previous times,
                # I don't want to dig now why that happens.
                if self.active not in names:
                    self.active = items[0][0]
                prop_params["default"] = self.active
                prop_params["options"] = set()
    
            # Can this cause problems? In the near future, shouldn't...
            type(self).enum = (prop_class, prop_params)
            #type(self).enum = bpy.props.EnumProperty(**prop_params)
    
            if len(items) != 0:
                self.enum = self.active
    
        def on_item_select(self):
            pass
    
        data_name = ""
        op_new = ""
        op_delete = ""
        icon = 'DOT'
    
        def draw(self, context, layout):
            if len(self.collection) == 0:
                if self.op_new:
                    layout.operator(self.op_new, icon=self.icon)
                else:
                    layout.label(
                        text="({})".format(self.data_name),
                        icon=self.icon)
                return
    
            row = layout.row(align=True)
            row.prop_menu_enum(self, "enum", text="", icon=self.icon)
            row.prop(self, "active", text="")
            if self.op_new:
    
                row.operator(self.op_new, text="", icon='ADD')
    
            if self.op_delete:
                row.operator(self.op_delete, text="", icon='X')
    # end class
    #============================================================================#
    # ===== PROPERTY DEFINITIONS ===== #
    
    # ===== TRANSFORM EXTRA OPTIONS ===== #
    class TransformExtraOptionsProp(bpy.types.PropertyGroup):
    
        use_relative_coords: bpy.props.BoolProperty(
    
            name="Relative coordinates",
            description="Consider existing transformation as the starting point",
    
            default=True)
    
        snap_interpolate_normals_mode: bpy.props.EnumProperty(
    
            items=[('NEVER', "Never", "Don't interpolate normals"),
                   ('ALWAYS', "Always", "Always interpolate normals"),
                   ('SMOOTH', "Smoothness-based", "Interpolate normals only "\
                   "for faces with smooth shading"),],
    
            name="Normal interpolation",
            description="Normal interpolation mode for snapping",
    
            default='SMOOTH')
    
        snap_only_to_solid: bpy.props.BoolProperty(
    
            name="Snap only to solid",
            description="Ignore wireframe/non-solid objects during snapping",
    
            default=False)
    
        snap_element_screen_size: bpy.props.IntProperty(
    
            name="Snap distance",
            description="Radius in pixels for snapping to edges/vertices",
    
            default=8,
            min=2,
            max=64)
    
        use_comma_separator: bpy.props.BoolProperty(
    
            name="Use comma separator",
            description="Use comma separator when copying/pasting"\
                        "coordinate values (instead of Tab character)",
            default=True,
            options={'HIDDEN'})
    
    
    # ===== 3D VECTOR LOCATION ===== #
    class LocationProp(bpy.types.PropertyGroup):
    
        pos: bpy.props.FloatVectorProperty(
    
            name="xyz", description="xyz coords",
            options={'HIDDEN'}, subtype='XYZ')
    
    # ===== HISTORY ===== #
    def update_history_max_size(self, context):
        settings = find_settings()
    
        history = settings.history
    
        prop_class, prop_params = type(history).current_id
        old_max = prop_params["max"]
    
        size = history.max_size
        try:
            int_size = int(size)
            int_size = max(int_size, 0)
            int_size = min(int_size, history.max_size_limit)
        except:
            int_size = old_max
    
        if old_max != int_size:
            prop_params["max"] = int_size
            type(history).current_id = (prop_class, prop_params)
    
        # also: clear immediately?
        for i in range(len(history.entries) - 1, int_size, -1):
            history.entries.remove(i)
    
        if str(int_size) != size:
            # update history.max_size if it's not inside the limits
            history.max_size = str(int_size)
    
    def update_history_id(self, context):
        scene = bpy.context.scene
    
        settings = find_settings()
        history = settings.history
    
        pos = history.get_pos()
        if pos is not None:
    
            # History doesn't depend on view (?)
    
            cursor_pos = get_cursor_location(scene=scene)
    
            if CursorHistoryProp.update_cursor_on_id_change:
                # Set cursor position anyway (we're changing v3d's
                # cursor, which may be separate from scene's)
                # This, however, should be done cautiously
                # from scripts, since, e.g., CursorMonitor
                # can supply wrong context -> cursor will be set
                # in a different view than required
                set_cursor_location(pos, v3d=context.space_data)
    
            if pos != cursor_pos:
                if (history.current_id == 0) and (history.last_id <= 1):
                    history.last_id = 1
                else:
                    history.last_id = history.curr_id
                history.curr_id = history.current_id
    
    
    class CursorHistoryBackward(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_history_backward"
        bl_label = "Cursor History Backward"
        bl_description = "Jump to previous position in cursor history"
    
        def execute(self, context):
            settings = find_settings()
            history = settings.history
            history.current_id += 1 # max is oldest
            return {'FINISHED'}
    
    class CursorHistoryForward(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_history_forward"
        bl_label = "Cursor History Forward"
        bl_description = "Jump to next position in cursor history"
    
        def execute(self, context):
            settings = find_settings()
            history = settings.history
            history.current_id -= 1 # 0 is newest
            return {'FINISHED'}
    
    
    class CursorHistoryProp(bpy.types.PropertyGroup):
        max_size_limit = 500
    
        update_cursor_on_id_change = True
    
        show_trace: bpy.props.BoolProperty(
    
            name="Trace",
            description="Show history trace",
            default=False)
    
        max_size: bpy.props.StringProperty(
    
            name="Size",
            description="History max size",
            default=str(50),
            update=update_history_max_size)
    
        current_id: bpy.props.IntProperty(
    
            name="Index",
            description="Current position in cursor location history",
            default=50,
            min=0,
            max=50,
            update=update_history_id)
    
        entries: bpy.props.CollectionProperty(
    
            type=LocationProp)
    
        curr_id: bpy.props.IntProperty(options={'HIDDEN'})
        last_id: bpy.props.IntProperty(options={'HIDDEN'})
    
        def get_pos(self, id = None):
            if id is None:
                id = self.current_id
    
            id = min(max(id, 0), len(self.entries) - 1)
    
            if id < 0:
                # history is empty
                return None
    
            return self.entries[id].pos
    
        # for updating the upper bound on file load
        def update_max_size(self):
            prop_class, prop_params = type(self).current_id
            # self.max_size expected to be always a correct integer
            prop_params["max"] = int(self.max_size)
            type(self).current_id = (prop_class, prop_params)
    
        def draw_trace(self, context):
            bgl.glColor4f(0.75, 1.0, 0.75, 1.0)
            bgl.glBegin(bgl.GL_LINE_STRIP)
            for entry in self.entries:
                p = entry.pos
                bgl.glVertex3f(p[0], p[1], p[2])
            bgl.glEnd()
    
        def draw_offset(self, context):
            bgl.glShadeModel(bgl.GL_SMOOTH)
    
            tfm_operator = CursorDynamicSettings.active_transform_operator
    
            bgl.glBegin(bgl.GL_LINE_STRIP)
    
            if tfm_operator:
                p = tfm_operator.particles[0]. \
                    get_initial_matrix().to_translation()
            else:
                p = self.get_pos(self.last_id)
            bgl.glColor4f(1.0, 0.75, 0.5, 1.0)
            bgl.glVertex3f(p[0], p[1], p[2])
    
            p = get_cursor_location(v3d=context.space_data)
    
            bgl.glColor4f(1.0, 1.0, 0.25, 1.0)
            bgl.glVertex3f(p[0], p[1], p[2])
    
            bgl.glEnd()
    
    # ===== BOOKMARK ===== #
    class BookmarkProp(bpy.types.PropertyGroup):
    
        name: bpy.props.StringProperty(
    
            name="name", description="bookmark name",
            options={'HIDDEN'})
    
        pos: bpy.props.FloatVectorProperty(
    
            name="xyz", description="xyz coords",
            options={'HIDDEN'}, subtype='XYZ')
    
    class BookmarkIDBlock(PseudoIDBlockBase):
        # Somewhy instance members aren't seen in update()
        # callbacks... but class members are.
        _self_update = [0]
        _dont_rename = [False]
    
        data_name = "Bookmark"
        op_new = "scene.cursor_3d_new_bookmark"
        op_delete = "scene.cursor_3d_delete_bookmark"
        icon = 'CURSOR'
    
        collection, active, enum = PseudoIDBlockBase.create_props(
            BookmarkProp, "Bookmark")
    
    class NewCursor3DBookmark(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_new_bookmark"
        bl_label = "New Bookmark"
        bl_description = "Add a new bookmark"
    
        name: bpy.props.StringProperty(
    
            name="Name",
            description="Name of the new bookmark",
            default="Mark")
    
        @classmethod
        def poll(cls, context):
            return context.area.type == 'VIEW_3D'
    
        def execute(self, context):
            settings = find_settings()
            library = settings.libraries.get_item()
            if not library:
                return {'CANCELLED'}
    
            bookmark = library.bookmarks.add(name=self.name)
    
            cusor_pos = get_cursor_location(v3d=context.space_data)
    
                bookmark.pos = library.convert_from_abs(context.space_data,
                                                        cusor_pos, True)
    
            except Exception as exc:
    
                self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
    
                return {'CANCELLED'}
    
            return {'FINISHED'}
    
    class DeleteCursor3DBookmark(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_delete_bookmark"
        bl_label = "Delete Bookmark"
        bl_description = "Delete active bookmark"
    
        def execute(self, context):
            settings = find_settings()
            library = settings.libraries.get_item()
            if not library:
                return {'CANCELLED'}
    
            name = library.bookmarks.active
    
            library.bookmarks.remove(key=name)
    
            return {'FINISHED'}
    
    class OverwriteCursor3DBookmark(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_overwrite_bookmark"
        bl_label = "Overwrite"
        bl_description = "Overwrite active bookmark "\
            "with the current cursor location"
    
        @classmethod
        def poll(cls, context):
            return context.area.type == 'VIEW_3D'
    
        def execute(self, context):
            settings = find_settings()
            library = settings.libraries.get_item()
            if not library:
                return {'CANCELLED'}
    
            bookmark = library.bookmarks.get_item()
            if not bookmark:
                return {'CANCELLED'}
    
            cusor_pos = get_cursor_location(v3d=context.space_data)
    
                bookmark.pos = library.convert_from_abs(context.space_data,
                                                        cusor_pos, True)
    
            except Exception as exc:
    
                self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
    
                return {'CANCELLED'}
    
            CursorDynamicSettings.recalc_csu(context, 'PRESS')
    
            return {'FINISHED'}
    
    class RecallCursor3DBookmark(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_recall_bookmark"
        bl_label = "Recall"
        bl_description = "Move cursor to the active bookmark"
    
        @classmethod
        def poll(cls, context):
            return context.area.type == 'VIEW_3D'
    
        def execute(self, context):
            settings = find_settings()
            library = settings.libraries.get_item()
            if not library:
                return {'CANCELLED'}
    
            bookmark = library.bookmarks.get_item()
            if not bookmark:
                return {'CANCELLED'}
    
                bookmark_pos = library.convert_to_abs(context.space_data,
                                                      bookmark.pos, True)
                set_cursor_location(bookmark_pos, v3d=context.space_data)
    
            except Exception as exc:
    
                self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
    
                return {'CANCELLED'}
    
            CursorDynamicSettings.recalc_csu(context)
    
            return {'FINISHED'}
    
    class SwapCursor3DBookmark(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_swap_bookmark"
        bl_label = "Swap"
        bl_description = "Swap cursor position with the active bookmark"
    
        @classmethod
        def poll(cls, context):
            return context.area.type == 'VIEW_3D'
    
        def execute(self, context):
            settings = find_settings()
            library = settings.libraries.get_item()
            if not library:
                return {'CANCELLED'}
    
            bookmark = library.bookmarks.get_item()
            if not bookmark:
                return {'CANCELLED'}
    
            cusor_pos = get_cursor_location(v3d=context.space_data)
    
                bookmark_pos = library.convert_to_abs(context.space_data,
                                                      bookmark.pos, True)
    
                set_cursor_location(bookmark_pos, v3d=context.space_data)
    
                bookmark.pos = library.convert_from_abs(context.space_data,
                                                        cusor_pos, True,
    
                    use_history=False)
            except Exception as exc:
    
                self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
    
                return {'CANCELLED'}
    
            CursorDynamicSettings.recalc_csu(context)
    
            return {'FINISHED'}
    
    # Will this be used?
    class SnapSelectionToCursor3DBookmark(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_snap_selection_to_bookmark"
        bl_label = "Snap Selection"
        bl_description = "Snap selection to the active bookmark"
    
    # Will this be used?
    class AddEmptyAtCursor3DBookmark(bpy.types.Operator):
        bl_idname = "scene.cursor_3d_add_empty_at_bookmark"
        bl_label = "Add Empty"
        bl_description = "Add new Empty at the active bookmark"
    
        @classmethod
        def poll(cls, context):
            return context.area.type == 'VIEW_3D'
    
        def execute(self, context):
            settings = find_settings()
            library = settings.libraries.get_item()
            if not library:
                return {'CANCELLED'}
    
            bookmark = library.bookmarks.get_item()
            if not bookmark:
                return {'CANCELLED'}
    
                matrix = library.get_matrix(use_history=False,
                                            v3d=context.space_data, warn=True)
    
                bookmark_pos = matrix * bookmark.pos
            except Exception as exc:
    
                self.report({'ERROR_INVALID_CONTEXT'}, exc.args[0])
    
                return {'CANCELLED'}
    
            name = "{}.{}".format(library.name, bookmark.name)
            obj = bpy.data.objects.new(name, None)
            obj.matrix_world = to_matrix4x4(matrix, bookmark_pos)
    
            context.collection.objects.link(obj)
    
            """
            for sel_obj in list(context.selected_objects):
                sel_obj.select = False
            obj.select = True
            context.scene.objects.active = obj
    
            # We need this to update bookmark position if
            # library's system is local/scaled/normal/etc.
            CursorDynamicSettings.recalc_csu(context, "PRESS")
            """
    
            # TODO: exit from editmode? It has separate history!
            # If we just link object to scene, it will not trigger
            # addition of new entry to Undo history
            bpy.ops.ed.undo_push(message="Add Object")
    
            return {'FINISHED'}
    
    # ===== BOOKMARK LIBRARY ===== #
    class BookmarkLibraryProp(bpy.types.PropertyGroup):
    
        name: bpy.props.StringProperty(
    
            name="Name", description="Name of the bookmark library",
            options={'HIDDEN'})
        bookmarks = bpy.props.PointerProperty(
            type=BookmarkIDBlock,
            options={'HIDDEN'})
    
        system: bpy.props.EnumProperty(
    
            items=[
                ('GLOBAL', "Global", "Global (absolute) coordinates"),
                ('LOCAL', "Local", "Local coordinate system, "\
                    "relative to the active object"),
                ('SCALED', "Scaled", "Scaled local coordinate system, "\
                    "relative to the active object"),
                ('NORMAL', "Normal", "Normal coordinate system, "\
                    "relative to the selected elements"),
                ('CONTEXT', "Context", "Current transform orientation; "\
                    "origin depends on selection"),
            ],
            default="GLOBAL",
            name="System",
            description="Coordinate system in which to store/recall "\
                        "cursor locations",
            options={'HIDDEN'})
    
        offset: bpy.props.BoolProperty(
    
            name="Offset",
            description="Store/recall relative to the last cursor position",
            default=False,
            options={'HIDDEN'})
    
        # Returned None means "operation is not aplicable"
    
        def get_matrix(self, use_history, v3d, warn=True, **kwargs):
    
            #particles, csu = gather_particles(**kwargs)
    
            # Ensure we have relevant CSU (Blender will crash
            # if we use the old one after Undo/Redo)
            CursorDynamicSettings.recalc_csu(bpy.context)
    
            csu = CursorDynamicSettings.csu
    
            if self.offset:
                # history? or keep separate for each scene?
                if not use_history:
    
                    csu.source_pos = get_cursor_location(v3d=v3d)
    
                else:
                    settings = find_settings()
                    history = settings.history
                    csu.source_pos = history.get_pos(history.last_id)
            else:
                csu.source_pos = Vector()
    
            active_obj = csu.tou.view_layer.objects.active
    
            if self.system == 'GLOBAL':
                sys_name = 'GLOBAL'
                pivot = 'WORLD'
            elif self.system == 'LOCAL':
                if not active_obj:
                    if warn:
                        raise Exception("There is no active object")
                    return None
                sys_name = 'LOCAL'
                pivot = 'ACTIVE'
            elif self.system == 'SCALED':
                if not active_obj:
                    if warn:
                        raise Exception("There is no active object")
                    return None
                sys_name = 'Scaled'
                pivot = 'ACTIVE'
            elif self.system == 'NORMAL':
    
                if not active_obj or active_obj.mode != 'EDIT':
    
                    if warn:
                        raise Exception("Active object must be in Edit mode")
                    return None
                sys_name = 'NORMAL'
                pivot = 'MEDIAN' # ?
            elif self.system == 'CONTEXT':
                sys_name = None # use current orientation
                pivot = None
    
                if active_obj and (active_obj.mode != 'OBJECT'):
                    if len(particles) == 0:
                        pivot = active_obj.matrix_world.to_translation()
    
            return csu.get_matrix(sys_name, self.offset, pivot)
    
        def convert_to_abs(self, v3d, pos, warn=False, **kwargs):
            kwargs.pop("use_history", None)
            matrix = self.get_matrix(False, v3d, warn, **kwargs)
    
            if not matrix:
                return None
            return matrix * pos
    
        def convert_from_abs(self, v3d, pos, warn=False, **kwargs):
            use_history = kwargs.pop("use_history", True)
            matrix = self.get_matrix(use_history, v3d, warn, **kwargs)
    
            if not matrix:
                return None
    
            try:
                return matrix.inverted() * pos
            except:
                # this is some degenerate object
                return Vector()
    
        def draw_bookmark(self, context):
            r = context.region
            rv3d = context.region_data
    
            bookmark = self.bookmarks.get_item()
            if not bookmark:
                return
    
            pos = self.convert_to_abs(context.space_data, bookmark.pos)
    
            if pos is None:
                return
    
            projected = location_3d_to_region_2d(r, rv3d, pos)
    
            if projected:
                # Store previous OpenGL settings
                smooth_prev = gl_get(bgl.GL_SMOOTH)
    
                dpi = context.preferences.system.dpi
    
                widget_unit = (pixelsize * dpi * 20.0 + 36.0) / 72.0
    
                bgl.glShadeModel(bgl.GL_SMOOTH)
                bgl.glLineWidth(2)
                bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
                bgl.glBegin(bgl.GL_LINE_STRIP)
    
                radius = widget_unit * 0.3 #6
    
                n = 8
                da = 2 * math.pi / n
                x, y = projected
                x, y = int(x), int(y)
                for i in range(n + 1):
                    a = i * da
                    dx = math.sin(a) * radius
                    dy = math.cos(a) * radius
                    if (i % 2) == 0:
                        bgl.glColor4f(0.0, 1.0, 0.0, 1.0)
                    else:
                        bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
                    bgl.glVertex2i(x + int(dx), y + int(dy))
                bgl.glEnd()
    
                # Restore previous OpenGL settings
                gl_enable(bgl.GL_SMOOTH, smooth_prev)
    
    class BookmarkLibraryIDBlock(PseudoIDBlockBase):
        # Somewhy instance members aren't seen in update()
        # callbacks... but class members are.
        _self_update = [0]
        _dont_rename = [False]
    
        data_name = "Bookmark Library"