Вот полный код аддона для Блендер:

import bpy
import os
import shutil
from xml.etree import ElementTree as ET
from bpy_extras.io_utils import ImportHelper, ExportHelper

bl_info = {
    "name": "F1 AI Track Layout Maker",
    "author": "AlonsoBeast",
    "version": (1, 0),
    "blender": (3, 3, 0),
    "description": "Import and export F1 track layout from XML files",
    "category": "Object",
}

gate_objects = []
splitter_data = []
brake_lines_data = []

def parse_brake_lines(root):
    brake_lines_elem = root.find('.//brake_lines')
    brake_lines = brake_lines_elem.findall('brake_line')

    parsed_brake_lines = []
    for brake_line in brake_lines:
        brake_id = int(brake_line.get('id'))
        gate_id = int(brake_line.get('gate_id'))

        parsed_brake_lines.append({
            'brake_id': brake_id,
            'gate_id': gate_id,
            'brake_data': brake_line.find('brake_data')
        })
    return parsed_brake_lines

def parse_hold_lines(root):
    hold_lines_elem = root.find('.//hold_lines')
    hold_lines = hold_lines_elem.findall('hold_line')

    parsed_hold_lines = []
    for hold_line in hold_lines:
        hold_line_id = int(hold_line.get('id'))
        gate_id = int(hold_line.get('gate_id'))
        brakeline_id = int(hold_line.get('brakeline_id'))

        parsed_hold_lines.append({
            'hold_line_id': hold_line_id,
            'gate_id': gate_id,
            'brakeline_id': brakeline_id
        })
    return parsed_hold_lines

def parse_min_speed_lines(root):
    min_speed_lines_elem = root.find('.//min_speed_lines')
    min_speed_lines = min_speed_lines_elem.findall('min_speed_line')

    parsed_min_speed_lines = []
    for min_speed_line in min_speed_lines:
        min_speed_line_id = int(min_speed_line.get('id'))
        gate_id = int(min_speed_line.get('gate_id'))
        min_speed = float(min_speed_line.get('min_speed'))

        parsed_min_speed_lines.append({
            'min_speed_line_id': min_speed_line_id,
            'gate_id': gate_id,
            'min_speed': min_speed
        })
    return parsed_min_speed_lines

def associate_data(brake_lines, hold_lines, min_speed_lines):
    associated_lines = []

    for brake_line in brake_lines:
        matching_hold_line = next((hold_line for hold_line in hold_lines if hold_line['brakeline_id'] == brake_line['brake_id']), None)

        if matching_hold_line:
            matched_min_speed_lines = [
                min_speed_line for min_speed_line in min_speed_lines
                if brake_line['gate_id'] < min_speed_line['gate_id'] < matching_hold_line['gate_id']
            ]

            associated_lines.append({
                'brake_line': brake_line,
                'hold_line': matching_hold_line,
                'min_speed_lines': matched_min_speed_lines
            })

    return associated_lines

def init_properties():
    bpy.types.Scene.show_brake_lines = bpy.props.BoolProperty(
        name="Show Brake Lines",
        description="Expand to show brake lines details",
        default=False
    )

