418 lines
15 KiB
C#
418 lines
15 KiB
C#
using System;
|
||
using System.Linq;
|
||
using AIImages.Models;
|
||
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 = "";
|
||
|
||
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;
|
||
|
||
/// <summary>
|
||
/// Обновляет данные персонажа
|
||
/// </summary>
|
||
private void RefreshPawnData()
|
||
{
|
||
appearanceData = AIImagesMod.PawnDescriptionService.ExtractAppearanceData(pawn);
|
||
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
|
||
}
|
||
|
||
/// <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 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,
|
||
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;
|
||
}
|
||
}
|
||
}
|