полный код аддона для блендер 3.3.0:

bl_info = {
    "name": "AI Progress Track Maker Importer",
    "author": "AlonsoBeast",
    "version": (1, 0, 4),
    "blender": (3, 3, 0),
    "location": "View3D > Sidebar > F1 Track Builder Toolkit",
    "description": "Tool for managing and transforming track progress curves",
    "category": "Import-Export",
}

import bpy
import xml.etree.ElementTree as ET
from bpy.props import StringProperty, IntProperty, BoolProperty
from bpy.types import Operator, Panel
from bpy_extras.io_utils import ImportHelper, ExportHelper
import tempfile
import shutil
from mathutils import Vector

def create_spline(name, points):
    curve_data = bpy.data.curves.new(name=name, type='CURVE')
    curve_data.dimensions = '3D'

    spline = curve_data.splines.new('POLY')
    spline.points.add(len(points) - 1)

    for i, (x, y, z) in enumerate(points):
        spline.points[i].co = (x, y, z, 1)  # Сохраняем Y и Z в исходном порядке save save

    curve_object = bpy.data.objects.new(name, curve_data)
    bpy.context.collection.objects.link(curve_object)

    return curve_object

def mirror_y_global(obj):
    bpy.context.view_layer.objects.active = obj
    obj.select_set(True)
    bpy.ops.object.mode_set(mode='OBJECT')
    bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
    bpy.ops.transform.mirror(orient_type='GLOBAL', constraint_axis=(False, True, False))
    obj.select_set(False)

def create_mesh_from_splines(left_spline, right_spline):
    left_points = left_spline.data.splines[0].points
    right_points = right_spline.data.splines[0].points

    vertices = []
    faces = []

    for i in range(len(left_points)):
        left_coord = left_points[i].co
        right_coord = right_points[i].co

        # Добавляем вершины в порядке X, Z, Y для меша
        vertices.append((left_coord.x, left_coord.z, left_coord.y))  # Точка слева
        vertices.append((right_coord.x, right_coord.z, right_coord.y))  # Точка справа
            
        # Если это не последняя точка, создаем грани
        if i < len(left_points) - 1:
            idx1 = i * 2
            idx2 = idx1 + 1
            idx3 = idx1 + 2
            idx4 = idx1 + 3

            faces.append((idx1, idx2, idx4))
            faces.append((idx1, idx4, idx3))

    # Создаем меш
    mesh_data = bpy.data.meshes.new("CombinedMesh")
    mesh_data.from_pydata(vertices, [], faces)
    mesh_data.update()

    # Создание нового объекта
    mesh_object = bpy.data.objects.new("CombinedMeshObject", mesh_data)
    bpy.context.collection.objects.link(mesh_object)

    # Применение глобального зеркалирования к мешу
    mirror_y_global(mesh_object)

    # Удаляем сплайны
    bpy.data.objects.remove(left_spline, do_unlink=True)
    bpy.data.objects.remove(right_spline, do_unlink=True)