class F1AITrackLayoutMakerPanel(bpy.types.Panel):
    bl_label = "F1 Track Builder Toolkit"
    bl_idname = "VIEW3D_PT_f1_track_builder_toolkit"
    bl_space_type = 'VIEW_3D'
    bl_region_type = 'UI'
    bl_category = 'F1 Track'

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

        layout.operator(ImportF1TrackLayout.bl_idname, text="Import F1 Track Layout")
        layout.operator(ExportF1TrackLayout.bl_idname, text="Export F1 Track Layout")
        layout.operator(UpdateGatePositions.bl_idname, text="Update Gates")

        if splitter_data:
            layout.label(text="Splitters in file:", icon='LINK_BLEND')
            for from_gate, to_gate, link_info in splitter_data:
                box = layout.box()
                row = box.row(align=True)

                row.label(text=f"Link ID: {link_info.get('id')}", icon='DOT')

                from_btn = row.operator("f1.select_splitter_gate", text=f"From_Gate: {from_gate}")
                from_btn.gate_name = from_gate

                to_btn = row.operator("f1.select_splitter_gate", text=f"To_Gate: {to_gate}")
                to_btn.gate_name = to_gate

                box.scale_y = 0.75

        layout.prop(context.scene, "show_brake_lines", icon="TRIA_DOWN" if context.scene.show_brake_lines else "TRIA_RIGHT", emboss=False)

        if context.scene.show_brake_lines:
            if brake_lines_data:
                layout.label(text=f"Brake Lines Num: {len(brake_lines_data)}", icon='DECORATE_DRIVER')
                for line_data in brake_lines_data:
                    box = layout.box()
                    brake_row = box.row(align=True)
                    brake_row.label(text=f"Brake ID: {line_data['brake_line']['brake_id']} / Gate {line_data['brake_line']['gate_id']}")
                    select_op = brake_row.operator("f1.select_gate_by_name", text="Select Gate", icon='RESTRICT_SELECT_OFF')
                    select_op.gate_name = f"gate_{line_data['brake_line']['gate_id']:02}"
                    new_gate_op = brake_row.operator("f1.new_gate_id", text="New Gate", icon='FILE_REFRESH')
                    new_gate_op.brake_line_id = line_data['brake_line']['brake_id']

                    for min_speed_line in line_data['min_speed_lines']:
                        ms_row = box.row(align=True)
                        ms_row.label(text=f"Min Speed ID: {min_speed_line['min_speed_line_id']} / Gate: {min_speed_line['gate_id']}")
                        ms_op = ms_row.operator("f1.select_gate_by_name", text="Select Gate", icon='RESTRICT_SELECT_OFF')
                        ms_op.gate_name = f"gate_{min_speed_line['gate_id']:02}"
                        new_ms_gate_op = ms_row.operator("f1.new_min_speed_gate_id", text="New Gate", icon='FILE_REFRESH')
                        new_ms_gate_op.min_speed_line_id = min_speed_line['min_speed_line_id']

                    hl_row = box.row(align=True)
                    hl_gate_id = line_data['hold_line']['gate_id']
                    hl_row.label(text=f"Hold Line ID: {line_data['hold_line']['hold_line_id']} / Gate {hl_gate_id}")
                    hl_select_op = hl_row.operator("f1.select_gate_by_name", text="Select Gate", icon='RESTRICT_SELECT_OFF')
                    hl_select_op.gate_name = f"gate_{hl_gate_id:02}"
                    new_hl_gate_op = hl_row.operator("f1.new_hold_line_gate_id", text="New Gate", icon='FILE_REFRESH')
                    new_hl_gate_op.hold_line_id = line_data['hold_line']['hold_line_id']

class NewGateIDOperator(bpy.types.Operator):
    """Select single gate object to set as new"""
    bl_idname = "f1.new_gate_id"
    bl_label = "New Gate ID"

    brake_line_id: bpy.props.IntProperty()

    def execute(self, context):
        selected_objects = context.selected_objects

        if len(selected_objects) != 1 or not selected_objects[0].name.startswith("gate_"):
            self.report({'WARNING'}, "Please select a single gate object")
            return {'CANCELLED'}

        new_gate_id = int(selected_objects[0].name.split("_")[1])

        for line_data in brake_lines_data:
            if line_data['brake_line']['brake_id'] == self.brake_line_id:
                line_data['brake_line']['gate_id'] = new_gate_id
                break

        self.report({'INFO'}, f"Gate ID updated to: {new_gate_id}")
        return {'FINISHED'}

