515 lines
18 KiB
C#
515 lines
18 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;
|
||
|
||
// Сервисы (получаем через 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;
|
||
}
|
||
}
|
||
}
|