Implement progress monitoring for image generation in AIImages mod, enhancing user experience with real-time updates. Add localized strings for new features in English and Russian. Refactor UI components for better organization and clarity. Update AIImages.dll to reflect these changes.
This commit is contained in:
Binary file not shown.
@@ -42,6 +42,7 @@
|
||||
<AIImages.Generation.Cancelling>Cancelling generation...</AIImages.Generation.Cancelling>
|
||||
<AIImages.Generation.Error>Generation error</AIImages.Generation.Error>
|
||||
<AIImages.Generation.SavedTo>Image saved to: {0}</AIImages.Generation.SavedTo>
|
||||
<AIImages.Generation.LoadedFromSave>Loaded saved portrait</AIImages.Generation.LoadedFromSave>
|
||||
<AIImages.Generation.NoImage>No image generated yet.\nClick "Generate Image" to start.</AIImages.Generation.NoImage>
|
||||
<!-- Settings -->
|
||||
<AIImages.Settings.ApiSection>API Settings</AIImages.Settings.ApiSection>
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
<AIImages.Generation.Cancelling>Отмена генерации...</AIImages.Generation.Cancelling>
|
||||
<AIImages.Generation.Error>Ошибка генерации</AIImages.Generation.Error>
|
||||
<AIImages.Generation.SavedTo>Изображение сохранено в: {0}</AIImages.Generation.SavedTo>
|
||||
<AIImages.Generation.LoadedFromSave>Загружен сохраненный портрет</AIImages.Generation.LoadedFromSave>
|
||||
<AIImages.Generation.NoImage>Изображение еще не сгенерировано.\nНажмите "Сгенерировать изображение" для начала.</AIImages.Generation.NoImage>
|
||||
<!-- Settings -->
|
||||
<AIImages.Settings.ApiSection>Настройки API</AIImages.Settings.ApiSection>
|
||||
|
||||
31
Source/AIImages/Components/PawnPortraitComp.cs
Normal file
31
Source/AIImages/Components/PawnPortraitComp.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Verse;
|
||||
|
||||
namespace AIImages.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// Компонент для хранения данных AI-сгенерированного портрета пешки
|
||||
/// </summary>
|
||||
public class PawnPortraitComp : ThingComp
|
||||
{
|
||||
/// <summary>
|
||||
/// Путь к сохраненному портрету
|
||||
/// </summary>
|
||||
public string PortraitPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Есть ли сохраненный портрет
|
||||
/// </summary>
|
||||
public bool HasPortrait => !string.IsNullOrEmpty(PortraitPath);
|
||||
|
||||
/// <summary>
|
||||
/// Сохранение/загрузка данных
|
||||
/// </summary>
|
||||
public override void PostExposeData()
|
||||
{
|
||||
base.PostExposeData();
|
||||
string portraitPath = PortraitPath;
|
||||
Scribe_Values.Look(ref portraitPath, "aiPortraitPath", null);
|
||||
PortraitPath = portraitPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Source/AIImages/Helpers/PawnPortraitHelper.cs
Normal file
90
Source/AIImages/Helpers/PawnPortraitHelper.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.IO;
|
||||
using AIImages.Components;
|
||||
using UnityEngine;
|
||||
using Verse;
|
||||
|
||||
namespace AIImages.Helpers
|
||||
{
|
||||
/// <summary>
|
||||
/// Вспомогательный класс для работы с портретами персонажей
|
||||
/// </summary>
|
||||
public static class PawnPortraitHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Получить компонент портрета пешки
|
||||
/// </summary>
|
||||
public static PawnPortraitComp GetPortraitComp(Pawn pawn)
|
||||
{
|
||||
return pawn?.TryGetComp<PawnPortraitComp>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сохранить путь к портрету на пешке
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получить путь к портрету пешки
|
||||
/// </summary>
|
||||
public static string GetPortraitPath(Pawn pawn)
|
||||
{
|
||||
var comp = GetPortraitComp(pawn);
|
||||
return comp?.PortraitPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Есть ли у пешки сохраненный портрет
|
||||
/// </summary>
|
||||
public static bool HasPortrait(Pawn pawn)
|
||||
{
|
||||
var comp = GetPortraitComp(pawn);
|
||||
return comp != null && comp.HasPortrait;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Загрузить портрет пешки как текстуру
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Очистить портрет пешки
|
||||
/// </summary>
|
||||
public static void ClearPortrait(Pawn pawn)
|
||||
{
|
||||
var comp = GetPortraitComp(pawn);
|
||||
if (comp != null)
|
||||
{
|
||||
comp.PortraitPath = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,4 +48,35 @@ namespace AIImages.Models
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Прогресс генерации изображения
|
||||
/// </summary>
|
||||
public class GenerationProgress
|
||||
{
|
||||
/// <summary>
|
||||
/// Процент завершения (0.0 - 1.0)
|
||||
/// </summary>
|
||||
public double Progress { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Текущий шаг
|
||||
/// </summary>
|
||||
public int CurrentStep { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Общее количество шагов
|
||||
/// </summary>
|
||||
public int TotalSteps { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Оставшееся время в секундах (приблизительно)
|
||||
/// </summary>
|
||||
public double EtaRelative { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Идет ли генерация в данный момент
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
42
Source/AIImages/Patches/PawnPortraitCompPatch.cs
Normal file
42
Source/AIImages/Patches/PawnPortraitCompPatch.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using AIImages.Components;
|
||||
using HarmonyLib;
|
||||
using Verse;
|
||||
|
||||
namespace AIImages.Patches
|
||||
{
|
||||
/// <summary>
|
||||
/// Патч для добавления PawnPortraitComp ко всем пешкам
|
||||
/// </summary>
|
||||
[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<PawnPortraitComp>() == null
|
||||
)
|
||||
{
|
||||
// Создаем компонент
|
||||
var comp = new PawnPortraitComp { parent = pawn };
|
||||
|
||||
// Инициализируем компонент
|
||||
comp.Initialize(null);
|
||||
|
||||
// Получаем список компонентов через рефлексию и добавляем наш
|
||||
var compsList = allCompsField.GetValue(pawn) as List<ThingComp>;
|
||||
if (compsList != null)
|
||||
{
|
||||
compsList.Add(comp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(',');
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ namespace AIImages.Services
|
||||
CancellationToken cancellationToken = default
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// Получает прогресс текущей генерации
|
||||
/// </summary>
|
||||
Task<GenerationProgress> GetProgressAsync(CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Проверяет доступность API
|
||||
/// </summary>
|
||||
|
||||
@@ -137,6 +137,42 @@ namespace AIImages.Services
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<GenerationProgress> 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<bool> CheckApiAvailability(
|
||||
string apiEndpoint,
|
||||
CancellationToken cancellationToken = default
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Загружает сохраненный портрет персонажа
|
||||
/// </summary>
|
||||
private void LoadSavedPortrait()
|
||||
{
|
||||
if (PawnPortraitHelper.HasPortrait(pawn))
|
||||
{
|
||||
generatedImage = PawnPortraitHelper.LoadPortrait(pawn);
|
||||
if (generatedImage != null)
|
||||
{
|
||||
generationStatus = "AIImages.Generation.LoadedFromSave".Translate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -100,6 +124,11 @@ namespace AIImages
|
||||
{
|
||||
this.pawn = newPawn;
|
||||
RefreshPawnData();
|
||||
|
||||
// Очищаем старое изображение при смене персонажа
|
||||
generatedImage = null;
|
||||
generationStatus = "";
|
||||
generationProgress = 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Мониторит прогресс генерации и обновляет UI
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Запускает генерацию изображения (обертка для безопасного fire-and-forget)
|
||||
/// </summary>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user