class NewMinSpeedGateIDOperator(bpy.types.Operator):
    """Select single gate object to set as new for Min Speed"""
    bl_idname = "f1.new_min_speed_gate_id"
    bl_label = "New Min Speed Gate ID"

    min_speed_line_id: bpy.props.IntProperty()

    def execute(self, context):
        selected_objects = context.selected_objects

        if len(selected_objects) != 1 or not selected_objects[0].name.startswith("gate_"):
            self.report({'WARNING'}, "Please select a single gate object")
            return {'CANCELLED'}

        new_gate_id = int(selected_objects[0].name.split("_")[1])

        for line_data in brake_lines_data:
            for min_speed_line in line_data['min_speed_lines']:
                if min_speed_line['min_speed_line_id'] == self.min_speed_line_id:
                    min_speed_line['gate_id'] = new_gate_id
                    break

        self.report({'INFO'}, f"Min Speed Gate ID updated to: {new_gate_id}")
        return {'FINISHED'}

class NewHoldLineGateIDOperator(bpy.types.Operator):
    """Select single gate object to set as new for Hold Line"""
    bl_idname = "f1.new_hold_line_gate_id"
    bl_label = "New Hold Line Gate ID"

    hold_line_id: bpy.props.IntProperty()

    def execute(self, context):
        selected_objects = context.selected_objects

        if len(selected_objects) != 1 or not selected_objects[0].name.startswith("gate_"):
            self.report({'WARNING'}, "Please select a single gate object")
            return {'CANCELLED'}

        new_gate_id = int(selected_objects[0].name.split("_")[1])

        for line_data in brake_lines_data:
            if line_data['hold_line']['hold_line_id'] == self.hold_line_id:
                line_data['hold_line']['gate_id'] = new_gate_id
                break

        self.report({'INFO'}, f"Hold Line Gate ID updated to: {new_gate_id}")
        return {'FINISHED'}

class SelectSplitterGate(bpy.types.Operator):
    """Select gate in scene"""
    bl_idname = "f1.select_splitter_gate"
    bl_label = "Select Splitter Gate"

    gate_name: bpy.props.StringProperty()

    def execute(self, context):
        gate_object = bpy.data.objects.get(self.gate_name)
        if gate_object is None:
            self.report({'WARNING'}, "Object not found")
            return {'CANCELLED'}

        if gate_object not in bpy.context.selected_objects:
            gate_object.select_set(True)

        context.view_layer.objects.active = gate_object
        return {'FINISHED'}

class SelectGateByName(bpy.types.Operator):
    """Select gate object by name in the scene"""
    bl_idname = "f1.select_gate_by_name"
    bl_label = "Select Gate by Name"

    gate_name: bpy.props.StringProperty()

    def execute(self, context):
        gate_object = bpy.data.objects.get(self.gate_name)
        if gate_object is None:
            self.report({'WARNING'}, f"Gate object {self.gate_name} not found")
            return {'CANCELLED'}

        if gate_object not in bpy.context.selected_objects:
            gate_object.select_set(True)

        context.view_layer.objects.active = gate_object
        return {'FINISHED'}