class IMPORT_OT_ai_progress_track_maker(Operator, ImportHelper):
    bl_idname = "import_mesh.ai_progress_track_maker"
    bl_label = "Import AI Track XML"
    filename_ext = ".xml"
    filter_glob: StringProperty(default="*.xml", options={'HIDDEN'})

    def execute(self, context):
        try:
            temp_filepath = tempfile.mktemp(suffix=".xml")
            shutil.copy(self.filepath, temp_filepath)
            context.scene.temp_ai_progress_file = temp_filepath

            tree = ET.parse(self.filepath)
            root = tree.getroot()

            # Обработка gates
            gates_element = root.find('gates')
            if gates_element is not None:
                left_points = []
                right_points = []
                for gate in gates_element.findall('gate'):
                    left = gate.find('left')
                    right = gate.find('right')

                    if left is None or right is None:
                        continue

                    # Извлекаем координаты в порядке X, Y, Z, но сохраняем Y и Z
                    left_coords = [float(x) for x in left.text.split()]
                    right_coords = [float(x) for x in right.text.split()]

                    left_points.append((left_coords[0], left_coords[1], left_coords[2]))  # X, Y, Z
                    right_points.append((right_coords[0], right_coords[1], right_coords[2]))  # X, Y, Z

                left_spline = create_spline("LEFT_SPLINE", left_points)
                right_spline = create_spline("RIGHT_SPLINE", right_points)
                mirror_y_global(left_spline)
                mirror_y_global(right_spline)

                # Создаем меш из сплайнов и удаляем их
                create_mesh_from_splines(left_spline, right_spline)

            else:
                self.report({'WARNING'}, "<gates> element not found in the file.")

            # Обработка points для создания 2DMAP_SPLINE
            points_element = root.find('points')
            if points_element is not None:
                for line in points_element.findall('line'):
                    if line.get("type") == "progress":
                        point_coords = []
                        for point in line.findall('point'):
                            position = point.find('position')
                            if position is not None:
                                coords = [float(x) for x in position.text.split()]
                                point_coords.append((coords[0], coords[2], coords[1]))  # X, Z, Y
                        twodmap_spline = create_spline("2DMAP_SPLINE", point_coords)
                        mirror_y_global(twodmap_spline)  # Зеркальное отражение для 2DMAP_SPLINE
            else:
                self.report({'WARNING'}, "<points> element not found in the file.")
        except ET.ParseError:
            self.report({'ERROR'}, "Error reading XML file.")
            return {'CANCELLED'}

        return {'FINISHED'}

class EXPORT_OT_ai_progress_track_maker(Operator, ExportHelper):
    bl_idname = "export_mesh.ai_progress_track_maker"
    bl_label = "Export AI Track XML"
    filename_ext = ".xml"
    filter_glob: StringProperty(default="*.xml", options={'HIDDEN'})

    def execute(self, context):
        temp_filepath = context.scene.get("temp_ai_progress_file", "")
        if not temp_filepath:
            self.report({'ERROR'}, "Temporary file not found. Please re-import the file.")
            return {'CANCELLED'}

        self.update_xml_with_current_coordinates(temp_filepath)
        shutil.copy(temp_filepath, self.filepath)
        return {'FINISHED'}

    def update_xml_with_current_coordinates(self, temp_filepath):
        try:
            tree = ET.parse(temp_filepath)
            root = tree.getroot()
        except ET.ParseError:
            self.report({'ERROR'}, "Error reading temporary XML file.")
            return {'CANCELLED'}

        gates_element = root.find('gates')
        if gates_element is None:
            self.report({'ERROR'}, "Element <gates> not found in the file.")
            return {'CANCELLED'}

        # Находим CombinedMeshObject
        combined_mesh_object = bpy.context.scene.objects.get("CombinedMeshObject")

        if not combined_mesh_object:
            self.report({'ERROR'}, "CombinedMeshObject not found in the scene.")
            return {'CANCELLED'}

        # Получаем координаты из CombinedMeshObject и делаем конвертацию XZY в XYZ
        vertices = combined_mesh_object.data.vertices
        left_points = []
        right_points = []

        for i in range(0, len(vertices), 2):
            if i + 1 < len(vertices):  # Проверка на наличие правой точки
                left_coord = vertices[i].co
                right_coord = vertices[i + 1].co

                # Конвертация из XZY в XYZ
                left_points.append((left_coord.x, -left_coord.z, left_coord.y))  # X, -Z, Y
                right_points.append((right_coord.x, -right_coord.z, right_coord.y))  # X, -Z, Y

        # Обновляем данные для gates
        for i, gate in enumerate(gates_element.findall('gate')):
            left_element = gate.find('left')
            right_element = gate.find('right')

            if left_element is not None and right_element is not None:
                if i < len(left_points) and i < len(right_points):
                    left_coords = left_points[i]
                    right_coords = right_points[i]

                    # Записываем в формате X, Y, Z
                    left_text = f"{round(left_coords[0], 3)} {round(-left_coords[1], 3)} {round(left_coords[2], 3)}"  # XYZ
                    right_text = f"{round(right_coords[0], 3)} {round(-right_coords[1], 3)} {round(right_coords[2], 3)}"  # XYZ

                    left_element.text = left_text
                    right_element.text = right_text

        # Обновление элемента <points> для 2DMAP_SPLINE
        points_element = root.find('points')
        if points_element is None:
            points_element = ET.SubElement(root, 'points')

        # Обновляем точки 2DMAP_SPLINE с глобальным зеркалированием Y
        twodmap_spline = bpy.context.scene.objects.get("2DMAP_SPLINE")
        twodmap_points = twodmap_spline.data.splines[0].points

        for i, point in enumerate(twodmap_points):
            x_coord = point.co.x
            y_coord = point.co.y
            z_coord = point.co.z

            # Применяем глобальное зеркалирование Y
            # Обратите внимание: Y координата станет отрицательной
            y_coord_mirrored = -y_coord

            # Учитываем переход с XYZ (Blender) на XZY для экспорта
            if i < len(points_element.findall('line')[0].findall('point')):
                point_element = points_element.find('line').findall('point')[i]
            else:
                point_element = ET.SubElement(points_element.find('line'), 'point', id=str(i))

            position_element = point_element.find('position')
            if position_element is not None:
                # Конвертация: X, -Y становится X, Z, Y для XZY
                position_element.text = f"{round(x_coord, 3)} {round(z_coord, 3)} {round(y_coord, 3)}"  # X, Z, -Y
            else:
                position_element = ET.SubElement(point_element, 'position', format="float3")
                position_element.text = f"{round(x_coord, 3)} {round(z_coord, 3)} {round(y_coord, 3)}"  # X, Z, -Y

        # Запись обновленного дерева обратно в файл
        tree.write(temp_filepath, encoding='utf-8', xml_declaration=True)

