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 { /// /// Окно для просмотра персонажа и генерации 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 = ""; private CancellationTokenSource cancellationTokenSource; // Прогресс генерации private double generationProgress = 0.0; private int currentStep = 0; private int totalSteps = 0; private double etaSeconds = 0.0; // Сервисы (получаем через 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 = false; // Убираем дублирующую кнопку "Закрыть" 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 mainScrollPosition = Vector2.zero; private Vector2 promptScrollPosition = Vector2.zero; private Vector2 negativePromptScrollPosition = Vector2.zero; private float copiedMessageTime = 0f; // Состояние сворачиваемых секций промптов private bool showPositivePrompt = false; private bool showNegativePrompt = false; /// /// Обновляет данные персонажа /// private void RefreshPawnData() { DebugLogger.Log($"[AI Images] RefreshPawnData called for {pawn?.Name}"); appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn); generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings(); // Загружаем сохраненный портрет, если есть LoadSavedPortrait(); DebugLogger.Log($"[AI Images] RefreshPawnData completed for {pawn?.Name}"); } /// /// Загружает сохраненный портрет персонажа /// private void LoadSavedPortrait() { DebugLogger.Log($"[AI Images] LoadSavedPortrait called for {pawn?.Name}"); if (PawnPortraitHelper.HasPortrait(pawn)) { DebugLogger.Log($"[AI Images] Portrait found for {pawn?.Name}, loading..."); generatedImage = PawnPortraitHelper.LoadPortrait(pawn); if (generatedImage != null) { generationStatus = "AIImages.Generation.LoadedFromSave".Translate(); DebugLogger.Log($"[AI Images] Successfully loaded portrait for {pawn?.Name}"); DebugLogger.Log( $"[AI Images] generatedImage is now set: {generatedImage != null}, size: {generatedImage.width}x{generatedImage.height}" ); } else { DebugLogger.Warning( $"[AI Images] Failed to load portrait texture for {pawn?.Name}" ); } } else { DebugLogger.Log($"[AI Images] No saved portrait found for {pawn?.Name}"); } } /// /// Освобождает ресурсы при закрытии окна /// public override void PreClose() { base.PreClose(); // Отменяем генерацию, если она идет if (isGenerating && cancellationTokenSource != null) { cancellationTokenSource.Cancel(); } // Освобождаем CancellationTokenSource cancellationTokenSource?.Dispose(); } /// /// Обновляет текущую пешку в окне /// public void UpdatePawn(Pawn newPawn) { DebugLogger.Log( $"[AI Images] UpdatePawn called - switching from {pawn?.Name} to {newPawn?.Name}" ); this.pawn = newPawn; // Очищаем старое изображение при смене персонажа generatedImage = null; generationStatus = ""; generationProgress = 0.0; currentStep = 0; totalSteps = 0; etaSeconds = 0.0; DebugLogger.Log($"[AI Images] Cleared generated image for {newPawn?.Name}"); // Сбрасываем состояние сворачиваемых секций showPositivePrompt = false; showNegativePrompt = false; // Обновляем данные персонажа (включая загрузку портрета) RefreshPawnData(); // Принудительно обновляем окно this.windowRect = new Rect( this.windowRect.x, this.windowRect.y, this.InitialSize.x, this.InitialSize.y ); // Принудительно перерисовываем окно this.SetInitialSizeAndPosition(); DebugLogger.Log($"[AI Images] UpdatePawn completed for {newPawn?.Name}"); } /// /// Получить текущую пешку /// public Pawn CurrentPawn => pawn; /// /// Отладочный метод для проверки состояния всех пешек /// public static void DebugAllPawns() { DebugLogger.Log("[AI Images] === DEBUG: Checking all pawns ==="); if (Current.Game?.Maps == null) { DebugLogger.Log("[AI Images] No game or maps available"); return; } int totalPawns = 0; int pawnsWithComponents = 0; int pawnsWithPortraits = 0; foreach (var map in Current.Game.Maps) { foreach (var pawn in map.mapPawns.AllPawns) { if (pawn.RaceProps?.Humanlike == true) { totalPawns++; var (hasComponent, hasPortrait) = CheckPawnPortraitStatus(pawn); if (hasComponent) pawnsWithComponents++; if (hasPortrait) pawnsWithPortraits++; } } } LogDebugSummary(totalPawns, pawnsWithComponents, pawnsWithPortraits); } private static (bool hasComponent, bool hasPortrait) CheckPawnPortraitStatus(Pawn pawn) { var comp = PawnPortraitHelper.GetPortraitComp(pawn); if (comp != null) { if (comp.HasPortrait) { DebugLogger.Log( $"[AI Images] {pawn.Name}: Has component with portrait '{comp.PortraitPath}'" ); return (true, true); } else { DebugLogger.Log($"[AI Images] {pawn.Name}: Has component but no portrait"); return (true, false); } } else { DebugLogger.Warning($"[AI Images] {pawn.Name}: No portrait component found!"); return (false, false); } } private static void LogDebugSummary( int totalPawns, int pawnsWithComponents, int pawnsWithPortraits ) { DebugLogger.Log($"[AI Images] === DEBUG SUMMARY ==="); DebugLogger.Log($"[AI Images] Total humanlike pawns: {totalPawns}"); DebugLogger.Log($"[AI Images] Pawns with components: {pawnsWithComponents}"); DebugLogger.Log($"[AI Images] Pawns with portraits: {pawnsWithPortraits}"); DebugLogger.Log($"[AI Images] === END DEBUG ==="); } /// /// Вызывается каждый кадр для обновления окна /// 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 GenerateImageAsync() { if (isGenerating) return; // Создаем новый CancellationTokenSource cancellationTokenSource?.Dispose(); cancellationTokenSource = new CancellationTokenSource(); isGenerating = true; generationStatus = "AIImages.Generation.InProgress".Translate(); generationProgress = 0.0; currentStep = 0; totalSteps = generationSettings.Steps; 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, SaveImagesToServer = AIImagesMod.Settings.saveImagesToServer, }; // Создаем отдельный CancellationTokenSource для мониторинга прогресса var progressCts = new CancellationTokenSource(); var progressTask = MonitorProgressAsync(progressCts.Token); // Генерируем изображение с поддержкой отмены var result = await apiService.GenerateImageAsync( request, cancellationTokenSource.Token ); // Останавливаем мониторинг прогресса progressCts.Cancel(); try { await progressTask; } catch (OperationCanceledException) { // Ожидаемое исключение при остановке мониторинга } finally { progressCts.Dispose(); } if (result.Success) { DebugLogger.Log($"[AI Images] Generation successful for {pawn?.Name}"); DebugLogger.Log( $"[AI Images] Image data size: {result.ImageData?.Length ?? 0} bytes" ); DebugLogger.Log($"[AI Images] Saved path: {result.SavedPath}"); // Загружаем текстуру generatedImage = new Texture2D(2, 2); generatedImage.LoadImage(result.ImageData); generationStatus = "AIImages.Generation.Success".Translate(); generationProgress = 1.0; // Сохраняем путь к портрету на персонаже DebugLogger.Log( $"[AI Images] About to save portrait path for {pawn?.Name}: {result.SavedPath}" ); PawnPortraitHelper.SavePortraitPath(pawn, result.SavedPath); Messages.Message( "AIImages.Generation.SavedTo".Translate(result.SavedPath), MessageTypeDefOf.PositiveEvent ); DebugLogger.Log( $"[AI Images] Portrait generation and saving completed for {pawn?.Name}" ); } else { generationStatus = $"AIImages.Generation.Failed".Translate() + ": {result.ErrorMessage}"; Messages.Message(generationStatus, MessageTypeDefOf.RejectInput); } } catch (OperationCanceledException) { generationStatus = "AIImages.Generation.Cancelled".Translate(); DebugLogger.Log("[AI Images] Generation cancelled by user"); } catch (Exception ex) { generationStatus = $"Error: {ex.Message}"; DebugLogger.Error($"[AI Images] Generation error: {ex}"); Messages.Message( $"AIImages.Generation.Error".Translate() + ": {ex.Message}", MessageTypeDefOf.RejectInput ); } finally { isGenerating = false; } } /// /// Мониторит прогресс генерации и обновляет UI /// private async System.Threading.Tasks.Task MonitorProgressAsync( CancellationToken cancellationToken ) { try { while (!cancellationToken.IsCancellationRequested) { var progress = await apiService.GetProgressAsync(cancellationToken); if (progress != null && progress.IsActive) { generationProgress = progress.Progress; currentStep = progress.CurrentStep; totalSteps = progress.TotalSteps; etaSeconds = progress.EtaRelative; DebugLogger.Log( $"[AI Images] Progress: {progress.Progress:P} - Step {progress.CurrentStep}/{progress.TotalSteps} - ETA: {progress.EtaRelative:F1}s" ); } // Обновляем каждые 500ms await System.Threading.Tasks.Task.Delay(500, cancellationToken); } } catch (OperationCanceledException) { // Ожидаемое исключение при остановке } catch (Exception ex) { DebugLogger.Warning($"[AI Images] Progress monitoring error: {ex.Message}"); } } /// /// Запускает генерацию изображения (обертка для безопасного fire-and-forget) /// private void StartGeneration() { AsyncHelper.FireAndForget(GenerateImageAsync(), "Image Generation"); } /// /// Отменяет генерацию изображения /// private void CancelGeneration() { if (isGenerating && cancellationTokenSource != null) { cancellationTokenSource.Cancel(); generationStatus = "AIImages.Generation.Cancelling".Translate(); } } public override void DoWindowContents(Rect inRect) { // Рассчитываем общую высоту контента float totalContentHeight = CalculateTotalContentHeight(); Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, totalContentHeight); Widgets.BeginScrollView(inRect, ref mainScrollPosition, viewRect); float curY = 0f; // Заголовок Text.Font = GameFont.Medium; Widgets.Label( new Rect(0f, curY, viewRect.width, 40f), "AIImages.Window.Title".Translate() ); curY += 45f; // Имя пешки Text.Font = GameFont.Small; Widgets.Label( new Rect(0f, curY, viewRect.width, 30f), "AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve()) ); curY += 40f; // Разделитель Widgets.DrawLineHorizontal(0f, curY, viewRect.width); curY += 10f; // Разделяем на две колонки: левая - информация, правая - изображение float leftColumnWidth = viewRect.width * 0.35f; float rightColumnWidth = viewRect.width * 0.62f; float columnGap = viewRect.width * 0.03f; // Определяем высоту колонок (берем большую из двух) float columnHeight = Mathf.Max(CalculateContentHeight(), CalculateRightColumnHeight()); // Левая колонка - информация Rect leftColumnRect = new Rect(0f, curY, leftColumnWidth, columnHeight); DrawLeftColumn(leftColumnRect); // Правая колонка - превью и управление Rect rightColumnRect = new Rect( leftColumnWidth + columnGap, curY, rightColumnWidth, columnHeight ); DrawRightColumn(rightColumnRect); Widgets.EndScrollView(); } private void DrawLeftColumn(Rect rect) { float contentY = rect.y; // Портрет персонажа (если есть) if (generatedImage != null) { float portraitSize = Mathf.Min(rect.width, 250f); Rect portraitRect = new Rect( rect.x + (rect.width - portraitSize) / 2f, contentY, portraitSize, portraitSize ); // Рамка вокруг портрета Widgets.DrawBox(portraitRect); GUI.DrawTexture(portraitRect.ContractedBy(2f), generatedImage); contentY += portraitSize + 15f; } // Заголовок секции Text.Font = GameFont.Medium; Widgets.Label( new Rect(rect.x, contentY, rect.width, 30f), "AIImages.CharacterInfo.Title".Translate() ); contentY += 35f; // Разделитель Widgets.DrawLineHorizontal(rect.x, contentY, rect.width); contentY += 10f; // Информация о персонаже DrawCharacterInfoContent(rect, contentY); } /// /// Отрисовывает информацию о персонаже в компактном виде /// private void DrawCharacterInfoContent(Rect parentRect, float startY) { float contentY = startY; float lineHeight = 22f; float labelWidth = parentRect.width * 0.45f; float valueWidth = parentRect.width * 0.50f; Text.Font = GameFont.Tiny; // Базовая информация var info = new[] { ("AIImages.Info.Gender".Translate(), appearanceData.Gender.ToString()), ("AIImages.Info.Age".Translate(), appearanceData.Age.ToString()), ("AIImages.Info.BodyType".Translate(), appearanceData.BodyType), ( "AIImages.Info.SkinTone".Translate(), ColorDescriptionService.GetSkinToneDescription(appearanceData.SkinColor) ), ("AIImages.Info.Hair".Translate(), appearanceData.HairStyle), ( "AIImages.Info.HairColor".Translate(), ColorDescriptionService.GetHairColorDescription(appearanceData.HairColor) ), }; foreach (var (label, value) in info) { // Подсветка строк через одну var infoArray = info.ToArray(); int index = Array.IndexOf(infoArray, (label, value)); if ((index % 2) == 0) { Widgets.DrawBoxSolid( new Rect(parentRect.x, contentY, parentRect.width, lineHeight), new Color(0.15f, 0.15f, 0.15f, 0.3f) ); } Widgets.Label( new Rect(parentRect.x + 5f, contentY, labelWidth, lineHeight), label + ":" ); Widgets.Label( new Rect(parentRect.x + labelWidth + 10f, contentY, valueWidth, lineHeight), value ); contentY += lineHeight; } // Черты характера if (pawn.story?.traits?.allTraits != null && pawn.story.traits.allTraits.Any()) { contentY += 15f; Text.Font = GameFont.Small; Widgets.Label( new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight), "AIImages.Info.Traits".Translate() + ":" ); contentY += lineHeight + 2f; Text.Font = GameFont.Tiny; var traitLabels = pawn.story.traits.allTraits.Select(trait => trait.LabelCap); foreach (var traitLabel in traitLabels) { Widgets.Label( new Rect(parentRect.x + 15f, contentY, parentRect.width - 20f, lineHeight), "• " + traitLabel ); contentY += lineHeight; } } // Гены if (pawn.genes?.GenesListForReading != null && pawn.genes.GenesListForReading.Any()) { contentY += 15f; Text.Font = GameFont.Small; Widgets.Label( new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight), "AIImages.Info.Genes".Translate() + ":" ); contentY += lineHeight + 2f; Text.Font = GameFont.Tiny; var geneLabels = pawn .genes.GenesListForReading.Where(gene => gene.Active) .Select(gene => gene.def.LabelCap); foreach (var geneLabel in geneLabels) { Widgets.Label( new Rect(parentRect.x + 15f, contentY, parentRect.width - 20f, lineHeight), "• " + geneLabel ); contentY += lineHeight; } } // Хедифы (состояния) if (pawn.health?.hediffSet?.hediffs != null && pawn.health.hediffSet.hediffs.Any()) { contentY += 15f; Text.Font = GameFont.Small; Widgets.Label( new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight), "AIImages.Info.Hediffs".Translate() + ":" ); contentY += lineHeight + 2f; Text.Font = GameFont.Tiny; var hediffLabels = pawn .health.hediffSet.hediffs.Where(hediff => hediff.Visible) .Select(hediff => hediff.LabelCap); foreach (var hediffLabel in hediffLabels) { Widgets.Label( new Rect(parentRect.x + 15f, contentY, parentRect.width - 20f, lineHeight), "• " + hediffLabel ); contentY += lineHeight; } } // Одежда var apparel = pawn.apparel?.WornApparel; if (apparel != null && apparel.Any()) { contentY += 15f; Text.Font = GameFont.Small; Widgets.Label( new Rect(parentRect.x + 5f, contentY, parentRect.width - 10f, lineHeight), "AIImages.Info.Apparel".Translate() + ":" ); contentY += lineHeight + 2f; Text.Font = GameFont.Tiny; foreach (var item in apparel) { var colorDesc = ColorDescriptionService.GetApparelColorDescription( item.DrawColor ); string apparelLabel = $"• {colorDesc} {item.def.label}"; float apparelHeight = Text.CalcHeight(apparelLabel, parentRect.width - 25f); Widgets.Label( new Rect( parentRect.x + 15f, contentY, parentRect.width - 25f, apparelHeight ), apparelLabel ); contentY += apparelHeight; } } } private void DrawRightColumn(Rect rect) { float curY = rect.y; curY = DrawImagePreview(rect, curY); curY = DrawGenerationStatus(rect, curY); curY = DrawProgressBar(rect, curY); curY = DrawGenerationButton(rect, curY); // Сворачиваемая секция с позитивным промптом curY = DrawPromptSection( rect, curY, "AIImages.Prompt.PositiveTitle".Translate(), ref showPositivePrompt, () => promptGeneratorService.GeneratePositivePrompt( appearanceData, generationSettings ), new Color(0.1f, 0.3f, 0.1f, 0.5f), ref promptScrollPosition ); curY += 5f; // Сворачиваемая секция с негативным промптом curY = DrawPromptSection( rect, curY, "AIImages.Prompt.NegativeTitle".Translate(), ref showNegativePrompt, () => promptGeneratorService.GenerateNegativePrompt(generationSettings), new Color(0.3f, 0.1f, 0.1f, 0.5f), ref negativePromptScrollPosition ); curY += 10f; // Кнопка обновления данных if ( Widgets.ButtonText( new Rect(rect.x, curY, rect.width, 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, curY, rect.width, 25f), "AIImages.Prompt.Copied".Translate() ); GUI.color = Color.white; } } private float CalculateContentHeight() { float height = 0f; // Портрет персонажа (если есть) if (generatedImage != null) { float portraitSize = 250f; // Максимальный размер портрета height += portraitSize + 15f; } // Заголовок "Информация о персонаже" height += 35f; // Разделитель height += 10f; // Базовая информация (6 строк по 22px) height += 6 * 22f; // Черты характера (если есть) if (pawn.story?.traits?.allTraits != null && pawn.story.traits.allTraits.Any()) { height += 15f; // Отступ height += 22f; // Заголовок "Черты характера" height += 2f; // Отступ height += pawn.story.traits.allTraits.Count * 22f; // Каждая черта } // Гены (если есть) if (pawn.genes?.GenesListForReading != null && pawn.genes.GenesListForReading.Any()) { height += 15f; // Отступ height += 22f; // Заголовок "Гены" height += 2f; // Отступ height += pawn.genes.GenesListForReading.Count(gene => gene.Active) * 22f; // Каждый активный ген } // Хедифы (если есть) if (pawn.health?.hediffSet?.hediffs != null && pawn.health.hediffSet.hediffs.Any()) { height += 15f; // Отступ height += 22f; // Заголовок "Хедифы" height += 2f; // Отступ height += pawn.health.hediffSet.hediffs.Count(hediff => hediff.Visible) * 22f; // Каждый видимый хедиф } // Одежда (если есть) var apparel = pawn.apparel?.WornApparel; if (apparel != null && apparel.Any()) { height += 15f; // Отступ height += 22f; // Заголовок "Одежда" height += 2f; // Отступ // Примерно по 22-30px на предмет одежды height += apparel.Count * 26f; } // Дополнительный отступ height += 50f; return height; } private float CalculateRightColumnHeight() { float height = 0f; // Превью изображения if (generatedImage != null) { height += 410f; // 400f изображение + 10f отступ } else if (!isGenerating) { height += 310f; // 300f placeholder + 10f отступ } // Статус генерации if (!string.IsNullOrEmpty(generationStatus)) { height += 30f; } // Прогресс бар if (isGenerating && generationProgress > 0.0) { height += 30f; } // Кнопка генерации height += 40f; // Позитивный промпт height += 36f; // Заголовок (32f + 4f отступ) if (showPositivePrompt) { height += 140f; // Бокс развернут (120f + 10f отступ + запас) } height += 5f; // Отступ между промптами // Негативный промпт height += 36f; // Заголовок (32f + 4f отступ) if (showNegativePrompt) { height += 140f; // Бокс развернут (120f + 10f отступ + запас) } height += 10f; // Отступ после промптов // Кнопка обновления height += 40f; // Увеличено с 35f до 40f // Сообщение о копировании if (copiedMessageTime > 0f) { height += 40f; } return height + 100f; // Увеличен дополнительный отступ снизу } private float CalculateTotalContentHeight() { float height = 0f; // Заголовок и имя пешки height += 45f + 40f; // Разделитель height += 10f; // Высота колонок (берем большую) float columnHeight = Mathf.Max(CalculateContentHeight(), CalculateRightColumnHeight()); height += columnHeight; // Дополнительный отступ снизу height += 30f; return height; } /// /// /// Отрисовывает секцию с промптом /// private float DrawPromptSection( Rect parentRect, float startY, string title, ref bool expanded, System.Func getPromptFunc, Color backgroundColor, ref Vector2 scrollPosition ) { float curY = startY; float headerHeight = 32f; // Получаем промпт string prompt = getPromptFunc(); // Рисуем заголовок с фоном Rect headerRect = new Rect(parentRect.x, curY, parentRect.width, headerHeight); Widgets.DrawBoxSolid(headerRect, new Color(0.25f, 0.25f, 0.25f, 0.8f)); Widgets.DrawBox(headerRect); // Иконка раскрытия string icon = expanded ? "▼" : "►"; Text.Font = GameFont.Small; Widgets.Label(new Rect(parentRect.x + 8f, curY + 6f, 20f, headerHeight), icon); // Заголовок Widgets.Label( new Rect(parentRect.x + 30f, curY + 6f, parentRect.width - 110f, headerHeight), title ); // Кнопка копирования в заголовке (увеличена ширина) Rect copyButtonRect = new Rect( parentRect.x + parentRect.width - 100f, curY + 4f, 95f, 24f ); if (Widgets.ButtonText(copyButtonRect, "📋 " + "AIImages.Copy".Translate())) { GUIUtility.systemCopyBuffer = prompt; copiedMessageTime = 2f; } // Клик на остальной области для раскрытия/сворачивания Rect clickableHeaderRect = new Rect( parentRect.x, curY, parentRect.width - 105f, headerHeight ); if (Widgets.ButtonInvisible(clickableHeaderRect)) { expanded = !expanded; } curY += headerHeight + 4f; // Рисуем содержимое если развернуто if (expanded) { float promptBoxHeight = 120f; Text.Font = GameFont.Tiny; float actualPromptHeight = Text.CalcHeight(prompt, parentRect.width - 20f); Rect promptOuterRect = new Rect( parentRect.x, curY, parentRect.width, promptBoxHeight ); Rect promptViewRect = new Rect(0f, 0f, parentRect.width - 20f, actualPromptHeight); // Рисуем фон Widgets.DrawBoxSolid(promptOuterRect, backgroundColor); Widgets.DrawBox(promptOuterRect); // Рисуем промпт с прокруткой Widgets.BeginScrollView( promptOuterRect.ContractedBy(5f), ref scrollPosition, promptViewRect ); var prevAnchor = Text.Anchor; Text.Anchor = TextAnchor.UpperLeft; Text.WordWrap = true; Widgets.Label( new Rect(0f, 0f, promptViewRect.width, promptViewRect.height), prompt ); Text.Anchor = prevAnchor; Text.WordWrap = true; Widgets.EndScrollView(); curY += promptBoxHeight + 10f; } return curY; } private float DrawImagePreview(Rect rect, float curY) { if (generatedImage != null) { float imageSize = Mathf.Min(rect.width, 400f); Rect imageRect = new Rect( rect.x + (rect.width - imageSize) / 2f, curY, imageSize, imageSize ); GUI.DrawTexture(imageRect, generatedImage); return curY + imageSize + 10f; } float placeholderSize = Mathf.Min(rect.width, 300f); Rect placeholderRect = new Rect( rect.x + (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; return curY + placeholderSize + 10f; } private float DrawGenerationStatus(Rect rect, float curY) { if (string.IsNullOrEmpty(generationStatus)) return curY; Text.Font = GameFont.Small; float statusHeight = Text.CalcHeight(generationStatus, rect.width); Widgets.Label(new Rect(rect.x, curY, rect.width, statusHeight), generationStatus); return curY + statusHeight + 10f; } private float DrawProgressBar(Rect rect, float curY) { if (!isGenerating || generationProgress <= 0.0) return curY; Rect progressBarRect = new Rect(rect.x, curY, rect.width, 24f); string progressText; if (totalSteps > 0) { progressText = $"{(generationProgress * 100):F1}% - Step {currentStep}/{totalSteps}"; if (etaSeconds > 0) { progressText += $" - ETA: {etaSeconds:F0}s"; } } else { progressText = $"{(generationProgress * 100):F1}%"; } Widgets.FillableBar(progressBarRect, (float)generationProgress); Text.Font = GameFont.Tiny; Text.Anchor = TextAnchor.MiddleCenter; Widgets.Label(progressBarRect, progressText); Text.Anchor = TextAnchor.UpperLeft; return curY + 30f; } private float DrawGenerationButton(Rect rect, float curY) { Text.Font = GameFont.Small; string buttonLabel = isGenerating ? "AIImages.Generation.Cancel".Translate() : "AIImages.Generation.Generate".Translate(); // Основная кнопка генерации (занимает 70% ширины) float buttonWidth = rect.width * 0.7f; if (Widgets.ButtonText(new Rect(rect.x, curY, buttonWidth, 35f), buttonLabel)) { if (isGenerating) CancelGeneration(); else StartGeneration(); } // Отладочная кнопка (занимает 25% ширины) float debugButtonWidth = rect.width * 0.25f; float debugButtonX = rect.x + buttonWidth + 10f; if (Widgets.ButtonText(new Rect(debugButtonX, curY, debugButtonWidth, 35f), "Debug")) { DebugAllPawns(); } return curY + 40f; } } }