аддон для блендер 3.3.0

import bpy
import os
import shutil
from xml.etree import ElementTree as ET
from bpy_extras.io_utils import ImportHelper, ExportHelper
from mathutils import Vector, Quaternion
from math import atan2, sin, cos, degrees, radians

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
    )

    # Новые свойства для выбора целевых кривых
    bpy.types.Scene.Track_Line_target = bpy.props.StringProperty(
        name="Track Line Target",
        default="Track_TARGET"
    )
    bpy.types.Scene.Start_Line_target = bpy.props.StringProperty(
        name="Start Line Target",
        default="Start_TARGET"
    )
    bpy.types.Scene.Trial_Line_target = bpy.props.StringProperty(
        name="Trial Line Target",
        default="Trial_TARGET"
    )
    bpy.types.Scene.Pit_Line_target = bpy.props.StringProperty(
        name="Pit Cone Target",
        default="Pit_TARGET"
    )

def extract_normal_and_set_orientation(gate_empty, normal):
    forward_axis = Vector((0, 0, 1))
    target_vector = Vector((normal[2], normal[1], normal[0]))
    rotation = forward_axis.rotation_difference(target_vector)

    # Инвертируем компонент W, чтобы повернуть на 180 градусов
    reordered_rotation = Quaternion((-rotation.w, rotation.x, rotation.z, rotation.y))
    
    gate_empty.rotation_mode = 'QUATERNION'
    gate_empty.rotation_quaternion = reordered_rotation

    gate_empty['imported_normal'] = normal  # Сохраняем оригинал нормали

def create_gate_object(i, x, y, z, gate, is_splitter, context):
    y_mirrored = -y
    gate_name = f"gate_{i:02}"
    
    gate_empty = bpy.data.objects.new(gate_name, None)
    gate_empty.empty_display_type = 'CONE'  # Убедитесь, что форма - это конус

    if is_splitter:
        # Найдем все To_Gate связанные со сплиттерами
        for link in splitter_data:
            from_gate, to_gate, _ = link
            # Если текущий gate_name соответствует to_gate
            if gate_name == to_gate:
                gate_empty.empty_display_size = 3.25 * 2.0  # Увеличиваем размер в 2.5 раза
                gate_empty.scale[1] = 1.0 / 1.5  # Уменьшаем длину (высоту) в 1.5 раза
                break
        else:
            gate_empty.empty_display_size = 3.25  # Обычный размер для других сплиттеров
    else:
        gate_empty.empty_display_size = 1.0  # Для обычных gate

    gate_empty.location = (x, y_mirrored, z)
    gate_empty["gate_number"] = i

    normal_elem = gate.find('normal')
    if normal_elem is not None:
        normal = list(map(float, normal_elem.text.split()))
        extract_normal_and_set_orientation(gate_empty, normal)
        gate_empty['imported_normal'] = normal
        
    # Передаём позицию gate и список waypoints для обработки
    waypoints_elem = gate.find('waypoints')
    if waypoints_elem is not None:
        waypoints = waypoints_elem.findall('waypoint')
        create_gate_waypoints(gate_empty, (x, y, z), normal, waypoints, context)

    gate_empty.rotation_euler.rotate_axis("Z", 3.14159)
    context.collection.objects.link(gate_empty)
    gate_objects.append(gate_empty)