class SPLINE_OT_select_gate(Operator):
    bl_idname = "spline.select_gate"
    bl_label = "Select Gate"
    point_index: IntProperty()

    def execute(self, context):
        left_spline = bpy.context.scene.objects.get("LEFT_SPLINE")
        right_spline = bpy.context.scene.objects.get("RIGHT_SPLINE")
        if left_spline and right_spline:
            for spline_object in (left_spline, right_spline):
                bpy.context.view_layer.objects.active = spline_object
                bpy.ops.object.mode_set(mode='EDIT')

                spline = spline_object.data.splines.active
                if spline and hasattr(spline, "points"):
                    if 0 <= self.point_index < len(spline.points):
                        spline.points[self.point_index].select = True
        return {'FINISHED'}

class SPLINE_OT_select_point(Operator):
    bl_idname = "spline.select_point"
    bl_label = "Select Spline Point"
    point_index: IntProperty()
    spline_name: StringProperty()

    def execute(self, context):
        spline_object = bpy.context.scene.objects.get(self.spline_name)
        if spline_object:
            bpy.context.view_layer.objects.active = spline_object
            bpy.ops.object.mode_set(mode='EDIT')

            spline = spline_object.data.splines.active
            if spline and hasattr(spline, "points"):
                if 0 <= self.point_index < len(spline.points):
                    for point in spline.points:
                        point.select = False
                    spline.points[self.point_index].select = True
        return {'FINISHED'}

class SPLINE_OT_select_split_gate(Operator):
    bl_idname = "spline.select_split_gate"
    bl_label = "Select Split Gate"
    split_gate_idx: IntProperty()

    def execute(self, context):
        left_spline = bpy.context.scene.objects.get("LEFT_SPLINE")
        right_spline = bpy.context.scene.objects.get("RIGHT_SPLINE")

        if left_spline and right_spline:
            for spline_object in (left_spline, right_spline):
                bpy.context.view_layer.objects.active = spline_object
                bpy.ops.object.mode_set(mode='EDIT')

                spline = spline_object.data.splines.active
                if spline and hasattr(spline, "points"):
                    if 0 <= self.split_gate_idx < len(spline.points):
                        for point in spline.points:
                            point.select = False
                        spline.points[self.split_gate_idx].select = True
        return {'FINISHED'}