class ImportF1TrackLayout(bpy.types.Operator, ImportHelper):
    bl_idname = "import.f1_track_layout"
    bl_label = "Import F1 Track Layout"
    filename_ext = ".xml"

    def execute(self, context):
        global gate_objects, splitter_data, brake_lines_data

        # Путь к текущему .blend файлу
        blend_dir = os.path.dirname(bpy.data.filepath)
        
        # Создаем папку temp, если она не существует
        temp_dir = os.path.join(blend_dir, "temp")
        if not os.path.exists(temp_dir):
            os.makedirs(temp_dir)

        # Путь к временному файлу
        temp_file = os.path.join(temp_dir, f"{os.path.basename(self.filepath)}_temp.xml")

        # Копируем оригинальный файл во временный
        shutil.copy2(self.filepath, temp_file)
        context.scene['temp_import_filepath'] = temp_file

        # Загрузка данных из временного файла
        return self.import_from_temp_file(context, temp_file)

    def import_from_temp_file(self, context, temp_file):
        try:
            tree = ET.parse(temp_file)
            root = tree.getroot()
            
            gates = root.find('.//gates').findall('gate')

            if not gates:
                self.report({'ERROR'}, "No gates found in the XML file.")
                return {'CANCELLED'}

            splitter_data.clear()
            gate_objects.clear()
            brake_lines_data.clear()
            
            links = root.find('.//links').findall('link')
            splitter_ids = set()
            
            for link in links:
                from_gate = int(link.get('from_gate'))
                to_gate = int(link.get('to_gate'))
                if abs(from_gate - to_gate) > 1:
                    splitter_ids.update([from_gate, to_gate])
                    gate_name_from = f"gate_{from_gate:02}"
                    gate_name_to = f"gate_{to_gate:02}"
                    splitter_data.append((gate_name_from, gate_name_to, link))
            
            fork_set = root.find('.//fork_sets/fork_set')
            if not fork_set:
                self.report({'ERROR'}, "No fork_set found in the XML file.")
                return {'CANCELLED'}
            
            split_gate_id = int(fork_set.get('gate_id'))
            Trial_CIRCLE_id = int(fork_set.find('.//fork[@fork_position="1"]').get('link_id'))

            track_curve_data = bpy.data.curves.new(name="Track_Line_Curve", type='CURVE')
            track_curve_data.dimensions = '3D'
            track_spline = track_curve_data.splines.new('BEZIER')

            trial_curve_data = bpy.data.curves.new(name="Trial_CIRCLE_Curve", type='CURVE')
            trial_curve_data.dimensions = '3D'
            trial_spline = trial_curve_data.splines.new('BEZIER')

            pit_curve_data = bpy.data.curves.new(name="Pit_CONE_Curve", type='CURVE')
            pit_curve_data.dimensions = '3D'
            pit_spline = pit_curve_data.splines.new('BEZIER')

            track_points = [gate for gate in gates if int(gate.get('id')) <= split_gate_id]
            trial_points = [gate for gate in gates if split_gate_id < int(gate.get('id')) <= Trial_CIRCLE_id]
            pit_points = [gate for gate in gates if int(gate.get('id')) > Trial_CIRCLE_id]

            track_spline.bezier_points.add(len(track_points) - 1)
            trial_spline.bezier_points.add(len(trial_points) - 1)
            pit_spline.bezier_points.add(len(pit_points) - 1)

            def create_gate_object(i, x, y, z, is_splitter, context, curve_type=None):
                gate_name = f"gate_{i:02}"
                gate_empty = bpy.data.objects.new(gate_name, None)
                if is_splitter:
                    gate_empty.empty_display_type = 'SPHERE'
                    gate_empty.empty_display_size = 3.25
                elif curve_type == 'Pit_CONE':
                    gate_empty.empty_display_type = 'CONE'
                    gate_empty.empty_display_size = 2.65
                elif curve_type == 'Trial_CIRCLE':
                    gate_empty.empty_display_type = 'CIRCLE'
                    gate_empty.empty_display_size = 1.85
                else:
                    gate_empty.empty_display_type = 'CUBE'
                    gate_empty.empty_display_size = 0.5
                gate_empty.location = (x, y, z)
                gate_empty["gate_number"] = i
                context.collection.objects.link(gate_empty)
                gate_objects.append(gate_empty)

            for i, gate in enumerate(track_points):
                position = gate.find('position')
                if position is not None:
                    x, z, y = map(float, position.text.split())
                    control_point = track_spline.bezier_points[i]
                    control_point.co = (x, y, z)
                    control_point.handle_left_type = 'VECTOR'
                    control_point.handle_right_type = 'VECTOR'

                    is_splitter = i in splitter_ids
                    create_gate_object(i, x, y, z, is_splitter, context, 'Track_Line')

            offset = len(track_points)
            for j, gate in enumerate(trial_points):
                position = gate.find('position')
                if position is not None:
                    x, z, y = map(float, position.text.split())
                    control_point = trial_spline.bezier_points[j]
                    control_point.co = (x, y, z)
                    control_point.handle_left_type = 'VECTOR'
                    control_point.handle_right_type = 'VECTOR'

                    is_splitter = offset + j in splitter_ids
                    create_gate_object(offset + j, x, y, z, is_splitter, context, 'Trial_CIRCLE')

            offset += len(trial_points)
            for k, gate in enumerate(pit_points):
                position = gate.find('position')
                if position is not None:
                    x, z, y = map(float, position.text.split())
                    control_point = pit_spline.bezier_points[k]
                    control_point.co = (x, y, z)
                    control_point.handle_left_type = 'VECTOR'
                    control_point.handle_right_type = 'VECTOR'

                    is_splitter = offset + k in splitter_ids
                    create_gate_object(offset + k, x, y, z, is_splitter, context, 'Pit_CONE')

            track_object = bpy.data.objects.new("Track_Line", track_curve_data)
            trial_object = bpy.data.objects.new("Trial_CIRCLE", trial_curve_data)
            pit_object = bpy.data.objects.new("Pit_CONE", pit_curve_data)
            context.collection.objects.link(track_object)
            context.collection.objects.link(trial_object)
            context.collection.objects.link(pit_object)

            bpy.context.view_layer.objects.active = track_object
            track_object.select_set(True)

            brake_lines = parse_brake_lines(root)
            hold_lines = parse_hold_lines(root)
            min_speed_lines = parse_min_speed_lines(root)
            brake_lines_data.extend(associate_data(brake_lines, hold_lines, min_speed_lines))

            self.report({'INFO'}, f"Imported track layout from: {temp_file}")
            return {'FINISHED'}

        except ET.ParseError as e:
            self.report({'ERROR'}, f"XML parse error: {e}")
            return {'CANCELLED'}