def create_gate_waypoints(gate_empty, position, normal, waypoints, context):
    """
    Создаёт EMPTY объекты для каждого waypoint, устанавливая локальные координаты относительно gate.
    """
    global gate_objects

    # Получаем номер текущего gate
    gate_number = gate_empty['gate_number']

    for wp in waypoints:  # Без использования enumerate
        length = float(wp.get('length', '0.0'))  # Берём значение length
        wp_type = wp.get('type')
        wp_id = int(wp.get('id'))

        # Координаты waypoint в ЛОКАЛЬНОЙ системе координат.
        wp_x = length        # X = length (локальная координата)
        wp_y = 0.0           # Y = 0 (фиксируется)
        wp_z = 0.0           # Z = 0 (фиксируется)

        # Создаём имя waypoint на основе значения wp_type и номера gate
        waypoint_name = f"{wp_type}_{gate_number:02}"

        # Создаем объект EMPTY для waypoint
        waypoint_empty = bpy.data.objects.new(waypoint_name, None)

        # Определяем тип и размер в зависимости от имени
        if "left_racing_limit" in waypoint_name or "right_racing_limit" in waypoint_name:
            waypoint_empty.empty_display_type = 'CUBE'
            waypoint_empty.scale = (0.3, 0.5, 0.3)  # Сжатый куб
        elif "left_track_limit" in waypoint_name or "right_track_limit" in waypoint_name:
            waypoint_empty.empty_display_type = 'CUBE'
            waypoint_empty.scale = (0.5, 0.8, 0.5)  # Сжатый куб, такой же как для racing_limit
        elif "racing_line" in waypoint_name:
            waypoint_empty.empty_display_type = 'SPHERE'
            waypoint_empty.scale = (0.15, 0.15, 0.15)  # Маленькая сфера
        else:
            waypoint_empty.empty_display_type = 'SPHERE'  # Значение по умолчанию
            waypoint_empty.scale = (0.5, 0.5, 0.5)  # Размер по умолчанию, если не указано

        # Устанавливаем локальные координаты waypoint
        waypoint_empty.location = (wp_x, wp_y, wp_z)

        # Свяжем waypoint с родительским gate через parent
        waypoint_empty.parent = gate_empty
        
        # Блокируем трансформацию по Y и Z
        waypoint_empty.lock_location[1] = True  # Запрет на перемещение по Y
        waypoint_empty.lock_location[2] = True  # Запрет на перемещение по Z

        # Добавляем свойства waypoint
        waypoint_empty['wp_id'] = wp_id
        waypoint_empty['wp_type'] = wp_type
        waypoint_empty['length'] = length

        # Добавляем объект в сцену
        context.collection.objects.link(waypoint_empty)


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")

        
        # Добавляем кнопку для скрытия/показа Track Limits
        layout.operator(ToggleWaypointVisibility.bl_idname, text="Hide / Unhide Track Limits")

        if splitter_data:
            layout.label(text="Splitters in file:", icon='PLUGIN')
            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

        # Создаем контейнер (box)
        box = layout.box()
        # Добавляем свойство в контейнер
        box.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='CON_ROTLIMIT')
                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']}")

                    # Формируем имя gate для проверки выделенности
                    gate_name = f"gate_{line_data['brake_line']['gate_id']:02}"
                    gate_object = bpy.data.objects.get(gate_name)

                    # Проверяем, выделен ли объект
                    is_selected = gate_object and gate_object in context.selected_objects

                    # Меняем текст и иконку кнопки в зависимости от выделения объекта
                    select_text = "SELECTED" if is_selected else "Select Gate"
                    select_icon = 'CHECKBOX_HLT' if is_selected else 'RESTRICT_SELECT_OFF'
                    select_op = brake_row.operator("f1.select_gate_by_name", text=select_text, icon=select_icon)
                    select_op.gate_name = gate_name

                    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']}")

                        # Проверяем выделение для Min Speed Gate
                        min_speed_gate_name = f"gate_{min_speed_line['gate_id']:02}"
                        min_speed_gate_object = bpy.data.objects.get(min_speed_gate_name)
                        min_speed_is_selected = min_speed_gate_object and min_speed_gate_object in context.selected_objects

                        # Меняем текст и иконку для Min Speed Gate
                        ms_select_text = "SELECTED" if min_speed_is_selected else "Select Gate"
                        ms_select_icon = 'CHECKBOX_HLT' if min_speed_is_selected else 'RESTRICT_SELECT_OFF'
                        ms_op = ms_row.operator("f1.select_gate_by_name", text=ms_select_text, icon=ms_select_icon)
                        ms_op.gate_name = min_speed_gate_name
                        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}")

                    # Проверяем выделение для Hold Line Gate
                    hl_gate_name = f"gate_{hl_gate_id:02}"
                    hl_gate_object = bpy.data.objects.get(hl_gate_name)
                    hl_is_selected = hl_gate_object and hl_gate_object in context.selected_objects

                    # Меняем текст и иконку для Hold Line Gate
                    hl_select_text = "SELECTED" if hl_is_selected else "Select Gate"
                    hl_select_icon = 'CHECKBOX_HLT' if hl_is_selected else 'RESTRICT_SELECT_OFF'
                    hl_select_op = hl_row.operator("f1.select_gate_by_name", text=hl_select_text, icon=hl_select_icon)
                    hl_select_op.gate_name = hl_gate_name
                    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']

        # Новые элементы интерфейса для работы с безье-кривыми
        layout.operator("f1.create_target_curves", text="Create Target Curves")

        layout.label(text="Transfer Shape Layout:")
        layout.operator("f1.create_target_surface", text="Create Target Surface")

        # Заранее заданные исходные и целевые кривые
        transfer_pairs = [
            ("Track_Line", "Track_TARGET"),
            ("Start_Line", "Start_TARGET"),
            ("Trial_Line", "Trial_TARGET"),
            ("Pit_Line", "Pit_TARGET"),
        ]

        for source_name, target_default in transfer_pairs:
            # Создаем строку для названия и целевой кривой
            row = layout.row(align=True)
            
            # Добавляем текст с названием кривой
            row.label(text=f"{source_name}:")
            
            # Поле для выбора целевой кривой, чтобы оно было максимально широким
            target_row = layout.row(align=True)
            target_row.prop_search(context.scene, f"{source_name}_target", bpy.data, "objects", text="", icon='OUTLINER_OB_EMPTY')

            # Создаем новую строку для кнопок
            button_row = layout.row(align=True)

            # Получаем объект сплайна для определения состояния видимости
            spline_object = bpy.data.objects.get(source_name)

            # Проверяем состояние видимости и определяем текст кнопки
            if spline_object and not spline_object.hide_viewport:
                button_text = "Hide"
                icon = 'HIDE_OFF'
            else:
                button_text = "Unhide"
                icon = 'HIDE_ON'

            hide_button = button_row.operator(HideSplineGatesOperator.bl_idname, text=button_text, icon=icon)
            hide_button.target_spline_name = source_name  # Устанавливаем имя сплайна для оператора

            # Кнопка Transfer
            transfer_op = button_row.operator("f1.transfer_shape", text="Transfer")
            transfer_op.source_curve_name = source_name
            transfer_op.target_curve_name = context.scene.get(f"{source_name}_target", target_default)

            # Кнопка Correct Way
            correct_op = button_row.operator("f1.correct_way", text="Correct Way")
            correct_op.source_curve_name = source_name

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):
    """Splitters determine the transition from one line to another, they tend to be close to each other, imagine that this is a plug and socket. See the 'from gate to gate' example"""
    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
        else:
            gate_object.select_set(False)

        return {'FINISHED'}