class SPLINE_OT_new_split_gate(Operator):
    bl_idname = "spline.new_split_gate"
    bl_label = "New Split Gate"
    split_idx: IntProperty()

    def execute(self, context):
        left_spline = bpy.context.scene.objects.get("LEFT_SPLINE")
        right_spline = bpy.context.scene.objects.get("RIGHT_SPLINE")
        
        selected_gate_index = None

        if left_spline and right_spline:
            for spline_object in (left_spline, right_spline):
                bpy.context.view_layer.objects.active = spline_object
                bpy.ops.object.mode_set(mode='EDIT')
                
                spline = spline_object.data.splines.active
                if spline and hasattr(spline, "points"):
                    for i, point in enumerate(spline.points):
                        if point.select:
                            selected_gate_index = i
                            break

        if selected_gate_index is not None:
            temp_filepath = context.scene.get("temp_ai_progress_file", "")
            if not temp_filepath:
                self.report({'ERROR'}, "Temporary file not found.")
                return {'CANCELLED'}
            
            try:
                tree = ET.parse(temp_filepath)
                root = tree.getroot()
                
                route_element = root.find('.//routes/route[@id="0"]')
                if route_element:
                    split_element = route_element.find(f'.//split[@id="{self.split_idx}"]')
                    if split_element is not None:
                        split_element.set("gate", str(selected_gate_index))
                        tree.write(temp_filepath, encoding='utf-8', xml_declaration=True)
                        self.report({'INFO'}, f"Updated split {self.split_idx} with new gate index {selected_gate_index}")
            except ET.ParseError:
                self.report({'ERROR'}, "Error reading temporary XML file.")
                return {'CANCELLED'}
        else:
            self.report({'ERROR'}, "No control point selected.")
            return {'CANCELLED'}

        return {'FINISHED'}        

class TransferShapeOperator(Operator):
    """Transfer Shape from Target to Source Curve"""
    bl_idname = "f1.transfer_shape"
    bl_label = "Transfer Shape"

    source_curve_name: StringProperty()
    target_curve_name: StringProperty()

    def execute(self, context):
        source_obj = bpy.data.objects.get(self.source_curve_name)
        target_obj = bpy.data.objects.get(self.target_curve_name)

        if not source_obj or not target_obj:
            self.report({'ERROR'}, f"Source or Target curve not found: {self.source_curve_name} or {self.target_curve_name}")
            return {'CANCELLED'}

        source_spline = source_obj.data.splines[0]
        target_spline = target_obj.data.splines[0]

        if not hasattr(source_spline, "points") or not hasattr(target_spline, "bezier_points"):
            self.report({'ERROR'}, "Incorrect spline types")
            return {'CANCELLED'}

        num_source_points = len(source_spline.points)
        num_target_points = len(target_spline.bezier_points)

        if num_target_points == 0:
            self.report({'ERROR'}, "Target spline has no points.")
            return {'CANCELLED'}

        # Перенос формы на основе контроля таргет кривой
        for i in range(num_source_points):
            # Рассчитываем нормализованный параметр t для исходного сплайна
            t = i / (num_source_points - 1)
            # Получаем интерполированные координаты для целевой кривой
            new_position = self.interpolate_position(target_spline, t)

            # Применяем зеркальное отражение
            source_spline.points[i].co = (new_position[0], -new_position[1], new_position[2], 1.0)

        self.report({'INFO'}, f"Transferred shape from {self.target_curve_name} to {self.source_curve_name}")
        return {'FINISHED'}

    def interpolate_position(self, spline, t):
        points = spline.bezier_points
        num_points = len(points)

        if num_points == 0:
            return Vector((0, 0, 0))  # Возвращаем нулевой вектор если не существует точек

        # Рассчитываем индексы точек для интерполяции
        i1 = int(t * (num_points - 1))
        i2 = min(i1 + 1, num_points - 1)

        p1 = points[i1].co
        p2 = points[i2].co

        alpha = (t * (num_points - 1)) % 1.0

        vec1 = Vector(p1)
        vec2 = Vector(p2)

        return vec1.lerp(vec2, alpha)

