USER
вот код аддона для блендер: 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_CONE_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='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']
# Новые элементы интерфейса для работы с безье-кривыми
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_CONE", "Pit_TARGET"),
]
for source_name, target_default in transfer_pairs:
row = layout.row(align=True)
row.label(text=f"{source_name}:")
# Пропс для выбора целевой кривой
row.prop_search(context.scene, f"{source_name}_target", bpy.data, "objects", text="")
# Кнопка Hide/Unhide
# Получаем объект сплайна для определения состояния видимости
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 = row.operator(HideSplineGatesOperator.bl_idname, text=button_text, icon=icon)
hide_button.target_spline_name = source_name # Устанавливаем имя сплайна для оператора
# Кнопка Transfer
transfer_op = 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 = 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):
"""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 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_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_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_CONE": [],
}
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_CONE"].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_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'}
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_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)
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_CONE_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()