Приветствую вас. Есть аддон для Blender 3.3.0, импорт normal элементов работает и конвертируется для Blender корректно, но нужно написать экспортер для нормалей, чтобы они превратились в исходный формат normal. При этом, должны учитываться все объекты normal, те которые подвергались изменения или же нет. Необходимо математически перерасчитать с учетом того, что мы будем производить вращения (ротации) empty gate_* объектов в сцене, чтобы менялись значения normal, при этом, учитывая особенность правила исходного normal value (пример:   <normal format="float3">0.908714 0.0 -0.417419</normal> ) в Blender указывается как: W / 0.213643 X/ 0.0 Y/ 0.0 Z/ -0.976912

Вот код аддона:


 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

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
    )

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

    reordered_rotation = Quaternion((rotation.w, rotation.x, rotation.z, rotation.y))
    
    gate_empty.rotation_mode = 'QUATERNION'
    gate_empty.rotation_quaternion = reordered_rotation

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'
    gate_empty.empty_display_size = 3.25 if is_splitter else 1.0
    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_empty.rotation_euler.rotate_axis("Z", 3.14159)
    context.collection.objects.link(gate_empty)
    gate_objects.append(gate_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")

        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_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_CIRCLE_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_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')

            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_CIRCLE_id]
            pit_points = [gate for gate in gates if int(gate.get('id')) > Trial_CIRCLE_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)

            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)

            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)

            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)

            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)

            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_CONE", 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'}

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

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

                        # Обновление нормали на основании текущей трансформации
                        rotation_quat = gate_object.rotation_quaternion
                        forward_vector = Vector((0, 0, 1))
                        normal_vector = rotation_quat @ forward_vector

                        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 = f"{round(normal_vector.x, 6)} {round(normal_vector.y, 6)} {round(normal_vector.z, 6)}"

            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)

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

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