Skip to content
Snippets Groups Projects
space_view3d_enhanced_3d_cursor.py 185 KiB
Newer Older
  • Learn to ignore specific revisions
  •             if settings.draw_T1:
                    bgl.glColor4f(1, 0, 1, 1)
                    draw_arrow(p0, y, _z, x) # X (1st tangential)
                if settings.draw_T2:
                    bgl.glColor4f(1, 1, 0, 1)
                    draw_arrow(p0, _z, x, y) # Y (2nd tangential)
    
                bgl.glEnable(bgl.GL_BLEND)
                bgl.glDisable(bgl.GL_DEPTH_TEST)
    
                if settings.draw_N:
                    bgl.glColor4f(0, 1, 1, 0.25)
                    draw_arrow(p0, _x, y, z) # Z (normal)
                if settings.draw_T1:
                    bgl.glColor4f(1, 0, 1, 0.25)
                    draw_arrow(p0, y, _z, x) # X (1st tangential)
                if settings.draw_T2:
                    bgl.glColor4f(1, 1, 0, 0.25)
                    draw_arrow(p0, _z, x, y) # Y (2nd tangential)
    
            if settings.draw_guides:
                p0 = dest_point
    
                try:
                    p00 = sys_matrix.inverted() * p0
                except:
                    # this is some degenerate system
                    p00 = p0.copy()
    
                axes_line_params = [
                    (Vector((0, p00.y, p00.z)), (1, 0, 0)),
                    (Vector((p00.x, 0, p00.z)), (0, 1, 0)),
                    (Vector((p00.x, p00.y, 0)), (0, 0, 1)),
                ]
    
                for i in range(3):
                    p1, color = axes_line_params[i]
                    p1 = sys_matrix * p1
                    constrained = (self.axes_coords[i] is not None) or \
                        (not self.allowed_axes[i])
                    alpha = (0.25 if constrained else 1.0)
                    draw_line_hidden_depth(p0, p1, color, \
                        alpha, alpha, False, True)
    
                # line from origin to cursor
                p0 = sys_origin
                p1 = dest_point
    
                bgl.glEnable(bgl.GL_LINE_STIPPLE)
                bgl.glColor4f(1, 1, 0, 1)
    
                draw_line_hidden_depth(p0, p1, (1, 1, 0), 1.0, 0.5, True, True)
    
            if settings.draw_snap_elements:
                sui = self.su.implementation
                if sui.potential_snap_elements and (sui.snap_type == 'EDGE'):
                    bgl.glDisable(bgl.GL_LINE_STIPPLE)
    
                    bgl.glEnable(bgl.GL_BLEND)
                    bgl.glDisable(bgl.GL_DEPTH_TEST)
    
                    bgl.glLineWidth(2)
                    bgl.glColor4f(0, 0, 1, 0.5)
    
                    bgl.glBegin(bgl.GL_LINE_LOOP)
                    for p in sui.potential_snap_elements:
                        bgl.glVertex3f(p[0], p[1], p[2])
                    bgl.glEnd()
                elif sui.potential_snap_elements and (sui.snap_type == 'FACE'):
                    bgl.glEnable(bgl.GL_BLEND)
                    bgl.glDisable(bgl.GL_DEPTH_TEST)
    
                    bgl.glColor4f(0, 1, 0, 0.5)
    
                    co = sui.potential_snap_elements
    
                    bgl.glBegin(bgl.GL_TRIANGLES)
                    for tri in tris:
                        for vi in tri:
                            p = co[vi]
                            bgl.glVertex3f(p[0], p[1], p[2])
                    bgl.glEnd()
    
        def draw_2d(self, context):
    
            if self.check_v3d_local(context):
                return
    
            r = context.region
            rv3d = context.region_data
    
            settings = find_settings()
    
            if settings.draw_snap_elements:
                sui = self.su.implementation
    
                snap_points = []
                if sui.potential_snap_elements and \
                        (sui.snap_type in {'VERTEX', 'VOLUME'}):
                    snap_points.extend(sui.potential_snap_elements)
                if sui.extra_snap_points:
                    snap_points.extend(sui.extra_snap_points)
    
                if snap_points:
                    bgl.glEnable(bgl.GL_BLEND)
    
                    bgl.glPointSize(5)
                    bgl.glColor4f(1, 0, 0, 0.5)
    
                    bgl.glBegin(bgl.GL_POINTS)
                    for p in snap_points:
                        p = location_3d_to_region_2d(r, rv3d, p)
                        if p is not None:
                            bgl.glVertex2f(p[0], p[1])
                    bgl.glEnd()
    
                    bgl.glPointSize(1)
    
            if self.transform_mode == 'MOVE':
                return
    
            bgl.glEnable(bgl.GL_LINE_STIPPLE)
    
            bgl.glLineWidth(1)
    
            bgl.glColor4f(0, 0, 0, 1)
            draw_line_2d(self.origin_xy, self.xy)
    
            bgl.glDisable(bgl.GL_LINE_STIPPLE)
    
            line_width = 3
            bgl.glLineWidth(line_width)
    
            L = 12.0
            arrow_len = 6.0
            arrow_width = 8.0
            arrow_space = 5.0
    
            Lmax = arrow_space * 2 + L * 2 + line_width
    
            pos = self.xy.to_2d()
            normal = self.prev_delta_xy.to_2d().normalized()
            dist = self.prev_delta_xy.length
            tangential = Vector((-normal[1], normal[0]))
    
            if self.transform_mode == 'ROTATE':
                n_axes = sum(int(v) for v in self.allowed_axes)
                if n_axes == 2:
                    bgl.glColor4f(0.4, 0.15, 0.15, 1)
                    for sgn in (-1, 1):
                        n = sgn * Vector((0, 1))
                        p0 = pos + arrow_space * n
                        draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
    
                    bgl.glColor4f(0.11, 0.51, 0.11, 1)
                    for sgn in (-1, 1):
                        n = sgn * Vector((1, 0))
                        p0 = pos + arrow_space * n
                        draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
                else:
                    bgl.glColor4f(0, 0, 0, 1)
                    for sgn in (-1, 1):
                        n = sgn * tangential
                        if dist < Lmax:
                            n *= dist / Lmax
                        p0 = pos + arrow_space * n
                        draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
            elif self.transform_mode == 'SCALE':
                bgl.glColor4f(0, 0, 0, 1)
                for sgn in (-1, 1):
                    n = sgn * normal
                    p0 = pos + arrow_space * n
                    draw_arrow_2d(p0, n, L, arrow_len, arrow_width)
    
            bgl.glLineWidth(1)
    
        def draw_axes_coords(self, context, header_size):
    
            if self.check_v3d_local(context):
                return
    
            if time.time() < (self.click_start + self.click_period):
                return
    
            v3d = context.space_data
    
            userprefs_view = context.preferences.view
    
            tool_settings = context.tool_settings
    
            settings = find_settings()
            tfm_opts = settings.transform_options
    
            localmat = CursorDynamicSettings.local_matrix
    
            font_id = 0 # default font
    
            font_size = 11
            blf.size(font_id, font_size, 72) # font, point size, dpi
    
            tet = context.preferences.themes[0].text_editor
    
            # Prepare the table...
            if self.transform_mode == 'MOVE':
                axis_prefix = ("D" if tfm_opts.use_relative_coords else "")
            elif self.transform_mode == 'SCALE':
                axis_prefix = "S"
            else:
                axis_prefix = "R"
            axis_names = ["X", "Y", "Z"]
    
            axis_cells = []
            coord_cells = []
            #caret_cell = TextCell("_", tet.cursor)
            caret_cell = TextCell("|", tet.cursor)
    
            try:
                axes_text = self.get_axes_text()
    
                for i in range(3):
    
                    alpha = (1.0 if self.allowed_axes[i] else 0.5)
                    text = axis_prefix + axis_names[i] + " : "
                    axis_cells.append(TextCell(text, color, alpha))
    
                    if self.axes_values[i]:
                        if self.axes_eval_success[i]:
                            color = tet.syntax_numbers
                        else:
                            color = tet.syntax_string
                    else:
    
                    text = axes_text[i]
                    coord_cells.append(TextCell(text, color))
            except Exception as e:
    
            mode_cells = []
    
            try:
                snap_type = self.su.implementation.snap_type
                if snap_type is None:
    
                elif (not self.use_object_centers) or \
                        (snap_type == 'INCREMENT'):
                    color = tet.syntax_numbers
                else:
                    color = tet.syntax_special
    
                text = snap_type or tool_settings.snap_element
    
                if text == 'VOLUME':
                    text = "BBOX"
                mode_cells.append(TextCell(text, color))
    
                if self.csu.tou.is_custom:
    
                else:
                    color = tet.syntax_builtin
                text = self.csu.tou.get_title()
                mode_cells.append(TextCell(text, color))
    
                text = self.csu.get_pivot_name(raw=True)
                if self.use_object_centers:
                    color = tet.syntax_special
                mode_cells.append(TextCell(text, color))
            except Exception as e:
    
            hdr_w, hdr_h = header_size
    
            try:
                xyz_x_start_min = 12
                xyz_x_start = xyz_x_start_min
                mode_x_start = 6
    
                mode_margin = 4
                xyz_margin = 16
                blend_margin = 32
    
                bgl.glColor4f(color[0], color[1], color[2], 1.0)
                draw_rect(0, 0, hdr_w, hdr_h)
    
                if tool_settings.use_snap_self:
                    x = hdr_w - mode_x_start
                    y = hdr_h / 2
                    cell = mode_cells[0]
                    x -= cell.w
                    y -= cell.h * 0.5
                    bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
                    draw_rect(x, y, cell.w, cell.h, 1, True)
    
                x = hdr_w - mode_x_start
                y = hdr_h / 2
                for cell in mode_cells:
                    cell.draw(x, y, (1, 0.5))
                    x -= (cell.w + mode_margin)
    
                curr_axis_x_start = 0
                curr_axis_x_end = 0
                caret_x = 0
    
                xyz_width = 0
                for i in range(3):
                    if i == self.current_axis:
    
    Campbell Barton's avatar
    Campbell Barton committed
                        curr_axis_x_start = xyz_width
    
                    xyz_width += axis_cells[i].w
    
                    if i == self.current_axis:
                        char_offset = 0
                        if self.axes_values[i]:
                            char_offset = blf.dimensions(font_id,
                                coord_cells[i].text[:self.caret_pos])[0]
                        caret_x = xyz_width + char_offset
    
                    xyz_width += coord_cells[i].w
    
                    if i == self.current_axis:
    
    Campbell Barton's avatar
    Campbell Barton committed
                        curr_axis_x_end = xyz_width
    
                    xyz_width += xyz_margin
    
                xyz_width = int(xyz_width)
                xyz_width_ext = xyz_width + blend_margin
    
                offset = (xyz_x_start + curr_axis_x_end) - hdr_w
                if offset > 0:
                    xyz_x_start -= offset
    
                offset = xyz_x_start_min - (xyz_x_start + curr_axis_x_start)
                if offset > 0:
                    xyz_x_start += offset
    
                offset = (xyz_x_start + caret_x) - hdr_w
                if offset > 0:
                    xyz_x_start -= offset
    
                # somewhy GL_BLEND should be set right here
                # to actually draw the box with blending %)
                # (perhaps due to text draw happened before)
                bgl.glEnable(bgl.GL_BLEND)
                bgl.glShadeModel(bgl.GL_SMOOTH)
                gl_enable(bgl.GL_SMOOTH, True)
    
                bgl.glBegin(bgl.GL_TRIANGLE_STRIP)
                bgl.glColor4f(color[0], color[1], color[2], 1.0)
                bgl.glVertex2i(0, 0)
                bgl.glVertex2i(0, hdr_h)
                bgl.glVertex2i(xyz_width, 0)
                bgl.glVertex2i(xyz_width, hdr_h)
                bgl.glColor4f(color[0], color[1], color[2], 0.0)
                bgl.glVertex2i(xyz_width_ext, 0)
                bgl.glVertex2i(xyz_width_ext, hdr_h)
                bgl.glEnd()
    
                x = xyz_x_start
                y = hdr_h / 2
                for i in range(3):
                    cell = axis_cells[i]
                    cell.draw(x, y, (0, 0.5))
                    x += cell.w
    
                    cell = coord_cells[i]
                    cell.draw(x, y, (0, 0.5))
                    x += (cell.w + xyz_margin)
    
                caret_x -= blf.dimensions(font_id, caret_cell.text)[0] * 0.5
                caret_cell.draw(xyz_x_start + caret_x, y, (0, 0.5))
    
                bgl.glEnable(bgl.GL_BLEND)
                bgl.glShadeModel(bgl.GL_SMOOTH)
                gl_enable(bgl.GL_SMOOTH, True)
    
                bgl.glBegin(bgl.GL_TRIANGLE_STRIP)
                bgl.glColor4f(color[0], color[1], color[2], 1.0)
                bgl.glVertex2i(0, 0)
                bgl.glVertex2i(0, hdr_h)
                bgl.glVertex2i(xyz_x_start_min, 0)
                bgl.glColor4f(color[0], color[1], color[2], 0.0)
                bgl.glVertex2i(xyz_x_start_min, hdr_h)
                bgl.glEnd()
    
            except Exception as e:
    
            return
    
        # ====== NORMAL SNAPSHOT ====== #
        def is_normal_visible(self):
            if self.csu.tou.get() == "Surface":
                return True
    
            if self.use_object_centers:
                return False
    
            return self.su.implementation.snap_type \
                not in {None, 'INCREMENT', 'VOLUME'}
    
        def get_normal_params(self, tfm_opts, dest_point):
            surf_matrix = self.csu.get_matrix("Surface")
            if tfm_opts.use_relative_coords:
                surf_origin = dest_point
            else:
                surf_origin = surf_matrix.to_translation()
    
            m3 = surf_matrix.to_3x3()
            p0 = surf_origin
            scl = self.gizmo_scale(p0)
    
            # Normal and tangential are not always orthogonal
            # (e.g. when normal is interpolated)
            x = (m3 * Vector((1, 0, 0))).normalized()
            y = (m3 * Vector((0, 1, 0))).normalized()
            z = (m3 * Vector((0, 0, 1))).normalized()
    
            _x = z.cross(y)
            _z = y.cross(x)
    
            return p0, x * scl, y * scl, z * scl, _x * scl, _z * scl
    
        def make_normal_snapshot(self, collection, tangential=False):
    
            settings = find_settings()
            tfm_opts = settings.transform_options
    
            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)
    
                snapshot = bpy.data.objects.new("normal_snapshot", None)
    
                if tangential:
    
                    m = MatrixCompose(_z, y, x, p0)
    
                else:
    
                    m = MatrixCompose(_x, y, z, p0)
    
                snapshot.empty_display_type = 'SINGLE_ARROW'
                #snapshot.empty_display_type = 'ARROWS'
    
                #snapshot.layers = [True] * 20 # ?
    
                collection.objects.link(snapshot)
    
    #============================================================================#
    
    
    class Particle:
        pass
    
    class View3D_Cursor(Particle):
        def __init__(self, context):
            assert context.space_data.type == 'VIEW_3D'
            self.v3d = context.space_data
            self.initial_pos = self.get_location()
            self.initial_matrix = Matrix.Translation(self.initial_pos)
    
        def revert(self):
            self.set_location(self.initial_pos)
    
        def get_location(self):
    
            return get_cursor_location(v3d=self.v3d)
    
        def set_location(self, value):
    
            set_cursor_location(Vector(value), v3d=self.v3d)
    
        def get_rotation(self):
            return Quaternion()
    
        def set_rotation(self, value):
            pass
    
        def get_scale(self):
            return Vector((1.0, 1.0, 1.0))
    
        def set_scale(self, value):
            pass
    
        def get_matrix(self):
            return Matrix.Translation(self.get_location())
    
        def set_matrix(self, value):
            self.set_location(value.to_translation())
    
        def get_initial_matrix(self):
            return self.initial_matrix
    
    class View3D_Object(Particle):
        def __init__(self, obj):
            self.obj = obj
    
        def get_location(self):
            # obj.location seems to be in parent's system...
            # or even maybe not bounded by constraints %)
            return self.obj.matrix_world.to_translation()
    
    class View3D_EditMesh_Vertex(Particle):
        pass
    
    class View3D_EditMesh_Edge(Particle):
        pass
    
    class View3D_EditMesh_Face(Particle):
        pass
    
    class View3D_EditSpline_Point(Particle):
        pass
    
    class View3D_EditSpline_BezierPoint(Particle):
        pass
    
    class View3D_EditSpline_BezierHandle(Particle):
        pass
    
    class View3D_EditMeta_Element(Particle):
        pass
    
    class View3D_EditBone_Bone(Particle):
        pass
    
    class View3D_EditBone_HeadTail(Particle):
        pass
    
    class View3D_PoseBone(Particle):
        pass
    
    class UV_Cursor(Particle):
        pass
    
    class UV_Vertex(Particle):
        pass
    
    class UV_Edge(Particle):
        pass
    
    class UV_Face(Particle):
        pass
    
    # Other types:
    # NLA / Dopesheet / Graph editor ...
    
    # Particles are used in the following situations:
    # - as subjects of transformation
    # - as reference point(s) for cursor transformation
    # Note: particles 'dragged' by Proportional Editing
    # are a separate issue (they can come and go).
    def gather_particles(**kwargs):
        context = kwargs.get("context", bpy.context)
    
        area_type = kwargs.get("area_type", context.area.type)
    
        scene = kwargs.get("scene", context.scene)
    
    	
        view_layer = kwargs.get("view_layer", context.view_layer)
    
        space_data = kwargs.get("space_data", context.space_data)
        region_data = kwargs.get("region_data", context.region_data)
    
        particles = []
        pivots = {}
        normal_system = None
    
        active_element = None
        cursor_pos = None
        median = None
    
        if area_type == 'VIEW_3D':
            context_mode = kwargs.get("context_mode", context.mode)
    
            selected_objects = kwargs.get("selected_objects",
                context.selected_objects)
    
            active_object = kwargs.get("active_object",
                context.active_object)
    
            if context_mode == 'OBJECT':
                for obj in selected_objects:
                    particle = View3D_Object(obj)
                    particles.append(particle)
    
                if active_object:
                    active_element = active_object.\
                        matrix_world.to_translation()
    
            # On Undo/Redo scene hash value is changed ->
            # -> the monitor tries to update the CSU ->
            # -> object.mode_set seem to somehow conflict
            # with Undo/Redo mechanisms.
    
            elif active_object and active_object.data and \
    
            (context_mode in {
            'EDIT_MESH', 'EDIT_METABALL',
            'EDIT_CURVE', 'EDIT_SURFACE',
            'EDIT_ARMATURE', 'POSE'}):
    
                m = active_object.matrix_world
    
                positions = []
                normal = Vector((0, 0, 0))
    
                if context_mode == 'EDIT_MESH':
    
                    bm = bmesh.from_edit_mesh(active_object.data)
    
                    if bm.select_history:
                        elem = bm.select_history[-1]
                        if isinstance(elem, bmesh.types.BMVert):
                            active_element = elem.co.copy()
                        else:
                            active_element = Vector()
                            for v in elem.verts:
                                active_element += v.co
                            active_element *= 1.0 / len(elem.verts)
    
                    for v in bm.verts:
                        if v.select:
                            positions.append(v.co)
                            normal += v.normal
    
                    # mimic Blender's behavior (as of now,
                    # order of selection is ignored)
                    if len(positions) == 2:
                        normal = positions[1] - positions[0]
                    elif len(positions) == 3:
                        a = positions[0] - positions[1]
                        b = positions[2] - positions[1]
                        normal = a.cross(b)
    
                elif context_mode == 'EDIT_METABALL':
                    active_elem = active_object.data.elements.active
                    if active_elem:
                        active_element = active_elem.co.copy()
                        active_element = active_object.\
                            matrix_world * active_element
    
                    # Currently there is no API for element.select
                    #for element in active_object.data.elements:
                    #    if element.select:
                    #        positions.append(element.co)
                elif context_mode == 'EDIT_ARMATURE':
                    # active bone seems to have the same pivot
                    # as median of the selection
                    '''
                    active_bone = active_object.data.edit_bones.active
                    if active_bone:
                        active_element = active_bone.head + \
                                         active_bone.tail
                        active_element = active_object.\
                            matrix_world * active_element
                    '''
    
                    for bone in active_object.data.edit_bones:
                        if bone.select_head:
                            positions.append(bone.head)
                        if bone.select_tail:
                            positions.append(bone.tail)
                elif context_mode == 'POSE':
                    active_bone = active_object.data.bones.active
                    if active_bone:
    
                        active_element = active_bone.\
                            matrix_local.translation.to_3d()
    
                        active_element = active_object.\
                            matrix_world * active_element
    
                    # consider only topmost parents
                    bones = set()
                    for bone in active_object.data.bones:
                        if bone.select:
                            bones.add(bone)
    
                    parents = set()
                    for bone in bones:
                        if not set(bone.parent_recursive).intersection(bones):
                            parents.add(bone)
    
                    for bone in parents:
    
                        positions.append(bone.matrix_local.translation.to_3d())
    
                else:
                    for spline in active_object.data.splines:
                        for point in spline.bezier_points:
                            if point.select_control_point:
                                positions.append(point.co)
                            else:
                                if point.select_left_handle:
                                    positions.append(point.handle_left)
                                if point.select_right_handle:
                                    positions.append(point.handle_right)
    
                            n = None
                            nL = point.co - point.handle_left
                            nR = point.co - point.handle_right
                            #nL = point.handle_left.copy()
                            #nR = point.handle_right.copy()
                            if point.select_control_point:
                                n = nL + nR
                            elif point.select_left_handle or \
                                 point.select_right_handle:
                                n = nL + nR
                            else:
                                if point.select_left_handle:
                                    n = -nL
                                if point.select_right_handle:
                                    n = nR
    
                            if n is not None:
                                if n.length_squared < epsilon:
                                    n = -nL
                                normal += n.normalized()
    
                        for point in spline.points:
                            if point.select:
                                positions.append(point.co)
    
                if len(positions) != 0:
                    if normal.length_squared < epsilon:
                        normal = Vector((0, 0, 1))
                    normal.rotate(m)
                    normal.normalize()
    
                    if (1.0 - abs(normal.z)) < epsilon:
                        t1 = Vector((1, 0, 0))
                    else:
                        t1 = Vector((0, 0, 1)).cross(normal)
                    t2 = t1.cross(normal)
    
                    normal_system = MatrixCompose(t1, t2, normal)
    
                    median, bbox_center = calc_median_bbox_pivots(positions)
                    median = m * median
                    bbox_center = m * bbox_center
    
                    # Currently I don't know how to get active mesh element
                    if active_element is None:
                        if context_mode == 'EDIT_ARMATURE':
                            # Somewhy EDIT_ARMATURE has such behavior
                            active_element = bbox_center
                        else:
                            active_element = median
                else:
                    if active_element is None:
                        active_element = active_object.\
                            matrix_world.to_translation()
    
                    median = active_element
                    bbox_center = active_element
    
                    normal_system = active_object.matrix_world.to_3x3()
    
                    normal_system.col[0].normalize()
                    normal_system.col[1].normalize()
                    normal_system.col[2].normalize()
    
            else:
                # paint/sculpt, etc.?
                particle = View3D_Object(active_object)
                particles.append(particle)
    
                if active_object:
                    active_element = active_object.\
                        matrix_world.to_translation()
    
            cursor_pos = get_cursor_location(v3d=space_data)
    
        #elif area_type == 'IMAGE_EDITOR':
            # currently there is no way to get UV editor's
            # offset (and maybe some other parameters
            # required to implement these operators)
            #cursor_pos = space_data.uv_editor.cursor_location
    
        #elif area_type == 'EMPTY':
        #elif area_type == 'GRAPH_EDITOR':
        #elif area_type == 'OUTLINER':
        #elif area_type == 'PROPERTIES':
        #elif area_type == 'FILE_BROWSER':
        #elif area_type == 'INFO':
        #elif area_type == 'SEQUENCE_EDITOR':
        #elif area_type == 'TEXT_EDITOR':
        #elif area_type == 'AUDIO_WINDOW':
        #elif area_type == 'DOPESHEET_EDITOR':
        #elif area_type == 'NLA_EDITOR':
        #elif area_type == 'SCRIPTS_WINDOW':
        #elif area_type == 'TIMELINE':
        #elif area_type == 'NODE_EDITOR':
        #elif area_type == 'LOGIC_EDITOR':
        #elif area_type == 'CONSOLE':
        #elif area_type == 'USER_PREFERENCES':
    
        else:
            print("gather_particles() not implemented for '{}'".\
                  format(area_type))
            return None, None
    
        # 'INDIVIDUAL_ORIGINS' is not handled here
    
        if cursor_pos:
            pivots['CURSOR'] = cursor_pos.copy()
    
        if active_element:
            # in v3d: ACTIVE_ELEMENT
            pivots['ACTIVE'] = active_element.copy()
    
        if (len(particles) != 0) and (median is None):
            positions = (p.get_location() for p in particles)
            median, bbox_center = calc_median_bbox_pivots(positions)
    
        if median:
            # in v3d: MEDIAN_POINT, in UV editor: MEDIAN
            pivots['MEDIAN'] = median.copy()
            # in v3d: BOUNDING_BOX_CENTER, in UV editor: CENTER
            pivots['CENTER'] = bbox_center.copy()
    
        csu = CoordinateSystemUtility(scene, space_data, region_data, \
    
            pivots, normal_system, view_layer)
    
        return particles, csu
    
    def calc_median_bbox_pivots(positions):
        median = None # pos can be 3D or 2D
        bbox = [None, None]
    
        n = 0
        for pos in positions:
            extend_bbox(bbox, pos)
            try:
                median += pos
            except:
                median = pos.copy()
            n += 1
    
        median = median / n
        bbox_center = (Vector(bbox[0]) + Vector(bbox[1])) * 0.5
    
        return median, bbox_center
    
    def extend_bbox(bbox, pos):
        try:
            bbox[0] = tuple(min(e0, e1) for e0, e1 in zip(bbox[0], pos))
            bbox[1] = tuple(max(e0, e1) for e0, e1 in zip(bbox[1], pos))
        except:
            bbox[0] = tuple(pos)
            bbox[1] = tuple(pos)
    
    
    # ====== COORDINATE SYSTEM UTILITY ====== #
    class CoordinateSystemUtility:
        pivot_name_map = {
            'CENTER':'CENTER',
            'BOUNDING_BOX_CENTER':'CENTER',
            'MEDIAN':'MEDIAN',
            'MEDIAN_POINT':'MEDIAN',
    
            'INDIVIDUAL_ORIGINS':'INDIVIDUAL',
            'ACTIVE_ELEMENT':'ACTIVE',
            'WORLD':'WORLD',
            'SURFACE':'SURFACE', # ?
            'BOOKMARK':'BOOKMARK',
        }
        pivot_v3d_map = {
            'CENTER':'BOUNDING_BOX_CENTER',
            'MEDIAN':'MEDIAN_POINT',
    
            'INDIVIDUAL':'INDIVIDUAL_ORIGINS',
            'ACTIVE':'ACTIVE_ELEMENT',
        }
    
        def __init__(self, scene, space_data, region_data, \
    
                     pivots, normal_system, view_layer):
    
            self.space_data = space_data
            self.region_data = region_data
    
            if space_data.type == 'VIEW_3D':
                self.pivot_map_inv = self.pivot_v3d_map
    
            self.tou = TransformOrientationUtility(
    
                scene, space_data, region_data, view_layer)
    
            self.tou.normal_system = normal_system
    
            self.pivots = pivots
    
            # Assigned by caller (for cursor or selection)
            self.source_pos = None
            self.source_rot = None
            self.source_scale = None
    
        def set_orientation(self, name):
            self.tou.set(name)
    
        def set_pivot(self, pivot):
            self.space_data.pivot_point = self.pivot_map_inv[pivot]
    
        def get_pivot_name(self, name=None, relative=None, raw=False):
            pivot = self.pivot_name_map[self.space_data.pivot_point]
            if raw:
                return pivot
    
            if not name:
                name = self.tou.get()
    
            if relative is None:
                settings = find_settings()
                tfm_opts = settings.transform_options
                relative = tfm_opts.use_relative_coords
    
            if relative:
                pivot = "RELATIVE"
            elif (name == 'GLOBAL') or (pivot == 'WORLD'):
                pivot = 'WORLD'
            elif (name == "Surface") or (pivot == 'SURFACE'):
                pivot = "SURFACE"
    
            return pivot
    
        def get_origin(self, name=None, relative=None, pivot=None):
            if not pivot:
                pivot = self.get_pivot_name(name, relative)
    
            if relative or (pivot == "RELATIVE"):
                # "relative" parameter overrides "pivot"
                return self.source_pos
            elif pivot == 'WORLD':
                return Vector()
            elif pivot == "SURFACE":
                runtime_settings = find_runtime_settings()
                return Vector(runtime_settings.surface_pos)
            else:
                if pivot == 'INDIVIDUAL':
                    pivot = 'MEDIAN'
    
                #if pivot == 'ACTIVE':
                #    print(self.pivots)
    
                try:
                    return self.pivots[pivot]
                except:
                    return Vector()
    
        def get_matrix(self, name=None, relative=None, pivot=None):
            if not name:
                name = self.tou.get()
    
            matrix = self.tou.get_matrix(name)
    
            if isinstance(pivot, Vector):
                pos = pivot
            else:
                pos = self.get_origin(name, relative, pivot)
    
            return to_matrix4x4(matrix, pos)
    
    # ====== TRANSFORM ORIENTATION UTILITIES ====== #
    class TransformOrientationUtility:
        special_systems = {"Surface", "Scaled"}
        predefined_systems = {
            'GLOBAL', 'LOCAL', 'VIEW', 'NORMAL', 'GIMBAL',
            "Scaled", "Surface",
        }
    
        def __init__(self, scene, v3d, rv3d, vwly):
    
            self.scene = scene
            self.v3d = v3d
            self.rv3d = rv3d
    
            self.view_layer = vwly
    
            self.custom_systems = [item for item in scene.orientations \
                if item.name not in self.special_systems]
    
            self.is_custom = False
            self.custom_id = -1
    
            # This is calculated elsewhere
            self.normal_system = None
    
            self.set(v3d.transform_orientation)
    
        def get(self):
            return self.transform_orientation
    
        def get_title(self):
            if self.is_custom:
                return self.transform_orientation
    
            name = self.transform_orientation
            return name[:1].upper() + name[1:].lower()
    
            if isinstance(name, int):
                n = len(self.custom_systems)
                if n == 0:
                    # No custom systems, do nothing
                    return
    
                increment = name
    
                if self.is_custom:
                    # If already custom, switch to next custom system
                    self.custom_id = (self.custom_id + increment) % n
    
                self.is_custom = True
    
                name = self.custom_systems[self.custom_id].name
            else:
                self.is_custom = name not in self.predefined_systems
    
                if self.is_custom:
                    self.custom_id = next((i for i, v in \
                        enumerate(self.custom_systems) if v.name == name), -1)
    
                if name in self.special_systems:
                    # Ensure such system exists
                    self.get_custom(name)