Привет, вот полный код аддона для Blender 3.3.0, просьба вытравить оттуда поиск fork_set. Только аккуратно, не задевая логику других. Главная задача, чтобы fork-set не привязывался к  безье и empty  объектам, можно даже сделать заглушку ,если удалить это из кода не получится. 


import bpy
import os
import xml.etree.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
        original_file_path = self.filepath
        context.scene['original_import_filepath'] = original_file_path  # Save filepath for later use

        if not os.path.isfile(original_file_path):
            self.report({'ERROR'}, "File does not exist.")
            return {'CANCELLED'}

        try:
            tree = ET.parse(original_file_path)
            root = tree.getroot()
        except ET.ParseError as e:
            self.report({'ERROR'}, f"XML parse error: {e}")
            return {'CANCELLED'}

        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, 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 = 2.45
            elif curve_type == 'Pit_CONE':
                gate_empty.empty_display_type = 'CONE'
                gate_empty.empty_display_size = 1.65  # 1.5 times the default size (0.5)
            elif curve_type == 'Trial_CIRCLE':
                gate_empty.empty_display_type = 'CIRCLE'
                gate_empty.empty_display_size = 1.85  # Adjust scale if needed
            else:  # Track_Line or unknown
                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, '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, '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, '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"Successfully imported {len(gates)} gates and processed brake lines.")
        return {'FINISHED'}

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

    def execute(self, context):
        # Retrieve the path to the original file
        original_file_path = context.scene.get('original_import_filepath', "")

        if not original_file_path:
            self.report({'ERROR'}, "Please import the track layout first.")
            return {'CANCELLED'}

        try:
            tree = ET.parse(original_file_path)
            root = tree.getroot()
        except ET.ParseError as e:
            self.report({'ERROR'}, f"XML parse error: {e}")
            return {'CANCELLED'}

        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, 2)} {round(control_point.co.z, 2)} {round(control_point.co.y, 2)}"

        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 = f'{line_data["brake_line"]["gate_id"]:02}'
                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 = f'{min_speed_line["gate_id"]:02}'
                    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 = f'{line_data["hold_line"]["gate_id"]:02}'
                hold_line_elem.set('gate_id', gate_id_str)

        export_filepath = os.path.splitext(original_file_path)[0] + "_modified.xml"
        tree.write(export_filepath)

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

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):
        global gate_objects

        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.name not in bpy.data.objects:
                            gate_empty = bpy.data.objects.new(f"gate_{gate_index:02}", None)
                            if gate_empty.empty_display_type == 'SPHERE':  # Keep splitters unchanged
                                gate_empty.empty_display_size = 2.75
                            elif curve_name == "Pit_CONE":
                                gate_empty.empty_display_type = 'CONE'
                                gate_empty.empty_display_size = 1.65  # 1.5 times the default size (0.5)
                            elif curve_name == "Trial_CIRCLE":
                                gate_empty.empty_display_type = 'CIRCLE'
                                gate_empty.empty_display_size = 1.85  # Adjust scale if needed
                            else:  # Track_Line or unknown
                                gate_empty.empty_display_type = 'CUBE'
                                gate_empty.empty_display_size = 0.5
                            gate_empty.location = (point.co.x, point.co.y, point.co.z)
                            gate_empty["gate_number"] = gate_index
                            context.collection.objects.link(gate_empty)
                            gate_objects[gate_index] = gate_empty
                        else:
                            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()