class CreateTargetSurface(bpy.types.Operator):
    """Create New Target Bezier Curves with Vector Handles"""
    bl_idname = "f1.create_target_surface"
    bl_label = "Create Target Surface"
    
    def execute(self, context):
        curve_names = ["Track_TARGET", "Start_TARGET", "Trial_TARGET", "Pit_TARGET"]
        
        for name in curve_names:
            if name not in bpy.data.objects:
                # Создаем новую кривую типа CURVE
                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)  # Всего две точки (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))  # На 20 метров вперед по оси Y
                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'}, "4 Target Curves Created with Straight Lines (Vector Handles)")
        return {'FINISHED'}
 
from mathutils import Vector, Quaternion

from mathutils import Vector, Quaternion
from math import atan2, sin, cos, degrees

from mathutils import Vector, Quaternion

class CorrectGateOrientationOperator(bpy.types.Operator):
    """Corrects the orientation of gate objects to align with the curve"""
    bl_idname = "f1.correct_way"
    bl_label = "Correct Way"

    source_curve_name: bpy.props.StringProperty()

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

        if not source_obj:
            self.report({'ERROR'}, "Source curve not found")
            return {'CANCELLED'}

        source_spline = source_obj.data.splines[0]

        selected_gates = [obj for obj in context.selected_objects if obj.name.startswith("gate_")]

        if not selected_gates:
            self.report({'WARNING'}, "No gate_* objects selected")
            return {'CANCELLED'}

        for gate_object in selected_gates:
            # Найти ближайшую точку для каждого gate_object
            min_distance = float('inf')
            closest_point_index = 0
            
            for i, point in enumerate(source_spline.bezier_points):
                distance = (gate_object.location - point.co).length
                if distance < min_distance:
                    min_distance = distance
                    closest_point_index = i
            
            # Определяем направление между текущей и следующей точкой
            current_point = source_spline.bezier_points[closest_point_index]
            next_index = (closest_point_index + 1) % len(source_spline.bezier_points)
            next_point = source_spline.bezier_points[next_index]
            
            direction_vector = (next_point.co - current_point.co).normalized()
            if direction_vector.length == 0:
                continue

            # Создаем кватернион из 'up' ориентации и направления
            rotation_quaternion = direction_vector.to_track_quat('Z', 'Y')

            # Разворачиваем объект на 180 градусов вокруг оси Z
            flip_quaternion = Quaternion((0.0, 0.0, 1.0), radians(180))
            rotation_quaternion = flip_quaternion @ rotation_quaternion

            # Устанавливаем ненужные компоненты к нулю
            rotation_quaternion.x = 0.0
            rotation_quaternion.y = 0.0

            # Применяем кватернион вращения к объекту
            gate_object.rotation_mode = 'QUATERNION'
            gate_object.rotation_quaternion = rotation_quaternion

            # Отладочный вывод
            print(f"Gate {gate_object.name}: Quaternion={rotation_quaternion}")

        self.report({'INFO'}, "Corrected orientation for selected gate_* objects")
        return {'FINISHED'}
        
