using System;
using System.Linq;
using AIImages.Models;
using RimWorld;
using UnityEngine;
using Verse;
#pragma warning disable IDE1006 // Naming Styles
namespace AIImages
{
///
/// Окно для просмотра персонажа и генерации AI изображений
///
[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 = "";
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;
// Извлекаем данные персонажа
RefreshPawnData();
}
public override Vector2 InitialSize => new Vector2(900f, 800f);
private Vector2 scrollPosition = Vector2.zero;
private float copiedMessageTime = 0f;
///
/// Обновляет данные персонажа
///
private void RefreshPawnData()
{
appearanceData = AIImagesMod.PawnDescriptionService.ExtractAppearanceData(pawn);
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
}
///
/// Обновляет текущую пешку в окне
///
public void UpdatePawn(Pawn newPawn)
{
this.pawn = newPawn;
RefreshPawnData();
}
///
/// Получить текущую пешку
///
public Pawn CurrentPawn => pawn;
///
/// Вызывается каждый кадр для обновления окна
///
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;
}
}
///
/// Асинхронная генерация изображения
///
private async System.Threading.Tasks.Task GenerateImage()
{
if (isGenerating)
return;
isGenerating = true;
generationStatus = "AIImages.Generation.InProgress".Translate();
try
{
// Генерируем промпты
string positivePrompt = AIImagesMod.PromptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
);
string negativePrompt = AIImagesMod.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 AIImagesMod.ApiService.GenerateImageAsync(request);
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 (Exception ex)
{
generationStatus = $"Error: {ex.Message}";
Log.Error($"[AI Images] Generation error: {ex}");
}
finally
{
isGenerating = false;
}
}
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 = AIImagesMod.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 = AIImagesMod.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 (
Widgets.ButtonText(
new Rect(rect.x, rect.y + curY, rect.width, 35f),
isGenerating
? "AIImages.Generation.Generating".Translate()
: "AIImages.Generation.Generate".Translate()
) && !isGenerating
)
{
_ = GenerateImage();
}
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 = AIImagesMod.PromptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
);
float promptHeight = Mathf.Min(Text.CalcHeight(promptText, rect.width), 150f);
Rect promptRect = new Rect(rect.x, rect.y + curY, rect.width, promptHeight);
// Рисуем промпт в скроллируемой области если он длинный
Widgets.DrawBoxSolid(promptRect, new Color(0.1f, 0.1f, 0.1f, 0.5f));
Widgets.Label(promptRect.ContractedBy(5f), promptText);
curY += promptHeight + 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 = AIImagesMod.PawnDescriptionService.GetAppearanceDescription(
pawn
);
height += Text.CalcHeight(appearanceText, 400f) + 20f;
// Разделитель
height += 15f;
// Заголовок "Одежда"
height += 35f;
// Текст одежды
string apparelText = AIImagesMod.PawnDescriptionService.GetApparelDescription(pawn);
height += Text.CalcHeight(apparelText, 400f) + 20f;
// Дополнительный отступ
height += 50f;
return height;
}
}
}