Files
ai-images/Source/AIImages/Window_AIImage.cs

515 lines
18 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
// Сервисы (получаем через 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 = true;
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 promptScrollPosition = Vector2.zero;
private float copiedMessageTime = 0f;
/// <summary>
/// Обновляет данные персонажа
/// </summary>
private void RefreshPawnData()
{
appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn);
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
}
/// <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();
}
/// <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();
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,
};
// Генерируем изображение с поддержкой отмены
var result = await apiService.GenerateImageAsync(
request,
cancellationTokenSource.Token
);
if (result.Success)
{
// Загружаем текстуру
generatedImage = new Texture2D(2, 2);
generatedImage.LoadImage(result.ImageData);
generationStatus = "AIImages.Generation.Success".Translate();
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>
/// Запускает генерацию изображения (обертка для безопасного 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;
// Секция "Внешность"
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(10f, contentY, scrollViewRect.width - 20f, 30f),
"AIImages.Appearance.SectionTitle".Translate()
);
contentY += 35f;
Text.Font = GameFont.Small;
string appearanceText = pawnDescriptionService.GetAppearanceDescription(pawn);
float appearanceHeight = Text.CalcHeight(appearanceText, scrollViewRect.width - 30f);
Widgets.Label(
new Rect(20f, contentY, scrollViewRect.width - 30f, appearanceHeight),
appearanceText
);
contentY += appearanceHeight + 20f;
// Разделитель
Widgets.DrawLineHorizontal(10f, contentY, scrollViewRect.width - 20f);
contentY += 15f;
// Секция "Одежда"
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(10f, contentY, scrollViewRect.width - 20f, 30f),
"AIImages.Apparel.SectionTitle".Translate()
);
contentY += 35f;
Text.Font = GameFont.Small;
string apparelText = pawnDescriptionService.GetApparelDescription(pawn);
float apparelHeight = Text.CalcHeight(apparelText, scrollViewRect.width - 30f);
Widgets.Label(
new Rect(20f, contentY, scrollViewRect.width - 30f, apparelHeight),
apparelText
);
Widgets.EndScrollView();
}
private void DrawRightColumn(Rect rect)
{
float curY = 0f;
// Превью изображения
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);
curY += imageSize + 10f;
}
else
{
// Placeholder для изображения
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;
curY += placeholderSize + 10f;
}
// Статус генерации
if (!string.IsNullOrEmpty(generationStatus))
{
Text.Font = GameFont.Small;
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
Widgets.Label(
new Rect(rect.x, rect.y + curY, rect.width, statusHeight),
generationStatus
);
curY += statusHeight + 10f;
}
// Кнопка генерации/отмены
Text.Font = GameFont.Small;
if (isGenerating)
{
// Показываем кнопку отмены во время генерации
if (
Widgets.ButtonText(
new Rect(rect.x, rect.y + curY, rect.width, 35f),
"AIImages.Generation.Cancel".Translate()
)
)
{
CancelGeneration();
}
}
else
{
// Показываем кнопку генерации
if (
Widgets.ButtonText(
new Rect(rect.x, rect.y + curY, rect.width, 35f),
"AIImages.Generation.Generate".Translate()
)
)
{
StartGeneration();
}
}
curY += 40f;
// Промпт секция
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(rect.x, rect.y + curY, rect.width, 30f),
"AIImages.Prompt.SectionTitle".Translate()
);
curY += 35f;
// Получаем промпт
Text.Font = GameFont.Tiny;
string promptText = promptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
);
// Фиксированная высота для области промпта
float promptBoxHeight = 150f;
float actualPromptHeight = Text.CalcHeight(promptText, rect.width - 20f);
Rect promptOuterRect = new Rect(rect.x, rect.y + curY, rect.width, promptBoxHeight);
Rect promptViewRect = new Rect(0f, 0f, rect.width - 20f, actualPromptHeight);
// Рисуем фон
Widgets.DrawBoxSolid(promptOuterRect, new Color(0.1f, 0.1f, 0.1f, 0.5f));
// Рисуем промпт с прокруткой
Widgets.BeginScrollView(
promptOuterRect.ContractedBy(5f),
ref promptScrollPosition,
promptViewRect
);
Widgets.Label(
new Rect(0f, 0f, promptViewRect.width, promptViewRect.height),
promptText
);
Widgets.EndScrollView();
curY += promptBoxHeight + 10f;
// Кнопка копирования промпта
if (
Widgets.ButtonText(
new Rect(rect.x, rect.y + curY, rect.width / 2f - 5f, 30f),
"AIImages.Prompt.CopyButton".Translate()
)
)
{
GUIUtility.systemCopyBuffer = promptText;
copiedMessageTime = 2f;
}
// Кнопка обновления данных
if (
Widgets.ButtonText(
new Rect(
rect.x + rect.width / 2f + 5f,
rect.y + curY,
rect.width / 2f - 5f,
30f
),
"AIImages.Window.Refresh".Translate()
)
)
{
RefreshPawnData();
}
// Сообщение о копировании
if (copiedMessageTime > 0f)
{
curY += 35f;
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
Widgets.Label(
new Rect(rect.x, rect.y + curY, rect.width, 25f),
"AIImages.Prompt.Copied".Translate()
);
GUI.color = Color.white;
}
}
private float CalculateContentHeight()
{
float height = 0f;
// Заголовок "Внешность"
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;
}
}
}