914 lines
33 KiB
C#
914 lines
33 KiB
C#
using System;
|
||
using System.Linq;
|
||
using System.Threading;
|
||
using AIImages.Helpers;
|
||
using AIImages.Models;
|
||
using AIImages.Services;
|
||
using RimWorld;
|
||
using UnityEngine;
|
||
using Verse;
|
||
|
||
#pragma warning disable IDE1006 // Naming Styles
|
||
|
||
namespace AIImages
|
||
{
|
||
/// <summary>
|
||
/// Окно для просмотра персонажа и генерации AI изображений
|
||
/// </summary>
|
||
[System.Diagnostics.CodeAnalysis.SuppressMessage(
|
||
"Style",
|
||
"IDE1006:Naming Styles",
|
||
Justification = "RimWorld Window naming convention"
|
||
)]
|
||
[System.Diagnostics.CodeAnalysis.SuppressMessage(
|
||
"Minor Code Smell",
|
||
"S101:Types should be named in PascalCase",
|
||
Justification = "RimWorld Window naming convention"
|
||
)]
|
||
public class Window_AIImage : Window
|
||
{
|
||
private Pawn pawn;
|
||
private PawnAppearanceData appearanceData;
|
||
private StableDiffusionSettings generationSettings;
|
||
|
||
private Texture2D generatedImage;
|
||
private bool isGenerating = false;
|
||
private string generationStatus = "";
|
||
private CancellationTokenSource cancellationTokenSource;
|
||
|
||
// Прогресс генерации
|
||
private double generationProgress = 0.0;
|
||
private int currentStep = 0;
|
||
private int totalSteps = 0;
|
||
private double etaSeconds = 0.0;
|
||
|
||
// Сервисы (получаем через DI)
|
||
private readonly IPawnDescriptionService pawnDescriptionService;
|
||
private readonly IPromptGeneratorService promptGeneratorService;
|
||
private readonly IStableDiffusionApiService apiService;
|
||
|
||
public Window_AIImage(Pawn pawn)
|
||
{
|
||
this.pawn = pawn;
|
||
this.doCloseX = true;
|
||
this.doCloseButton = false; // Убираем дублирующую кнопку "Закрыть"
|
||
this.forcePause = false;
|
||
this.absorbInputAroundWindow = false;
|
||
this.draggable = true;
|
||
this.preventCameraMotion = false;
|
||
|
||
// Получаем сервисы через DI контейнер
|
||
var services = AIImagesMod.Services;
|
||
pawnDescriptionService = services.PawnDescriptionService;
|
||
promptGeneratorService = services.PromptGeneratorService;
|
||
apiService = services.ApiService;
|
||
|
||
// Извлекаем данные персонажа
|
||
RefreshPawnData();
|
||
}
|
||
|
||
public override Vector2 InitialSize => new Vector2(900f, 800f);
|
||
|
||
private Vector2 scrollPosition = Vector2.zero;
|
||
private Vector2 rightColumnScrollPosition = Vector2.zero;
|
||
private Vector2 promptScrollPosition = Vector2.zero;
|
||
private Vector2 negativePromptScrollPosition = Vector2.zero;
|
||
private float copiedMessageTime = 0f;
|
||
|
||
// Состояние сворачиваемых секций
|
||
private bool showPositivePrompt = false;
|
||
private bool showNegativePrompt = false;
|
||
private bool showCharacterInfo = true;
|
||
|
||
/// <summary>
|
||
/// Обновляет данные персонажа
|
||
/// </summary>
|
||
private void RefreshPawnData()
|
||
{
|
||
appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn);
|
||
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
|
||
|
||
// Загружаем сохраненный портрет, если есть
|
||
LoadSavedPortrait();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Загружает сохраненный портрет персонажа
|
||
/// </summary>
|
||
private void LoadSavedPortrait()
|
||
{
|
||
if (PawnPortraitHelper.HasPortrait(pawn))
|
||
{
|
||
generatedImage = PawnPortraitHelper.LoadPortrait(pawn);
|
||
if (generatedImage != null)
|
||
{
|
||
generationStatus = "AIImages.Generation.LoadedFromSave".Translate();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Освобождает ресурсы при закрытии окна
|
||
/// </summary>
|
||
public override void PreClose()
|
||
{
|
||
base.PreClose();
|
||
|
||
// Отменяем генерацию, если она идет
|
||
if (isGenerating && cancellationTokenSource != null)
|
||
{
|
||
cancellationTokenSource.Cancel();
|
||
}
|
||
|
||
// Освобождаем CancellationTokenSource
|
||
cancellationTokenSource?.Dispose();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Обновляет текущую пешку в окне
|
||
/// </summary>
|
||
public void UpdatePawn(Pawn newPawn)
|
||
{
|
||
this.pawn = newPawn;
|
||
RefreshPawnData();
|
||
|
||
// Очищаем старое изображение при смене персонажа
|
||
generatedImage = null;
|
||
generationStatus = "";
|
||
generationProgress = 0.0;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получить текущую пешку
|
||
/// </summary>
|
||
public Pawn CurrentPawn => pawn;
|
||
|
||
/// <summary>
|
||
/// Вызывается каждый кадр для обновления окна
|
||
/// </summary>
|
||
public override void WindowUpdate()
|
||
{
|
||
base.WindowUpdate();
|
||
|
||
// Проверяем, изменилась ли выбранная пешка
|
||
Pawn selectedPawn = Find.Selector.SelectedPawns.FirstOrDefault(p =>
|
||
p.IsColonist && p.Spawned && p.Faction == Faction.OfPlayer
|
||
);
|
||
|
||
// Если выбрана новая колонистская пешка, обновляем окно
|
||
if (selectedPawn != null && selectedPawn != pawn)
|
||
{
|
||
UpdatePawn(selectedPawn);
|
||
}
|
||
|
||
// Уменьшаем таймер сообщения о копировании
|
||
if (copiedMessageTime > 0f)
|
||
{
|
||
copiedMessageTime -= Time.deltaTime;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Асинхронная генерация изображения с поддержкой отмены
|
||
/// </summary>
|
||
private async System.Threading.Tasks.Task GenerateImageAsync()
|
||
{
|
||
if (isGenerating)
|
||
return;
|
||
|
||
// Создаем новый CancellationTokenSource
|
||
cancellationTokenSource?.Dispose();
|
||
cancellationTokenSource = new CancellationTokenSource();
|
||
|
||
isGenerating = true;
|
||
generationStatus = "AIImages.Generation.InProgress".Translate();
|
||
generationProgress = 0.0;
|
||
currentStep = 0;
|
||
totalSteps = generationSettings.Steps;
|
||
|
||
try
|
||
{
|
||
// Генерируем промпты
|
||
string positivePrompt = promptGeneratorService.GeneratePositivePrompt(
|
||
appearanceData,
|
||
generationSettings
|
||
);
|
||
string negativePrompt = promptGeneratorService.GenerateNegativePrompt(
|
||
generationSettings
|
||
);
|
||
|
||
// Создаем запрос
|
||
var request = new GenerationRequest
|
||
{
|
||
Prompt = positivePrompt,
|
||
NegativePrompt = negativePrompt,
|
||
Steps = generationSettings.Steps,
|
||
CfgScale = generationSettings.CfgScale,
|
||
Width = generationSettings.Width,
|
||
Height = generationSettings.Height,
|
||
Sampler = generationSettings.Sampler,
|
||
Scheduler = generationSettings.Scheduler,
|
||
Seed = generationSettings.Seed,
|
||
Model = AIImagesMod.Settings.apiEndpoint,
|
||
SaveImagesToServer = AIImagesMod.Settings.saveImagesToServer,
|
||
};
|
||
|
||
// Создаем отдельный CancellationTokenSource для мониторинга прогресса
|
||
var progressCts = new CancellationTokenSource();
|
||
var progressTask = MonitorProgressAsync(progressCts.Token);
|
||
|
||
// Генерируем изображение с поддержкой отмены
|
||
var result = await apiService.GenerateImageAsync(
|
||
request,
|
||
cancellationTokenSource.Token
|
||
);
|
||
|
||
// Останавливаем мониторинг прогресса
|
||
progressCts.Cancel();
|
||
try
|
||
{
|
||
await progressTask;
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// Ожидаемое исключение при остановке мониторинга
|
||
}
|
||
finally
|
||
{
|
||
progressCts.Dispose();
|
||
}
|
||
|
||
if (result.Success)
|
||
{
|
||
// Загружаем текстуру
|
||
generatedImage = new Texture2D(2, 2);
|
||
generatedImage.LoadImage(result.ImageData);
|
||
generationStatus = "AIImages.Generation.Success".Translate();
|
||
generationProgress = 1.0;
|
||
|
||
// Сохраняем путь к портрету на персонаже
|
||
PawnPortraitHelper.SavePortraitPath(pawn, result.SavedPath);
|
||
|
||
Messages.Message(
|
||
"AIImages.Generation.SavedTo".Translate(result.SavedPath),
|
||
MessageTypeDefOf.PositiveEvent
|
||
);
|
||
}
|
||
else
|
||
{
|
||
generationStatus =
|
||
$"AIImages.Generation.Failed".Translate() + ": {result.ErrorMessage}";
|
||
Messages.Message(generationStatus, MessageTypeDefOf.RejectInput);
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
generationStatus = "AIImages.Generation.Cancelled".Translate();
|
||
Log.Message("[AI Images] Generation cancelled by user");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
generationStatus = $"Error: {ex.Message}";
|
||
Log.Error($"[AI Images] Generation error: {ex}");
|
||
Messages.Message(
|
||
$"AIImages.Generation.Error".Translate() + ": {ex.Message}",
|
||
MessageTypeDefOf.RejectInput
|
||
);
|
||
}
|
||
finally
|
||
{
|
||
isGenerating = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Мониторит прогресс генерации и обновляет UI
|
||
/// </summary>
|
||
private async System.Threading.Tasks.Task MonitorProgressAsync(
|
||
CancellationToken cancellationToken
|
||
)
|
||
{
|
||
try
|
||
{
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
var progress = await apiService.GetProgressAsync(cancellationToken);
|
||
|
||
if (progress != null && progress.IsActive)
|
||
{
|
||
generationProgress = progress.Progress;
|
||
currentStep = progress.CurrentStep;
|
||
totalSteps = progress.TotalSteps;
|
||
etaSeconds = progress.EtaRelative;
|
||
|
||
Log.Message(
|
||
$"[AI Images] Progress: {progress.Progress:P} - Step {progress.CurrentStep}/{progress.TotalSteps} - ETA: {progress.EtaRelative:F1}s"
|
||
);
|
||
}
|
||
|
||
// Обновляем каждые 500ms
|
||
await System.Threading.Tasks.Task.Delay(500, cancellationToken);
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
// Ожидаемое исключение при остановке
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Warning($"[AI Images] Progress monitoring error: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Запускает генерацию изображения (обертка для безопасного fire-and-forget)
|
||
/// </summary>
|
||
private void StartGeneration()
|
||
{
|
||
AsyncHelper.FireAndForget(GenerateImageAsync(), "Image Generation");
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отменяет генерацию изображения
|
||
/// </summary>
|
||
private void CancelGeneration()
|
||
{
|
||
if (isGenerating && cancellationTokenSource != null)
|
||
{
|
||
cancellationTokenSource.Cancel();
|
||
generationStatus = "AIImages.Generation.Cancelling".Translate();
|
||
}
|
||
}
|
||
|
||
public override void DoWindowContents(Rect inRect)
|
||
{
|
||
float curY = 0f;
|
||
|
||
// Заголовок
|
||
Text.Font = GameFont.Medium;
|
||
Widgets.Label(
|
||
new Rect(0f, curY, inRect.width, 40f),
|
||
"AIImages.Window.Title".Translate()
|
||
);
|
||
curY += 45f;
|
||
|
||
// Имя пешки
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(
|
||
new Rect(0f, curY, inRect.width, 30f),
|
||
"AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve())
|
||
);
|
||
curY += 40f;
|
||
|
||
// Разделитель
|
||
Widgets.DrawLineHorizontal(0f, curY, inRect.width);
|
||
curY += 10f;
|
||
|
||
// Разделяем на две колонки: левая - информация, правая - изображение
|
||
float leftColumnWidth = inRect.width * 0.55f;
|
||
float rightColumnWidth = inRect.width * 0.42f;
|
||
float columnGap = inRect.width * 0.03f;
|
||
|
||
// Левая колонка - прокручиваемая информация
|
||
Rect leftColumnRect = new Rect(0f, curY, leftColumnWidth, inRect.height - curY);
|
||
DrawLeftColumn(leftColumnRect);
|
||
|
||
// Правая колонка - превью и управление
|
||
Rect rightColumnRect = new Rect(
|
||
leftColumnWidth + columnGap,
|
||
curY,
|
||
rightColumnWidth,
|
||
inRect.height - curY
|
||
);
|
||
DrawRightColumn(rightColumnRect);
|
||
}
|
||
|
||
private void DrawLeftColumn(Rect rect)
|
||
{
|
||
Rect scrollViewRect = new Rect(0f, 0f, rect.width - 20f, CalculateContentHeight());
|
||
Widgets.BeginScrollView(rect, ref scrollPosition, scrollViewRect);
|
||
|
||
float contentY = 0f;
|
||
|
||
// Портрет персонажа (если есть)
|
||
if (generatedImage != null)
|
||
{
|
||
float portraitSize = Mathf.Min(scrollViewRect.width - 20f, 200f);
|
||
Rect portraitRect = new Rect(
|
||
(scrollViewRect.width - portraitSize) / 2f,
|
||
contentY,
|
||
portraitSize,
|
||
portraitSize
|
||
);
|
||
|
||
// Рамка вокруг портрета
|
||
Widgets.DrawBox(portraitRect);
|
||
GUI.DrawTexture(portraitRect.ContractedBy(2f), generatedImage);
|
||
contentY += portraitSize + 20f;
|
||
}
|
||
|
||
// Сворачиваемая секция "Информация о персонаже"
|
||
DrawCollapsibleSection(
|
||
scrollViewRect,
|
||
contentY,
|
||
"AIImages.CharacterInfo.Title".Translate(),
|
||
ref showCharacterInfo,
|
||
() => DrawCharacterInfoContent(scrollViewRect)
|
||
);
|
||
|
||
Widgets.EndScrollView();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает информацию о персонаже в компактном виде
|
||
/// </summary>
|
||
private float DrawCharacterInfoContent(Rect parentRect)
|
||
{
|
||
float contentHeight = 0f;
|
||
float lineHeight = 24f;
|
||
float indent = 10f;
|
||
|
||
Text.Font = GameFont.Tiny;
|
||
|
||
// Базовая информация
|
||
var info = new[]
|
||
{
|
||
("AIImages.Info.Gender".Translate(), appearanceData.Gender.ToString()),
|
||
("AIImages.Info.Age".Translate(), appearanceData.Age.ToString()),
|
||
("AIImages.Info.BodyType".Translate(), appearanceData.BodyType),
|
||
(
|
||
"AIImages.Info.SkinTone".Translate(),
|
||
ColorDescriptionService.GetSkinToneDescription(appearanceData.SkinColor)
|
||
),
|
||
("AIImages.Info.Hair".Translate(), appearanceData.HairStyle),
|
||
(
|
||
"AIImages.Info.HairColor".Translate(),
|
||
ColorDescriptionService.GetHairColorDescription(appearanceData.HairColor)
|
||
),
|
||
};
|
||
|
||
foreach (var (label, value) in info)
|
||
{
|
||
Widgets.Label(
|
||
new Rect(indent, contentHeight, parentRect.width * 0.4f, lineHeight),
|
||
label + ":"
|
||
);
|
||
Widgets.Label(
|
||
new Rect(
|
||
parentRect.width * 0.42f,
|
||
contentHeight,
|
||
parentRect.width * 0.55f,
|
||
lineHeight
|
||
),
|
||
value
|
||
);
|
||
contentHeight += lineHeight;
|
||
}
|
||
|
||
// Черты характера
|
||
if (pawn.story?.traits?.allTraits != null && pawn.story.traits.allTraits.Any())
|
||
{
|
||
contentHeight += 10f;
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(
|
||
new Rect(indent, contentHeight, parentRect.width - indent * 2, lineHeight),
|
||
"AIImages.Info.Traits".Translate() + ":"
|
||
);
|
||
contentHeight += lineHeight;
|
||
|
||
Text.Font = GameFont.Tiny;
|
||
var traitLabels = pawn.story.traits.allTraits.Select(trait => trait.LabelCap);
|
||
foreach (var traitLabel in traitLabels)
|
||
{
|
||
Widgets.Label(
|
||
new Rect(
|
||
indent * 2,
|
||
contentHeight,
|
||
parentRect.width - indent * 3,
|
||
lineHeight
|
||
),
|
||
"• " + traitLabel
|
||
);
|
||
contentHeight += lineHeight;
|
||
}
|
||
}
|
||
|
||
// Одежда
|
||
var apparel = pawn.apparel?.WornApparel;
|
||
if (apparel != null && apparel.Any())
|
||
{
|
||
contentHeight += 10f;
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(
|
||
new Rect(indent, contentHeight, parentRect.width - indent * 2, lineHeight),
|
||
"AIImages.Info.Apparel".Translate() + ":"
|
||
);
|
||
contentHeight += lineHeight;
|
||
|
||
Text.Font = GameFont.Tiny;
|
||
foreach (var item in apparel)
|
||
{
|
||
var colorDesc = ColorDescriptionService.GetApparelColorDescription(
|
||
item.DrawColor
|
||
);
|
||
string apparelLabel = $"• {colorDesc} {item.def.label}";
|
||
float apparelHeight = Text.CalcHeight(
|
||
apparelLabel,
|
||
parentRect.width - indent * 3
|
||
);
|
||
Widgets.Label(
|
||
new Rect(
|
||
indent * 2,
|
||
contentHeight,
|
||
parentRect.width - indent * 3,
|
||
apparelHeight
|
||
),
|
||
apparelLabel
|
||
);
|
||
contentHeight += apparelHeight;
|
||
}
|
||
}
|
||
|
||
return contentHeight + 10f;
|
||
}
|
||
|
||
private void DrawRightColumn(Rect rect)
|
||
{
|
||
// Рассчитываем высоту контента для скролла
|
||
float contentHeight = CalculateRightColumnHeight();
|
||
Rect scrollViewRect = new Rect(0f, 0f, rect.width - 20f, contentHeight);
|
||
|
||
Widgets.BeginScrollView(rect, ref rightColumnScrollPosition, scrollViewRect);
|
||
|
||
float curY = 0f;
|
||
|
||
curY = DrawImagePreview(scrollViewRect, curY);
|
||
curY = DrawGenerationStatus(scrollViewRect, curY);
|
||
curY = DrawProgressBar(scrollViewRect, curY);
|
||
curY = DrawGenerationButton(scrollViewRect, curY);
|
||
|
||
// Сворачиваемая секция с позитивным промптом
|
||
curY = DrawPromptSection(
|
||
scrollViewRect,
|
||
curY,
|
||
"AIImages.Prompt.PositiveTitle".Translate(),
|
||
ref showPositivePrompt,
|
||
() =>
|
||
promptGeneratorService.GeneratePositivePrompt(
|
||
appearanceData,
|
||
generationSettings
|
||
),
|
||
new Color(0.1f, 0.3f, 0.1f, 0.5f),
|
||
ref promptScrollPosition
|
||
);
|
||
|
||
curY += 5f;
|
||
|
||
// Сворачиваемая секция с негативным промптом
|
||
curY = DrawPromptSection(
|
||
scrollViewRect,
|
||
curY,
|
||
"AIImages.Prompt.NegativeTitle".Translate(),
|
||
ref showNegativePrompt,
|
||
() => promptGeneratorService.GenerateNegativePrompt(generationSettings),
|
||
new Color(0.3f, 0.1f, 0.1f, 0.5f),
|
||
ref negativePromptScrollPosition
|
||
);
|
||
|
||
curY += 10f;
|
||
|
||
// Кнопка обновления данных
|
||
if (
|
||
Widgets.ButtonText(
|
||
new Rect(0f, curY, scrollViewRect.width, 30f),
|
||
"AIImages.Window.Refresh".Translate()
|
||
)
|
||
)
|
||
{
|
||
RefreshPawnData();
|
||
}
|
||
|
||
// Сообщение о копировании
|
||
if (copiedMessageTime > 0f)
|
||
{
|
||
curY += 35f;
|
||
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
|
||
Widgets.Label(
|
||
new Rect(0f, curY, scrollViewRect.width, 25f),
|
||
"AIImages.Prompt.Copied".Translate()
|
||
);
|
||
GUI.color = Color.white;
|
||
}
|
||
|
||
Widgets.EndScrollView();
|
||
}
|
||
|
||
private float CalculateContentHeight()
|
||
{
|
||
float height = 0f;
|
||
|
||
// Портрет персонажа (если есть)
|
||
if (generatedImage != null)
|
||
{
|
||
float portraitSize = Mathf.Min(400f, 200f);
|
||
height += portraitSize + 15f;
|
||
}
|
||
|
||
// Заголовок "Внешность"
|
||
height += 35f;
|
||
|
||
// Текст внешности
|
||
string appearanceText = pawnDescriptionService.GetAppearanceDescription(pawn);
|
||
height += Text.CalcHeight(appearanceText, 400f) + 20f;
|
||
|
||
// Разделитель
|
||
height += 15f;
|
||
|
||
// Заголовок "Одежда"
|
||
height += 35f;
|
||
|
||
// Текст одежды
|
||
string apparelText = pawnDescriptionService.GetApparelDescription(pawn);
|
||
height += Text.CalcHeight(apparelText, 400f) + 20f;
|
||
|
||
// Дополнительный отступ
|
||
height += 50f;
|
||
|
||
return height;
|
||
}
|
||
|
||
private float CalculateRightColumnHeight()
|
||
{
|
||
float height = 0f;
|
||
|
||
// Превью изображения
|
||
if (generatedImage != null)
|
||
{
|
||
height += 200f + 10f;
|
||
}
|
||
else if (!isGenerating)
|
||
{
|
||
height += 100f + 10f;
|
||
}
|
||
|
||
// Статус генерации
|
||
if (!string.IsNullOrEmpty(generationStatus))
|
||
{
|
||
height += 30f;
|
||
}
|
||
|
||
// Прогресс бар
|
||
if (isGenerating && generationProgress > 0.0)
|
||
{
|
||
height += 30f;
|
||
}
|
||
|
||
// Кнопка генерации
|
||
height += 40f;
|
||
|
||
// Позитивный промпт
|
||
height += 35f; // Заголовок
|
||
height += 100f + 10f; // Бокс
|
||
|
||
// Негативный промпт
|
||
height += 35f; // Заголовок
|
||
height += 100f + 10f; // Бокс
|
||
|
||
// Кнопки копирования
|
||
height += 35f;
|
||
|
||
// Кнопка обновления
|
||
height += 35f;
|
||
|
||
// Сообщение о копировании
|
||
if (copiedMessageTime > 0f)
|
||
{
|
||
height += 30f;
|
||
}
|
||
|
||
return height + 50f; // Дополнительный отступ
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает сворачиваемую секцию с заголовком и содержимым
|
||
/// </summary>
|
||
private void DrawCollapsibleSection(
|
||
Rect parentRect,
|
||
float startY,
|
||
string title,
|
||
ref bool expanded,
|
||
System.Func<float> drawContentFunc
|
||
)
|
||
{
|
||
float curY = startY;
|
||
float headerHeight = 32f;
|
||
|
||
// Рисуем заголовок с фоном
|
||
Rect headerRect = new Rect(0f, curY, parentRect.width, headerHeight);
|
||
Widgets.DrawBoxSolid(headerRect, new Color(0.25f, 0.25f, 0.25f, 0.8f));
|
||
Widgets.DrawBox(headerRect);
|
||
|
||
// Иконка раскрытия (треугольник)
|
||
string icon = expanded ? "▼" : "►";
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(new Rect(8f, curY + 6f, 20f, headerHeight), icon);
|
||
|
||
// Заголовок
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(new Rect(30f, curY + 6f, parentRect.width - 40f, headerHeight), title);
|
||
|
||
// Клик для раскрытия/сворачивания
|
||
if (Widgets.ButtonInvisible(headerRect))
|
||
{
|
||
expanded = !expanded;
|
||
}
|
||
|
||
// Рисуем содержимое если развернуто
|
||
if (expanded)
|
||
{
|
||
drawContentFunc();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отрисовывает секцию с промптом
|
||
/// </summary>
|
||
private float DrawPromptSection(
|
||
Rect parentRect,
|
||
float startY,
|
||
string title,
|
||
ref bool expanded,
|
||
System.Func<string> getPromptFunc,
|
||
Color backgroundColor,
|
||
ref Vector2 scrollPosition
|
||
)
|
||
{
|
||
float curY = startY;
|
||
float headerHeight = 32f;
|
||
|
||
// Получаем промпт
|
||
string prompt = getPromptFunc();
|
||
|
||
// Рисуем заголовок с фоном
|
||
Rect headerRect = new Rect(0f, curY, parentRect.width, headerHeight);
|
||
Widgets.DrawBoxSolid(headerRect, new Color(0.25f, 0.25f, 0.25f, 0.8f));
|
||
Widgets.DrawBox(headerRect);
|
||
|
||
// Иконка раскрытия
|
||
string icon = expanded ? "▼" : "►";
|
||
Text.Font = GameFont.Small;
|
||
Widgets.Label(new Rect(8f, curY + 6f, 20f, headerHeight), icon);
|
||
|
||
// Заголовок
|
||
Widgets.Label(new Rect(30f, curY + 6f, parentRect.width - 80f, headerHeight), title);
|
||
|
||
// Кнопка копирования в заголовке
|
||
Rect copyButtonRect = new Rect(parentRect.width - 70f, curY + 4f, 65f, 24f);
|
||
if (Widgets.ButtonText(copyButtonRect, "📋 " + "AIImages.Copy".Translate()))
|
||
{
|
||
GUIUtility.systemCopyBuffer = prompt;
|
||
copiedMessageTime = 2f;
|
||
}
|
||
|
||
// Клик на остальной области для раскрытия/сворачивания
|
||
Rect clickableHeaderRect = new Rect(0f, curY, parentRect.width - 75f, headerHeight);
|
||
if (Widgets.ButtonInvisible(clickableHeaderRect))
|
||
{
|
||
expanded = !expanded;
|
||
}
|
||
|
||
curY += headerHeight + 4f;
|
||
|
||
// Рисуем содержимое если развернуто
|
||
if (expanded)
|
||
{
|
||
float promptBoxHeight = 120f;
|
||
Text.Font = GameFont.Tiny;
|
||
float actualPromptHeight = Text.CalcHeight(prompt, parentRect.width - 20f);
|
||
|
||
Rect promptOuterRect = new Rect(0f, curY, parentRect.width, promptBoxHeight);
|
||
Rect promptViewRect = new Rect(0f, 0f, parentRect.width - 20f, actualPromptHeight);
|
||
|
||
// Рисуем фон
|
||
Widgets.DrawBoxSolid(promptOuterRect, backgroundColor);
|
||
Widgets.DrawBox(promptOuterRect);
|
||
|
||
// Рисуем промпт с прокруткой
|
||
Widgets.BeginScrollView(
|
||
promptOuterRect.ContractedBy(5f),
|
||
ref scrollPosition,
|
||
promptViewRect
|
||
);
|
||
|
||
var prevAnchor = Text.Anchor;
|
||
Text.Anchor = TextAnchor.UpperLeft;
|
||
Text.WordWrap = true;
|
||
|
||
Widgets.Label(
|
||
new Rect(0f, 0f, promptViewRect.width, promptViewRect.height),
|
||
prompt
|
||
);
|
||
|
||
Text.Anchor = prevAnchor;
|
||
Text.WordWrap = true;
|
||
|
||
Widgets.EndScrollView();
|
||
|
||
curY += promptBoxHeight + 10f;
|
||
}
|
||
|
||
return curY;
|
||
}
|
||
|
||
private float DrawImagePreview(Rect rect, float curY)
|
||
{
|
||
if (generatedImage != null)
|
||
{
|
||
float imageSize = Mathf.Min(rect.width, 400f);
|
||
Rect imageRect = new Rect(
|
||
rect.x + (rect.width - imageSize) / 2f,
|
||
rect.y + curY,
|
||
imageSize,
|
||
imageSize
|
||
);
|
||
GUI.DrawTexture(imageRect, generatedImage);
|
||
return curY + imageSize + 10f;
|
||
}
|
||
|
||
float placeholderSize = Mathf.Min(rect.width, 300f);
|
||
Rect placeholderRect = new Rect(
|
||
rect.x + (rect.width - placeholderSize) / 2f,
|
||
rect.y + curY,
|
||
placeholderSize,
|
||
placeholderSize
|
||
);
|
||
Widgets.DrawBoxSolid(placeholderRect, new Color(0.2f, 0.2f, 0.2f));
|
||
Text.Anchor = TextAnchor.MiddleCenter;
|
||
Widgets.Label(placeholderRect, "AIImages.Generation.NoImage".Translate());
|
||
Text.Anchor = TextAnchor.UpperLeft;
|
||
return curY + placeholderSize + 10f;
|
||
}
|
||
|
||
private float DrawGenerationStatus(Rect rect, float curY)
|
||
{
|
||
if (string.IsNullOrEmpty(generationStatus))
|
||
return curY;
|
||
|
||
Text.Font = GameFont.Small;
|
||
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
|
||
Widgets.Label(
|
||
new Rect(rect.x, rect.y + curY, rect.width, statusHeight),
|
||
generationStatus
|
||
);
|
||
return curY + statusHeight + 10f;
|
||
}
|
||
|
||
private float DrawProgressBar(Rect rect, float curY)
|
||
{
|
||
if (!isGenerating || generationProgress <= 0.0)
|
||
return curY;
|
||
|
||
Rect progressBarRect = new Rect(rect.x, rect.y + curY, rect.width, 24f);
|
||
|
||
string progressText;
|
||
if (totalSteps > 0)
|
||
{
|
||
progressText =
|
||
$"{(generationProgress * 100):F1}% - Step {currentStep}/{totalSteps}";
|
||
if (etaSeconds > 0)
|
||
{
|
||
progressText += $" - ETA: {etaSeconds:F0}s";
|
||
}
|
||
}
|
||
else
|
||
{
|
||
progressText = $"{(generationProgress * 100):F1}%";
|
||
}
|
||
|
||
Widgets.FillableBar(progressBarRect, (float)generationProgress);
|
||
Text.Font = GameFont.Tiny;
|
||
Text.Anchor = TextAnchor.MiddleCenter;
|
||
Widgets.Label(progressBarRect, progressText);
|
||
Text.Anchor = TextAnchor.UpperLeft;
|
||
return curY + 30f;
|
||
}
|
||
|
||
private float DrawGenerationButton(Rect rect, float curY)
|
||
{
|
||
Text.Font = GameFont.Small;
|
||
string buttonLabel = isGenerating
|
||
? "AIImages.Generation.Cancel".Translate()
|
||
: "AIImages.Generation.Generate".Translate();
|
||
|
||
if (Widgets.ButtonText(new Rect(rect.x, rect.y + curY, rect.width, 35f), buttonLabel))
|
||
{
|
||
if (isGenerating)
|
||
CancelGeneration();
|
||
else
|
||
StartGeneration();
|
||
}
|
||
return curY + 40f;
|
||
}
|
||
}
|
||
}
|