class TransferShapeOperator(bpy.types.Operator):
    """Transfer Shape from Source to Target Curve"""
    bl_idname = "f1.transfer_shape"
    bl_label = "Transfer Shape"
    
    source_curve_name: bpy.props.StringProperty()
    target_curve_name: bpy.props.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'}, "Source or Target curve not found")
            return {'CANCELLED'}
        
        source_spline = source_obj.data.splines[0]
        target_spline = target_obj.data.splines[0]
        
        # Interpolate target bezier points to match source spline's points
        total_source_points = len(source_spline.bezier_points)
        total_target_points = len(target_spline.bezier_points) - 1
        
        if total_target_points < 1:
            self.report({'ERROR'}, "Target curve must have at least 2 control points")
            return {'CANCELLED'}
        
        # Use linear interpolation to calculate new positions along the target curve
        for i, point in enumerate(source_spline.bezier_points):
            t = i / (total_source_points - 1)
            interpolated_index = int(t * total_target_points)
            next_interpolated_index = min(interpolated_index + 1, total_target_points)
            
            low_point = target_spline.bezier_points[interpolated_index].co
            high_point = target_spline.bezier_points[next_interpolated_index].co
            
            new_position = low_point.lerp(high_point, t * total_target_points - interpolated_index)
            point.co = new_position
        
        self.report({'INFO'}, f"Transferred shape to {self.source_curve_name}")
        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_dir = os.path.dirname(bpy.data.filepath)
        
        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

        success = self.import_from_temp_file(context, temp_file)

        if success == {'FINISHED'}:
            bpy.ops.object.update_gate_positions()

        return success

    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_Line_id = int(fork_set.find('.//fork[@fork_position="1"]').get('link_id'))

            split_link_elem = root.find('.//links/link[@id="0"]')
            if split_link_elem is None:
                self.report({'ERROR'}, "Link with ID 0 not found in the XML file.")
                return {'CANCELLED'}

            trial_split_gate_id = int(split_link_elem.get('from_gate'))

            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_Line_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_Line_Curve", type='CURVE')
            pit_curve_data.dimensions = '3D'
            pit_spline = pit_curve_data.splines.new('BEZIER')

            trial_line_curve_data = bpy.data.curves.new(name="Trial_Line_Curve", type='CURVE')
            trial_line_curve_data.dimensions = '3D'
            trial_line_spline = trial_line_curve_data.splines.new('BEZIER')

            start_line_curve_data = bpy.data.curves.new(name="Start_Line_Curve", type='CURVE')
            start_line_curve_data.dimensions = '3D'
            start_line_spline = start_line_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_Line_id]
            pit_points = [gate for gate in gates if int(gate.get('id')) > Trial_Line_id]

            trial_line_points = [gate for gate in trial_points if int(gate.get('id')) <= trial_split_gate_id]
            start_line_points = [gate for gate in trial_points if int(gate.get('id')) > trial_split_gate_id]

            track_spline.bezier_points.add(len(track_points) - 1)
            trial_line_spline.bezier_points.add(len(trial_line_points) - 1)
            start_line_spline.bezier_points.add(len(start_line_points) - 1)
            pit_spline.bezier_points.add(len(pit_points) - 1)

            # Временный словарь для связи empty_gate объектов со сплайнами
            empty_gates_info = {
                "Track_Line": [],
                "Trial_Line": [],
                "Start_Line": [],
                "Pit_Line": [],
            }

            for i, gate in enumerate(track_points):
                position = gate.find('position')
                if position is not None:
                    x, z, y = map(float, position.text.split())
                    y_mirrored = -y

                    control_point = track_spline.bezier_points[i]
                    control_point.co = (x, y_mirrored, 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_mirrored, z, gate, is_splitter, context)

                    # Добавляем данный пустышку в временный словарь
                    empty_gates_info["Track_Line"].append(f"gate_{i:02}")

            offset = len(track_points)
            for j, gate in enumerate(trial_line_points):
                position = gate.find('position')
                if position is not None:
                    x, z, y = map(float, position.text.split())
                    y_mirrored = -y

                    control_point = trial_line_spline.bezier_points[j]
                    control_point.co = (x, y_mirrored, 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_mirrored, z, gate, is_splitter, context)

                    # Добавляем данный пустышку в словарь
                    empty_gates_info["Trial_Line"].append(f"gate_{offset + j:02}")

            offset += len(trial_line_points)
            for k, gate in enumerate(start_line_points):
                position = gate.find('position')
                if position is not None:
                    x, z, y = map(float, position.text.split())
                    y_mirrored = -y

                    control_point = start_line_spline.bezier_points[k]
                    control_point.co = (x, y_mirrored, 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_mirrored, z, gate, is_splitter, context)

                    # Добавляем данный пустышку в словарь
                    empty_gates_info["Start_Line"].append(f"gate_{offset + k:02}")

            offset += len(start_line_points)
            for l, gate in enumerate(pit_points):
                position = gate.find('position')
                if position is not None:
                    x, z, y = map(float, position.text.split())
                    y_mirrored = -y

                    control_point = pit_spline.bezier_points[l]
                    control_point.co = (x, y_mirrored, z)
                    control_point.handle_left_type = 'VECTOR'
                    control_point.handle_right_type = 'VECTOR'

                    is_splitter = offset + l in splitter_ids
                    create_gate_object(offset + l, x, y_mirrored, z, gate, is_splitter, context)

                    # Добавляем данный пустышку в словарь
                    empty_gates_info["Pit_Line"].append(f"gate_{offset + l:02}")

            # Сохраним временный словарь в свойство сцены
            context.scene['empty_gates_info'] = empty_gates_info

            track_object = bpy.data.objects.new("Track_Line", track_curve_data)
            trial_line_object = bpy.data.objects.new("Trial_Line", trial_line_curve_data)
            start_line_object = bpy.data.objects.new("Start_Line", start_line_curve_data)
            pit_object = bpy.data.objects.new("Pit_Line", pit_curve_data)
            context.collection.objects.link(track_object)
            context.collection.objects.link(trial_line_object)
            context.collection.objects.link(start_line_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'}
            
class HideSplineGatesOperator(bpy.types.Operator):
    """Hide/Unhide selected spline and associated empty gates"""
    bl_idname = "f1.hide_unhide_spline_gates"
    bl_label = "Hide/Unhide Spline and Gates"

    target_spline_name: bpy.props.StringProperty()

    def execute(self, context):
        empty_gates_info = context.scene.get('empty_gates_info', {})
        gates_to_hide = empty_gates_info.get(self.target_spline_name, [])

        # Скрытие/показ сплайна
        spline_object = bpy.data.objects.get(self.target_spline_name)

        if spline_object:
            # Переключение состояния видимости сплайна
            spline_object.hide_viewport = not spline_object.hide_viewport

        # Скрытие/показ связанных empty gates
        for gate_name in gates_to_hide:
            gate = bpy.data.objects.get(gate_name)
            if gate:
                # Переключение состояния видимости для empty gate и его дочерних объектов
                gate.hide_viewport = not gate.hide_viewport
                for child in gate.children:
                    child.hide_viewport = gate.hide_viewport  # Дочерние будут иметь то же состояние видимости

        self.report({'INFO'}, f"Toggled {self.target_spline_name} and its associated gates visibility")
        return {'FINISHED'}    


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 = int(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 ToggleWaypointVisibility(bpy.types.Operator):
    """Hide or Unhide Track Limits"""
    bl_idname = "f1.toggle_waypoint_visibility"
    bl_label = "Hide/Unhide Track Limits"

    def execute(self, context):
        # Проходим по всем объектам и изменяем видимость
        for obj in context.collection.objects:
            if any(limit in obj.name for limit in ["left_racing_limit", "right_racing_limit",
                                                    "left_track_limit", "right_track_limit"]):
                obj.hide_viewport = not obj.hide_viewport  # Меняем состояние видимости
        
        return {'FINISHED'}

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 export_normal(self, gate_object):
        # Извлекаем кватернион объекта
        rotation_quat = gate_object.rotation_quaternion

        # Предполагаем, что forward-вектор задан как (1, 0, 0)
        example_forward_vector = Vector((1, 0, 0))

        # Получаем нормаль из ориентации
        normal_vector = rotation_quat @ example_forward_vector

        # Инвертируем вектор нормали (разворачиваем его на 180 градусов)
        flipped_normal = -normal_vector

        # Поменять местами Y и Z, а X оставить положительным
        return f"{-round(flipped_normal.x, 6)} {round(abs(flipped_normal.z), 6)} {round(flipped_normal.y, 6)}"

    def export_waypoints(self, gate_object, gate_elem):
        """
        Экспортирует waypoints с учётом их локальных координат (length).
        """
        wp_objects = [obj for obj in gate_object.children if "_" in obj.name and len(obj.name.split("_")) >= 2]
        wp_container = gate_elem.find('waypoints')

        if wp_container is not None:
            for wp_obj in wp_objects:
                # Извлекаем id из имени waypoint
                *wp_type_parts, gate_id_str = wp_obj.name.split("_")
                wp_type = "_".join(wp_type_parts)  # Возвращаем type в виде строки
                wp_id = wp_obj['wp_id']  # Получаем id из свойства

                # Поиск соответствующего элемента
                wp_elem = wp_container.find(f"waypoint[@id='{wp_id}']")
                if wp_elem is not None:
                    # Координаты waypoint в ЛОКАЛЬНОЙ системе координат (только X)
                    length = wp_obj.location.x  # Используем только X-координату в локальной системе

                    # Обновляем XML length
                    wp_elem.set('length', f"{length:.3f}")

                    # Убедимся, что Y и Z всегда равны 0
                    wp_elem.set('y', "0.0")
                    wp_elem.set('z', "0.0")

    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()

            gates = root.find('.//gates').findall('gate')
            for gate_object in gate_objects:
                gate_id = gate_object.get("gate_number", None)
                if gate_id is not None and gate_id < len(gates):
                    gate_elem = root.find(f".//gates/gate[@id='{gate_id}']")
                    if gate_elem is not None:
                        position = gate_elem.find('position')
                        if position is not None:
                            x, z, y = gate_object.location.x, gate_object.location.z, -gate_object.location.y
                            position.text = f"{round(x, 3)} {round(z, 3)} {round(y, 3)}"

                        # Используем перенесённый метод для экспорта нормали
                        normal_export = self.export_normal(gate_object)

                        normal_elem = gate_elem.find('normal')
                        if normal_elem is None:
                            normal_elem = ET.SubElement(gate_elem, 'normal')

                        normal_elem.set('format', 'float3')
                        normal_elem.text = normal_export

                        # Экспортируем waypoints для каждого gate
                        self.export_waypoints(gate_object, gate_elem)

            # Экспортируем дополнительные элементы
            brake_lines_elem = root.find('.//brake_lines')
            if brake_lines_elem is not None:
                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:
                        brake_line_elem.set('gate_id', str(line_data["brake_line"]["gate_id"]))

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

            # Сохранение изменений в 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_Line", "Start_Line", "Pit_Line"])

    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_line_points = len(bpy.data.objects["Trial_Line"].data.splines[0].bezier_points) if "Trial_Line" in bpy.data.objects else 0
        num_start_line_points = len(bpy.data.objects["Start_Line"].data.splines[0].bezier_points) if "Start_Line" in bpy.data.objects else 0

        for curve_name in ["Track_Line", "Trial_Line", "Start_Line", "Pit_Line"]:
            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_Line":
                        gate_index = num_track_points + i
                    elif curve_name == "Start_Line":
                        gate_index = num_track_points + num_trial_line_points + i
                    else:
                        gate_index = num_track_points + num_trial_line_points + num_start_line_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)
    bpy.utils.register_class(CorrectGateOrientationOperator)

    # Регистрация новых классов
    bpy.utils.register_class(CreateTargetSurface)  
    bpy.utils.register_class(TransferShapeOperator)
    bpy.utils.register_class(ToggleWaypointVisibility) 
    bpy.utils.register_class(HideSplineGatesOperator) 

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)

    # Отмена регистрации новых классов
    del bpy.types.Scene.Track_Line_target
    del bpy.types.Scene.Start_Line_target
    del bpy.types.Scene.Trial_Line_target
    del bpy.types.Scene.Pit_Line_target
    bpy.utils.unregister_class(CreateTargetSurface)  # Убедитесь, что этот класс отпиан
    bpy.utils.unregister_class(TransferShapeOperator)
    bpy.utils.unregister_class(ToggleWaypointVisibility)
    bpy.utils.unregister_class(HideSplineGatesOperator)

if __name__ == "__main__":
    register()