class CreateTargetProgress(Operator):
    """Create Target Bezier Curves with Vector Handles"""
    bl_idname = "f1.create_target_progress"
    bl_label = "Create Target Surface"
    bl_description = ("Adds two target splines in the scene. "
                      "Draw the track shape and use transfer shape progress "
                      "on them in the next section.")
    
    def execute(self, context):
        curve_names = ["LEFT_TARGET", "RIGHT_TARGET", "2DMAP_TARGET"]
        for name in curve_names:
            if name not in bpy.data.objects:
                curve_data = bpy.data.curves.new(name=name, type='CURVE')
                curve_data.dimensions = '3D'
                spline = curve_data.splines.new('BEZIER')
                spline.bezier_points.add(1)

                start_point = spline.bezier_points[0]
                start_point.co = Vector((0, 0, 0))
                start_point.handle_left_type = 'VECTOR'
                start_point.handle_right_type = 'VECTOR'
                
                end_point = spline.bezier_points[1]
                end_point.co = Vector((0, 20, 0))
                end_point.handle_left_type = 'VECTOR'
                end_point.handle_right_type = 'VECTOR'

                curve_object = bpy.data.objects.new(name, curve_data)
                context.collection.objects.link(curve_object)
        
        self.report({'INFO'}, "Created LEFT_TARGET, RIGHT_TARGET, and 2DMAP_TARGET curves")
        return {'FINISHED'}

class VIEW3D_PT_ai_progress_track_maker_panel(Panel):
    bl_label = "F1 Track AI Progress Maker"
    bl_idname = "VIEW3D_PT_ai_progress_track_maker"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'F1 Track Builder Toolkit'

    def draw(self, context):
        layout = self.layout
        scene = context.scene

        layout.operator(IMPORT_OT_ai_progress_track_maker.bl_idname, text="Import Track Progress", icon='IMPORT')
        layout.operator(EXPORT_OT_ai_progress_track_maker.bl_idname, text="Export Track Progress", icon='EXPORT')

        layout.operator(CreateTargetProgress.bl_idname, text="Add New Shape Spline", icon='MOD_LINEART')
        layout.label(text="Progress Spline Shape Transfer", icon='OUTLINER_DATA_CURVE')

        # LEFT_SPLINE
        layout.label(text="Target for LEFT_SPLINE")
        row = layout.row(align=True)
        row.prop_search(scene, "left_spline_target", bpy.data, "objects", text="")
        left_transfer_op = row.operator(TransferShapeOperator.bl_idname, text="Transfer", icon='ARROW_LEFTRIGHT')
        left_transfer_op.source_curve_name = "LEFT_SPLINE"
        left_transfer_op.target_curve_name = scene.left_spline_target

        # RIGHT_SPLINE
        layout.label(text="Target for RIGHT_SPLINE")
        row = layout.row(align=True)
        row.prop_search(scene, "right_spline_target", bpy.data, "objects", text="")
        right_transfer_op = row.operator(TransferShapeOperator.bl_idname, text="Transfer", icon='ARROW_LEFTRIGHT')
        right_transfer_op.source_curve_name = "RIGHT_SPLINE"
        right_transfer_op.target_curve_name = scene.right_spline_target

        # 2DMAP_SPLINE
        layout.label(text="Target for 2DMAP_SPLINE")
        row = layout.row(align=True)
        row.prop_search(scene, "twodmap_spline_target", bpy.data, "objects", text="")
        twodmap_transfer_op = row.operator(TransferShapeOperator.bl_idname, text="Transfer", icon='ARROW_LEFTRIGHT')
        twodmap_transfer_op.source_curve_name = "2DMAP_SPLINE"
        twodmap_transfer_op.target_curve_name = scene.twodmap_spline_target
        
        row = layout.row()
        row.prop(scene, "show_gates", icon="TRIA_DOWN" if scene.show_gates else "TRIA_RIGHT", emboss=False, text="Show Gates")

        if scene.show_gates:
            left_spline = bpy.context.scene.objects.get("LEFT_SPLINE")
            right_spline = bpy.context.scene.objects.get("RIGHT_SPLINE")

            if left_spline and right_spline:
                for i in range(len(left_spline.data.splines[0].points)):
                    row = layout.row()
                    gate_op = row.operator("spline.select_gate", text=f"Gate ID: {i}")
                    gate_op.point_index = i
                    left_op = row.operator("spline.select_point", text="Left")
                    left_op.point_index = i
                    left_op.spline_name = "LEFT_SPLINE"
                    right_op = row.operator("spline.select_point", text="Right")
                    right_op.point_index = i
                    right_op.spline_name = "RIGHT_SPLINE"

        # New section for splits
        box = layout.box()
        box.label(text="Track Splits", icon='SNAP_PEEL_OBJECT')

        temp_filepath = context.scene.get("temp_ai_progress_file", "")
        if temp_filepath:
            try:
                tree = ET.parse(temp_filepath)
                root = tree.getroot()
                
                route_element = root.find('.//routes/route[@id="0"]')
                if route_element:
                    for split_element in route_element.findall('.//split'):
                        split_id = split_element.get("id")
                        split_type = split_element.get("type").capitalize()
                        gate_id = split_element.get("gate")

                        row = box.row()
                        row.label(text=f"{split_id} {split_type} / Gate {gate_id}")

                        select_op = row.operator(SPLINE_OT_select_split_gate.bl_idname, text="Select Gate", icon='RESTRICT_SELECT_OFF')
                        select_op.split_gate_idx = int(gate_id)

                        new_gate_op = row.operator(SPLINE_OT_new_split_gate.bl_idname, text="New Gate", icon='FILE_REFRESH')
                        new_gate_op.split_idx = int(split_id)
            except ET.ParseError:
                box.label(text="Error reading XML file.")

