diff --git a/Assemblies/AIImages.dll b/Assemblies/AIImages.dll
index 9745481..cf9e1cb 100644
Binary files a/Assemblies/AIImages.dll and b/Assemblies/AIImages.dll differ
diff --git a/Languages/English/Keyed/AIImages.xml b/Languages/English/Keyed/AIImages.xml
index f19385a..a5af5b9 100644
--- a/Languages/English/Keyed/AIImages.xml
+++ b/Languages/English/Keyed/AIImages.xml
@@ -42,6 +42,7 @@
Cancelling generation...
Generation error
Image saved to: {0}
+ Loaded saved portrait
No image generated yet.\nClick "Generate Image" to start.
API Settings
diff --git a/Languages/Russian/Keyed/AIImages.xml b/Languages/Russian/Keyed/AIImages.xml
index 3a753e5..c1f9852 100644
--- a/Languages/Russian/Keyed/AIImages.xml
+++ b/Languages/Russian/Keyed/AIImages.xml
@@ -42,6 +42,7 @@
Отмена генерации...
Ошибка генерации
Изображение сохранено в: {0}
+ Загружен сохраненный портрет
Изображение еще не сгенерировано.\nНажмите "Сгенерировать изображение" для начала.
Настройки API
diff --git a/Source/AIImages/Components/PawnPortraitComp.cs b/Source/AIImages/Components/PawnPortraitComp.cs
new file mode 100644
index 0000000..f159a8a
--- /dev/null
+++ b/Source/AIImages/Components/PawnPortraitComp.cs
@@ -0,0 +1,31 @@
+using Verse;
+
+namespace AIImages.Components
+{
+ ///
+ /// Компонент для хранения данных AI-сгенерированного портрета пешки
+ ///
+ public class PawnPortraitComp : ThingComp
+ {
+ ///
+ /// Путь к сохраненному портрету
+ ///
+ public string PortraitPath { get; set; }
+
+ ///
+ /// Есть ли сохраненный портрет
+ ///
+ public bool HasPortrait => !string.IsNullOrEmpty(PortraitPath);
+
+ ///
+ /// Сохранение/загрузка данных
+ ///
+ public override void PostExposeData()
+ {
+ base.PostExposeData();
+ string portraitPath = PortraitPath;
+ Scribe_Values.Look(ref portraitPath, "aiPortraitPath", null);
+ PortraitPath = portraitPath;
+ }
+ }
+}
diff --git a/Source/AIImages/Helpers/PawnPortraitHelper.cs b/Source/AIImages/Helpers/PawnPortraitHelper.cs
new file mode 100644
index 0000000..39b3240
--- /dev/null
+++ b/Source/AIImages/Helpers/PawnPortraitHelper.cs
@@ -0,0 +1,90 @@
+using System.IO;
+using AIImages.Components;
+using UnityEngine;
+using Verse;
+
+namespace AIImages.Helpers
+{
+ ///
+ /// Вспомогательный класс для работы с портретами персонажей
+ ///
+ public static class PawnPortraitHelper
+ {
+ ///
+ /// Получить компонент портрета пешки
+ ///
+ public static PawnPortraitComp GetPortraitComp(Pawn pawn)
+ {
+ return pawn?.TryGetComp();
+ }
+
+ ///
+ /// Сохранить путь к портрету на пешке
+ ///
+ public static void SavePortraitPath(Pawn pawn, string path)
+ {
+ var comp = GetPortraitComp(pawn);
+ if (comp != null)
+ {
+ comp.PortraitPath = path;
+ Log.Message($"[AI Images] Saved portrait path for {pawn.Name}: {path}");
+ }
+ }
+
+ ///
+ /// Получить путь к портрету пешки
+ ///
+ public static string GetPortraitPath(Pawn pawn)
+ {
+ var comp = GetPortraitComp(pawn);
+ return comp?.PortraitPath;
+ }
+
+ ///
+ /// Есть ли у пешки сохраненный портрет
+ ///
+ public static bool HasPortrait(Pawn pawn)
+ {
+ var comp = GetPortraitComp(pawn);
+ return comp != null && comp.HasPortrait;
+ }
+
+ ///
+ /// Загрузить портрет пешки как текстуру
+ ///
+ public static Texture2D LoadPortrait(Pawn pawn)
+ {
+ string path = GetPortraitPath(pawn);
+
+ if (string.IsNullOrEmpty(path) || !File.Exists(path))
+ {
+ return null;
+ }
+
+ try
+ {
+ byte[] imageData = File.ReadAllBytes(path);
+ Texture2D texture = new Texture2D(2, 2);
+ texture.LoadImage(imageData);
+ return texture;
+ }
+ catch (System.Exception ex)
+ {
+ Log.Warning($"[AI Images] Failed to load portrait for {pawn.Name}: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Очистить портрет пешки
+ ///
+ public static void ClearPortrait(Pawn pawn)
+ {
+ var comp = GetPortraitComp(pawn);
+ if (comp != null)
+ {
+ comp.PortraitPath = null;
+ }
+ }
+ }
+}
diff --git a/Source/AIImages/Models/GenerationRequest.cs b/Source/AIImages/Models/GenerationRequest.cs
index 057949d..eec40e2 100644
--- a/Source/AIImages/Models/GenerationRequest.cs
+++ b/Source/AIImages/Models/GenerationRequest.cs
@@ -48,4 +48,35 @@ namespace AIImages.Models
};
}
}
+
+ ///
+ /// Прогресс генерации изображения
+ ///
+ public class GenerationProgress
+ {
+ ///
+ /// Процент завершения (0.0 - 1.0)
+ ///
+ public double Progress { get; set; }
+
+ ///
+ /// Текущий шаг
+ ///
+ public int CurrentStep { get; set; }
+
+ ///
+ /// Общее количество шагов
+ ///
+ public int TotalSteps { get; set; }
+
+ ///
+ /// Оставшееся время в секундах (приблизительно)
+ ///
+ public double EtaRelative { get; set; }
+
+ ///
+ /// Идет ли генерация в данный момент
+ ///
+ public bool IsActive { get; set; }
+ }
}
diff --git a/Source/AIImages/Patches/PawnPortraitCompPatch.cs b/Source/AIImages/Patches/PawnPortraitCompPatch.cs
new file mode 100644
index 0000000..261faf3
--- /dev/null
+++ b/Source/AIImages/Patches/PawnPortraitCompPatch.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+using System.Reflection;
+using AIImages.Components;
+using HarmonyLib;
+using Verse;
+
+namespace AIImages.Patches
+{
+ ///
+ /// Патч для добавления PawnPortraitComp ко всем пешкам
+ ///
+ [HarmonyPatch(typeof(ThingWithComps), nameof(ThingWithComps.InitializeComps))]
+ public static class PawnPortraitCompPatch
+ {
+ private static FieldInfo allCompsField = AccessTools.Field(typeof(ThingWithComps), "comps");
+
+ [HarmonyPostfix]
+ public static void AddPortraitComp(ThingWithComps __instance)
+ {
+ // Проверяем, является ли объект пешкой-гуманоидом и нет ли уже компонента
+ if (
+ __instance is Pawn pawn
+ && pawn.RaceProps?.Humanlike == true
+ && pawn.GetComp() == null
+ )
+ {
+ // Создаем компонент
+ var comp = new PawnPortraitComp { parent = pawn };
+
+ // Инициализируем компонент
+ comp.Initialize(null);
+
+ // Получаем список компонентов через рефлексию и добавляем наш
+ var compsList = allCompsField.GetValue(pawn) as List;
+ if (compsList != null)
+ {
+ compsList.Add(comp);
+ }
+ }
+ }
+ }
+}
diff --git a/Source/AIImages/Services/AdvancedPromptGenerator.cs b/Source/AIImages/Services/AdvancedPromptGenerator.cs
index b927114..8e3d90a 100644
--- a/Source/AIImages/Services/AdvancedPromptGenerator.cs
+++ b/Source/AIImages/Services/AdvancedPromptGenerator.cs
@@ -63,7 +63,14 @@ namespace AIImages.Services
StringBuilder prompt = new StringBuilder();
- // 1. Художественный стиль
+ // 1. Базовый пользовательский промпт (если указан) - идет первым
+ if (!string.IsNullOrEmpty(settings.PositivePrompt))
+ {
+ prompt.Append(settings.PositivePrompt);
+ prompt.Append(", ");
+ }
+
+ // 2. Художественный стиль
if (
ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt)
&& !string.IsNullOrEmpty(stylePrompt)
@@ -73,14 +80,14 @@ namespace AIImages.Services
prompt.Append(", ");
}
- // 2. Тип кадра - автоматически добавляем "portrait" для генерации персонажей
+ // 3. Тип кадра - автоматически добавляем "portrait" для генерации персонажей
prompt.Append("portrait, head and shoulders of ");
- // 3. Базовое описание (возраст и пол)
+ // 4. Базовое описание (возраст и пол)
prompt.Append(GetAgeAndGenderDescription(appearanceData));
prompt.Append(", ");
- // 4. Тип тела
+ // 5. Тип тела
string bodyType = GetBodyTypeDescription(appearanceData.BodyType);
if (!string.IsNullOrEmpty(bodyType))
{
@@ -88,14 +95,14 @@ namespace AIImages.Services
prompt.Append(", ");
}
- // 5. Цвет кожи
+ // 6. Цвет кожи
string skinTone = ColorDescriptionService.GetSkinToneDescription(
appearanceData.SkinColor
);
prompt.Append(skinTone);
prompt.Append(", ");
- // 6. Волосы
+ // 7. Волосы
string hairDescription = GetHairDescription(appearanceData);
if (!string.IsNullOrEmpty(hairDescription))
{
@@ -103,7 +110,7 @@ namespace AIImages.Services
prompt.Append(", ");
}
- // 7. Настроение и выражение на основе черт характера
+ // 8. Настроение и выражение на основе черт характера
string moodDescription = GetMoodFromTraits(appearanceData.Traits);
if (!string.IsNullOrEmpty(moodDescription))
{
@@ -111,7 +118,7 @@ namespace AIImages.Services
prompt.Append(", ");
}
- // 8. Одежда
+ // 9. Одежда
string apparelDescription = GetApparelDescription(appearanceData.Apparel);
if (!string.IsNullOrEmpty(apparelDescription))
{
@@ -119,13 +126,6 @@ namespace AIImages.Services
prompt.Append(", ");
}
- // 9. Базовый пользовательский промпт (если указан)
- if (!string.IsNullOrEmpty(settings.PositivePrompt))
- {
- prompt.Append(settings.PositivePrompt);
- prompt.Append(", ");
- }
-
// 10. Качественные теги
prompt.Append(GetQualityTags(settings.ArtStyle));
@@ -136,7 +136,14 @@ namespace AIImages.Services
{
StringBuilder negativePrompt = new StringBuilder();
- // Базовые негативные промпты
+ // 1. Пользовательский негативный промпт (если указан) - идет первым
+ if (!string.IsNullOrEmpty(settings.NegativePrompt))
+ {
+ negativePrompt.Append(settings.NegativePrompt);
+ negativePrompt.Append(", ");
+ }
+
+ // 2. Базовые негативные промпты
negativePrompt.Append(
"ugly, deformed, low quality, blurry, bad anatomy, worst quality, "
);
@@ -144,7 +151,7 @@ namespace AIImages.Services
"mutated, disfigured, bad proportions, extra limbs, missing limbs, "
);
- // Специфичные для стиля негативы
+ // 3. Специфичные для стиля негативы
switch (settings.ArtStyle)
{
case ArtStyle.Realistic:
@@ -159,12 +166,6 @@ namespace AIImages.Services
break;
}
- // Пользовательский негативный промпт
- if (!string.IsNullOrEmpty(settings.NegativePrompt))
- {
- negativePrompt.Append(settings.NegativePrompt);
- }
-
return negativePrompt.ToString().Trim().TrimEnd(',');
}
diff --git a/Source/AIImages/Services/IStableDiffusionApiService.cs b/Source/AIImages/Services/IStableDiffusionApiService.cs
index e47ef7f..6d77d3c 100644
--- a/Source/AIImages/Services/IStableDiffusionApiService.cs
+++ b/Source/AIImages/Services/IStableDiffusionApiService.cs
@@ -18,6 +18,11 @@ namespace AIImages.Services
CancellationToken cancellationToken = default
);
+ ///
+ /// Получает прогресс текущей генерации
+ ///
+ Task GetProgressAsync(CancellationToken cancellationToken = default);
+
///
/// Проверяет доступность API
///
diff --git a/Source/AIImages/Services/StableDiffusionNetAdapter.cs b/Source/AIImages/Services/StableDiffusionNetAdapter.cs
index 3a89d04..cd8fc8c 100644
--- a/Source/AIImages/Services/StableDiffusionNetAdapter.cs
+++ b/Source/AIImages/Services/StableDiffusionNetAdapter.cs
@@ -137,6 +137,42 @@ namespace AIImages.Services
}
}
+ public async Task GetProgressAsync(
+ CancellationToken cancellationToken = default
+ )
+ {
+ ThrowIfDisposed();
+
+ try
+ {
+ // Используем Progress сервис библиотеки
+ var progress = await _client.Progress.GetProgressAsync(cancellationToken);
+
+ // Маппируем на наш тип
+ return new GenerationProgress
+ {
+ Progress = progress.Progress,
+ CurrentStep = progress.State?.SamplingStep ?? 0,
+ TotalSteps = progress.State?.SamplingSteps ?? 0,
+ EtaRelative = progress.EtaRelative,
+ IsActive = progress.Progress > 0 && progress.Progress < 1.0,
+ };
+ }
+ catch (Exception ex)
+ {
+ Log.Warning($"[AI Images] Failed to get progress: {ex.Message}");
+ // Возвращаем пустой прогресс при ошибке
+ return new GenerationProgress
+ {
+ Progress = 0,
+ CurrentStep = 0,
+ TotalSteps = 0,
+ EtaRelative = 0,
+ IsActive = false,
+ };
+ }
+ }
+
public async Task CheckApiAvailability(
string apiEndpoint,
CancellationToken cancellationToken = default
diff --git a/Source/AIImages/Window_AIImage.cs b/Source/AIImages/Window_AIImage.cs
index 68093a3..2aaee78 100644
--- a/Source/AIImages/Window_AIImage.cs
+++ b/Source/AIImages/Window_AIImage.cs
@@ -36,6 +36,12 @@ namespace AIImages
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;
@@ -74,6 +80,24 @@ namespace AIImages
{
appearanceData = pawnDescriptionService.ExtractAppearanceData(pawn);
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
+
+ // Загружаем сохраненный портрет, если есть
+ LoadSavedPortrait();
+ }
+
+ ///
+ /// Загружает сохраненный портрет персонажа
+ ///
+ private void LoadSavedPortrait()
+ {
+ if (PawnPortraitHelper.HasPortrait(pawn))
+ {
+ generatedImage = PawnPortraitHelper.LoadPortrait(pawn);
+ if (generatedImage != null)
+ {
+ generationStatus = "AIImages.Generation.LoadedFromSave".Translate();
+ }
+ }
}
///
@@ -100,6 +124,11 @@ namespace AIImages
{
this.pawn = newPawn;
RefreshPawnData();
+
+ // Очищаем старое изображение при смене персонажа
+ generatedImage = null;
+ generationStatus = "";
+ generationProgress = 0.0;
}
///
@@ -146,6 +175,9 @@ namespace AIImages
isGenerating = true;
generationStatus = "AIImages.Generation.InProgress".Translate();
+ generationProgress = 0.0;
+ currentStep = 0;
+ totalSteps = generationSettings.Steps;
try
{
@@ -173,18 +205,41 @@ namespace AIImages
Model = AIImagesMod.Settings.apiEndpoint,
};
+ // Создаем отдельный 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)
{
// Загружаем текстуру
generatedImage = new Texture2D(2, 2);
generatedImage.LoadImage(result.ImageData);
generationStatus = "AIImages.Generation.Success".Translate();
+ generationProgress = 1.0;
+
+ // Сохраняем путь к портрету на персонаже
+ PawnPortraitHelper.SavePortraitPath(pawn, result.SavedPath);
Messages.Message(
"AIImages.Generation.SavedTo".Translate(result.SavedPath),
@@ -218,6 +273,45 @@ namespace AIImages
}
}
+ ///
+ /// Мониторит прогресс генерации и обновляет 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;
+
+ Log.Message(
+ $"[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)
+ {
+ Log.Warning($"[AI Images] Progress monitoring error: {ex.Message}");
+ }
+ }
+
///
/// Запускает генерацию изображения (обертка для безопасного fire-and-forget)
///
@@ -288,6 +382,20 @@ namespace AIImages
float contentY = 0f;
+ // Портрет персонажа (если есть)
+ if (generatedImage != null)
+ {
+ float portraitSize = Mathf.Min(scrollViewRect.width - 20f, 200f);
+ Rect portraitRect = new Rect(
+ (scrollViewRect.width - portraitSize) / 2f,
+ contentY,
+ portraitSize,
+ portraitSize
+ );
+ GUI.DrawTexture(portraitRect, generatedImage);
+ contentY += portraitSize + 15f;
+ }
+
// Секция "Внешность"
Text.Font = GameFont.Medium;
Widgets.Label(
@@ -332,77 +440,10 @@ namespace AIImages
{
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;
+ curY = DrawImagePreview(rect, curY);
+ curY = DrawGenerationStatus(rect, curY);
+ curY = DrawProgressBar(rect, curY);
+ curY = DrawGenerationButton(rect, curY);
// Промпт секция
Text.Font = GameFont.Medium;
@@ -488,6 +529,13 @@ namespace AIImages
{
float height = 0f;
+ // Портрет персонажа (если есть)
+ if (generatedImage != null)
+ {
+ float portraitSize = Mathf.Min(400f, 200f);
+ height += portraitSize + 15f;
+ }
+
// Заголовок "Внешность"
height += 35f;
@@ -510,5 +558,95 @@ namespace AIImages
return height;
}
+
+ 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,
+ rect.y + 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,
+ 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;
+ 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, rect.y + 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, rect.y + 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();
+
+ if (Widgets.ButtonText(new Rect(rect.x, rect.y + curY, rect.width, 35f), buttonLabel))
+ {
+ if (isGenerating)
+ CancelGeneration();
+ else
+ StartGeneration();
+ }
+ return curY + 40f;
+ }
}
}