def restore_gate_objects(context):
    temp_file = context.scene.get('temp_import_filepath', "")
    if not temp_file or not os.path.isfile(temp_file):
        return

    try:
        tree = ET.parse(temp_file)
        root = tree.getroot()

        gates = root.find('.//gates').findall('gate')

        global gate_objects
        gate_objects.clear()

        for gate in gates:
            gate_id = gate.get('id')
            gate_name = f"gate_{gate_id:02}"
            gate_object = bpy.data.objects.get(gate_name)
            if gate_object:
                gate_objects.append(gate_object)
    except ET.ParseError:
        return

class ExportF1TrackLayout(bpy.types.Operator, ExportHelper):
    bl_idname = "export.f1_track_layout"
    bl_label = "Export F1 Track Layout"
    filename_ext = ".xml"

    filter_glob: bpy.props.StringProperty(
        default="*.xml",
        options={'HIDDEN'},
    )

    def execute(self, context):
        temp_file = context.scene.get('temp_import_filepath', "")

        if not temp_file or not os.path.isfile(temp_file):
            self.report({'ERROR'}, "Temporary file not found. Please import the track layout first.")
            return {'CANCELLED'}

        try:
            tree = ET.parse(temp_file)
            root = tree.getroot()

            # Обновление данных ворот в XML
            track_curve = bpy.data.objects.get("Track_Line")
            trial_curve = bpy.data.objects.get("Trial_CIRCLE")
            pit_curve = bpy.data.objects.get("Pit_CONE")

            if not track_curve or not trial_curve or not pit_curve:
                self.report({'ERROR'}, "Some curve objects are missing.")
                return {'CANCELLED'}

            track_points = track_curve.data.splines[0].bezier_points
            trial_points = trial_curve.data.splines[0].bezier_points
            pit_points = pit_curve.data.splines[0].bezier_points
            combined_control_points = list(track_points) + list(trial_points) + list(pit_points)

            gates = root.find('.//gates').findall('gate')
            for i, control_point in enumerate(combined_control_points):
                if i < len(gates):
                    position = gates[i].find('position')
                    if position is not None:
                        position.text = f"{round(control_point.co.x, 3)} {round(control_point.co.z, 3)} {round(control_point.co.y, 3)}"

            # Обновление данных тормозных и удерживающих линий
            brake_lines_elem = root.find('.//brake_lines')
            for line_data in brake_lines_data:
                brake_line_elem = brake_lines_elem.find(f'brake_line[@id="{line_data["brake_line"]["brake_id"]}"]')
                if brake_line_elem is not None:
                    gate_id_str = str(line_data["brake_line"]["gate_id"])
                    brake_line_elem.set('gate_id', gate_id_str)

                min_speed_lines_elem = root.find('.//min_speed_lines')
                for min_speed_line in line_data['min_speed_lines']:
                    min_speed_line_elem = min_speed_lines_elem.find(f'min_speed_line[@id="{min_speed_line["min_speed_line_id"]}"]')
                    if min_speed_line_elem is not None:
                        gate_id_str = str(min_speed_line["gate_id"])
                        min_speed_line_elem.set('gate_id', gate_id_str)
                        min_speed_line_elem.set('min_speed', f'{min_speed_line["min_speed"]:.2f}')

            hold_lines_elem = root.find('.//hold_lines')
            for line_data in brake_lines_data:
                hold_line_elem = hold_lines_elem.find(f'hold_line[@id="{line_data["hold_line"]["hold_line_id"]}"]')
                if hold_line_elem is not None:
                    gate_id_str = str(line_data["hold_line"]["gate_id"])
                    hold_line_elem.set('gate_id', gate_id_str)

            # Сохранение обновленного XML
            tree.write(self.filepath)

            self.report({'INFO'}, f"Successfully exported track layout to: {self.filepath}")
            return {'FINISHED'}

        except ET.ParseError as e:
            self.report({'ERROR'}, f"XML parse error: {e}")
            return {'CANCELLED'}

