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, 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.width - imageSize) / 2f, curY, imageSize, imageSize ); GUI.DrawTexture(imageRect, generatedImage); curY += imageSize + 10f; } else { // Placeholder для изображения float placeholderSize = Mathf.Min(rect.width, 300f); Rect placeholderRect = new Rect( (rect.width - placeholderSize) / 2f, 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(0f, curY, rect.width, statusHeight), generationStatus); curY += statusHeight + 10f; } // Кнопка генерации Text.Font = GameFont.Small; if ( Widgets.ButtonText( new Rect(0f, 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(0f, 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(0f, 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(0f, curY, rect.width / 2f - 5f, 30f), "AIImages.Prompt.CopyButton".Translate() ) ) { GUIUtility.systemCopyBuffer = promptText; copiedMessageTime = 2f; } // Кнопка обновления данных if ( Widgets.ButtonText( new Rect(rect.width / 2f + 5f, 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(0f, 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; } } }