def register():
    bpy.types.Scene.left_spline_target = StringProperty(
        name="Left Spline Target"
    )
    bpy.types.Scene.right_spline_target = StringProperty(
        name="Right Spline Target"
    )
    bpy.types.Scene.twodmap_spline_target = StringProperty(
        name="2DMAP Spline Target"
    )
    bpy.types.Scene.temp_ai_progress_file = StringProperty(
        name="Temporary AI Progress File Path"
    )
    bpy.types.Scene.show_gates = BoolProperty(
        name="Show Gates",
        default=False
    )

    bpy.utils.register_class(IMPORT_OT_ai_progress_track_maker)
    bpy.utils.register_class(EXPORT_OT_ai_progress_track_maker)
    bpy.utils.register_class(SPLINE_OT_select_gate)
    bpy.utils.register_class(SPLINE_OT_select_point)
    bpy.utils.register_class(TransferShapeOperator)
    bpy.utils.register_class(CreateTargetProgress)
    bpy.utils.register_class(SPLINE_OT_select_split_gate)
    bpy.utils.register_class(SPLINE_OT_new_split_gate)
    bpy.utils.register_class(VIEW3D_PT_ai_progress_track_maker_panel)

def unregister():
    del bpy.types.Scene.left_spline_target
    del bpy.types.Scene.right_spline_target
    del bpy.types.Scene.twodmap_spline_target
    del bpy.types.Scene.temp_ai_progress_file
    del bpy.types.Scene.show_gates

    bpy.utils.unregister_class(IMPORT_OT_ai_progress_track_maker)
    bpy.utils.unregister_class(EXPORT_OT_ai_progress_track_maker)
    bpy.utils.unregister_class(VIEW3D_PT_ai_progress_track_maker_panel)
    bpy.utils.unregister_class(SPLINE_OT_select_gate)
    bpy.utils.unregister_class(SPLINE_OT_select_point)
    bpy.utils.unregister_class(TransferShapeOperator)
    bpy.utils.unregister_class(CreateTargetProgress)
    bpy.utils.unregister_class(SPLINE_OT_select_split_gate)
    bpy.utils.unregister_class(SPLINE_OT_new_split_gate)

if __name__ == "__main__":
    register()