class UpdateGatePositions(bpy.types.Operator):
    """Update gate positions based on the Bezier control points"""
    bl_idname = "object.update_gate_positions"
    bl_label = "Update Gate Positions"

    @classmethod
    def poll(cls, context):
        return any(name in bpy.data.objects for name in ["Track_Line", "Trial_CIRCLE", "Pit_CONE"])

    def execute(self, context):
        restore_gate_objects(context)

        num_track_points = len(bpy.data.objects["Track_Line"].data.splines[0].bezier_points) if "Track_Line" in bpy.data.objects else 0
        num_trial_points = len(bpy.data.objects["Trial_CIRCLE"].data.splines[0].bezier_points) if "Trial_CIRCLE" in bpy.data.objects else 0

        for curve_name in ["Track_Line", "Trial_CIRCLE", "Pit_CONE"]:
            if curve_name in bpy.data.objects:
                curve_object = bpy.data.objects[curve_name]
                control_points = curve_object.data.splines[0].bezier_points

                for i, point in enumerate(control_points):
                    if curve_name == "Track_Line":
                        gate_index = i
                    elif curve_name == "Trial_CIRCLE":
                        gate_index = num_track_points + i
                    else:
                        gate_index = num_track_points + num_trial_points + i

                    if gate_index < len(gate_objects):
                        gate_empty = gate_objects[gate_index]

                        if gate_empty:
                            gate_empty.location = (point.co.x, point.co.y, point.co.z)

        return {'FINISHED'}

def register():
    init_properties()
    bpy.utils.register_class(F1AITrackLayoutMakerPanel)
    bpy.utils.register_class(ImportF1TrackLayout)
    bpy.utils.register_class(ExportF1TrackLayout)
    bpy.utils.register_class(UpdateGatePositions)
    bpy.utils.register_class(SelectSplitterGate)
    bpy.utils.register_class(SelectGateByName)
    bpy.utils.register_class(NewGateIDOperator)
    bpy.utils.register_class(NewMinSpeedGateIDOperator)
    bpy.utils.register_class(NewHoldLineGateIDOperator)

def unregister():
    del bpy.types.Scene.show_brake_lines
    bpy.utils.unregister_class(F1AITrackLayoutMakerPanel)
    bpy.utils.unregister_class(ImportF1TrackLayout)
    bpy.utils.unregister_class(ExportF1TrackLayout)
    bpy.utils.unregister_class(UpdateGatePositions)
    bpy.utils.unregister_class(SelectSplitterGate)
    bpy.utils.unregister_class(SelectGateByName)
    bpy.utils.unregister_class(NewGateIDOperator)
    bpy.utils.unregister_class(NewMinSpeedGateIDOperator)
    bpy.utils.unregister_class(NewHoldLineGateIDOperator)

if __name__ == "__main__":
    register()