Implement AI Images gallery feature in RimWorld mod, allowing users to view, delete, and manage generated images. Update UI components to include a gallery button and enhance image management functionality. Add localization strings for gallery features in English and Russian. Update AIImages.dll to reflect these changes.
This commit is contained in:
Binary file not shown.
@@ -104,4 +104,18 @@
|
|||||||
<AIImages.Settings.ClearAllImages>Clear All Generated Images</AIImages.Settings.ClearAllImages>
|
<AIImages.Settings.ClearAllImages>Clear All Generated Images</AIImages.Settings.ClearAllImages>
|
||||||
<AIImages.Settings.ClearAllImagesConfirm>Are you sure you want to delete all generated portrait images? This action cannot be undone.</AIImages.Settings.ClearAllImagesConfirm>
|
<AIImages.Settings.ClearAllImagesConfirm>Are you sure you want to delete all generated portrait images? This action cannot be undone.</AIImages.Settings.ClearAllImagesConfirm>
|
||||||
<AIImages.Settings.ClearAllImagesSuccess>Successfully deleted {0} portrait image(s)</AIImages.Settings.ClearAllImagesSuccess>
|
<AIImages.Settings.ClearAllImagesSuccess>Successfully deleted {0} portrait image(s)</AIImages.Settings.ClearAllImagesSuccess>
|
||||||
|
<!-- Gallery -->
|
||||||
|
<AIImages.Gallery.Title>AI Images Gallery</AIImages.Gallery.Title>
|
||||||
|
<AIImages.Gallery.Count>Total images: {0}</AIImages.Gallery.Count>
|
||||||
|
<AIImages.Gallery.Empty>Gallery is empty</AIImages.Gallery.Empty>
|
||||||
|
<AIImages.Gallery.LoadError>Load error</AIImages.Gallery.LoadError>
|
||||||
|
<AIImages.Gallery.DeleteSelected>Delete selected</AIImages.Gallery.DeleteSelected>
|
||||||
|
<AIImages.Gallery.DeleteAll>Delete all ({0})</AIImages.Gallery.DeleteAll>
|
||||||
|
<AIImages.Gallery.ConfirmDelete>Delete selected image?</AIImages.Gallery.ConfirmDelete>
|
||||||
|
<AIImages.Gallery.ConfirmDeleteAll>Delete all {0} images? This action cannot be undone.</AIImages.Gallery.ConfirmDeleteAll>
|
||||||
|
<AIImages.Gallery.DeleteError>Delete error: {0}</AIImages.Gallery.DeleteError>
|
||||||
|
<AIImages.Gallery.Deleted>Image successfully deleted</AIImages.Gallery.Deleted>
|
||||||
|
<AIImages.Gallery.AllDeleted>Successfully deleted {0} images</AIImages.Gallery.AllDeleted>
|
||||||
|
<AIImages.Gallery.OpenGallery>Open gallery</AIImages.Gallery.OpenGallery>
|
||||||
|
<AIImages.Gallery.ImagesCount>({0})</AIImages.Gallery.ImagesCount>
|
||||||
</LanguageData>
|
</LanguageData>
|
||||||
|
|||||||
@@ -104,4 +104,18 @@
|
|||||||
<AIImages.Settings.ClearAllImages>Очистить все сгенерированные изображения</AIImages.Settings.ClearAllImages>
|
<AIImages.Settings.ClearAllImages>Очистить все сгенерированные изображения</AIImages.Settings.ClearAllImages>
|
||||||
<AIImages.Settings.ClearAllImagesConfirm>Вы уверены, что хотите удалить все сгенерированные портреты? Это действие нельзя отменить.</AIImages.Settings.ClearAllImagesConfirm>
|
<AIImages.Settings.ClearAllImagesConfirm>Вы уверены, что хотите удалить все сгенерированные портреты? Это действие нельзя отменить.</AIImages.Settings.ClearAllImagesConfirm>
|
||||||
<AIImages.Settings.ClearAllImagesSuccess>Успешно удалено {0} изображений портретов</AIImages.Settings.ClearAllImagesSuccess>
|
<AIImages.Settings.ClearAllImagesSuccess>Успешно удалено {0} изображений портретов</AIImages.Settings.ClearAllImagesSuccess>
|
||||||
|
<!-- Gallery -->
|
||||||
|
<AIImages.Gallery.Title>Галерея AI изображений</AIImages.Gallery.Title>
|
||||||
|
<AIImages.Gallery.Count>Всего изображений: {0}</AIImages.Gallery.Count>
|
||||||
|
<AIImages.Gallery.Empty>В галерее пока нет изображений</AIImages.Gallery.Empty>
|
||||||
|
<AIImages.Gallery.LoadError>Ошибка загрузки</AIImages.Gallery.LoadError>
|
||||||
|
<AIImages.Gallery.DeleteSelected>Удалить выбранное</AIImages.Gallery.DeleteSelected>
|
||||||
|
<AIImages.Gallery.DeleteAll>Удалить всё ({0})</AIImages.Gallery.DeleteAll>
|
||||||
|
<AIImages.Gallery.ConfirmDelete>Удалить выбранное изображение?</AIImages.Gallery.ConfirmDelete>
|
||||||
|
<AIImages.Gallery.ConfirmDeleteAll>Удалить все {0} изображений? Это действие нельзя отменить.</AIImages.Gallery.ConfirmDeleteAll>
|
||||||
|
<AIImages.Gallery.DeleteError>Ошибка удаления: {0}</AIImages.Gallery.DeleteError>
|
||||||
|
<AIImages.Gallery.Deleted>Изображение успешно удалено</AIImages.Gallery.Deleted>
|
||||||
|
<AIImages.Gallery.AllDeleted>Успешно удалено {0} изображений</AIImages.Gallery.AllDeleted>
|
||||||
|
<AIImages.Gallery.OpenGallery>Открыть галерею</AIImages.Gallery.OpenGallery>
|
||||||
|
<AIImages.Gallery.ImagesCount>({0})</AIImages.Gallery.ImagesCount>
|
||||||
</LanguageData>
|
</LanguageData>
|
||||||
|
|||||||
222
README.md
222
README.md
@@ -0,0 +1,222 @@
|
|||||||
|
# AI Images - RimWorld Mod
|
||||||
|
|
||||||
|
Мод для RimWorld, который генерирует AI-изображения персонажей (пешек) с помощью Stable Diffusion API.
|
||||||
|
|
||||||
|
## 🌟 Возможности
|
||||||
|
|
||||||
|
### 📸 Генерация AI-изображений
|
||||||
|
- **Автоматическое описание персонажей** на основе:
|
||||||
|
- Внешности (пол, возраст, тип тела, цвет кожи)
|
||||||
|
- Прически и цвета волос
|
||||||
|
- Одежды с материалами, качеством и цветами
|
||||||
|
- Черт характера
|
||||||
|
- Генов (биотехнологии)
|
||||||
|
- Состояний здоровья
|
||||||
|
|
||||||
|
### 🎨 Гибкая настройка стилей
|
||||||
|
- **9 предустановленных художественных стилей**:
|
||||||
|
- Реалистичный (Photorealistic)
|
||||||
|
- Полуреалистичный (Semi-Realistic)
|
||||||
|
- Аниме
|
||||||
|
- Концепт-арт
|
||||||
|
- Цифровая живопись
|
||||||
|
- Масляная живопись
|
||||||
|
- Эскиз
|
||||||
|
- Cel-shaded
|
||||||
|
- Без стиля (кастомные промпты)
|
||||||
|
|
||||||
|
- **Возможность создания собственных стилей** через XML-конфигурацию
|
||||||
|
- Базовые промпты и негативные промпты
|
||||||
|
- Автоматическое добавление качественных тегов
|
||||||
|
|
||||||
|
### 📐 Размеры изображений
|
||||||
|
- **Предустановки**: квадратные, портретные, ландшафтные
|
||||||
|
- **Кастомные размеры**: полный контроль над шириной и высотой
|
||||||
|
- **Два типа генерации**: портрет или полное тело
|
||||||
|
|
||||||
|
### 🖼️ Галерея изображений
|
||||||
|
- **Отдельная галерея** для каждого персонажа
|
||||||
|
- **Просмотр всех сгенерированных изображений** в удобной сетке
|
||||||
|
- **Выбор изображений** кликом по миниатюре
|
||||||
|
- **Удаление** отдельных или всех изображений
|
||||||
|
- **Отображение даты создания** и имени файла
|
||||||
|
|
||||||
|
### ⚙️ Настройки Stable Diffusion
|
||||||
|
- **Полная интеграция с Stable Diffusion WebUI**
|
||||||
|
- Загрузка моделей, сэмплеров и планировщиков из API
|
||||||
|
- Настройка количества шагов, CFG Scale, seed
|
||||||
|
- Превью промптов с возможностью копирования
|
||||||
|
- Прогресс-бар генерации с ETA
|
||||||
|
|
||||||
|
### 🎛️ Продвинутые функции
|
||||||
|
- **Автоматическое сохранение** изображений
|
||||||
|
- **История генераций** для каждого персонажа
|
||||||
|
- **Обратная совместимость** со старыми сохранениями
|
||||||
|
- **Двуязычный интерфейс** (русский/английский)
|
||||||
|
- **Отладочные логи** для диагностики
|
||||||
|
|
||||||
|
## 🚀 Установка
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
- **RimWorld** версии 1.6
|
||||||
|
- **Harmony** (автоматически подтягивается через зависимости)
|
||||||
|
- **Stable Diffusion WebUI** (локально или удаленно)
|
||||||
|
|
||||||
|
### Зависимости
|
||||||
|
Мод использует следующие зависимости (автоматически подтягиваются):
|
||||||
|
- `brrainz.harmony` - Harmony
|
||||||
|
- `rim.job.world` - RimJobWorld (необязательно, но в зависимостях)
|
||||||
|
|
||||||
|
### Шаги установки
|
||||||
|
1. Клонируйте репозиторий или скачайте релиз
|
||||||
|
2. Скопируйте папку `ai-images` в директорию `Mods` RimWorld
|
||||||
|
3. Запустите Stable Diffusion WebUI
|
||||||
|
4. В настройках RimWorld включите мод AI Images
|
||||||
|
5. В настройках мода укажите адрес API Stable Diffusion (по умолчанию `http://127.0.0.1:7860`)
|
||||||
|
|
||||||
|
## 📖 Использование
|
||||||
|
|
||||||
|
### Генерация изображений
|
||||||
|
|
||||||
|
1. **Откройте окно AI Images**:
|
||||||
|
- Нажмите на кнопку "AI Портрет" в панели действий персонажа
|
||||||
|
|
||||||
|
2. **Настройте параметры**:
|
||||||
|
- Выберите художественный стиль
|
||||||
|
- Выберите тип изображения (портрет/полное тело)
|
||||||
|
- Настройте размер изображения
|
||||||
|
- При необходимости измените другие параметры
|
||||||
|
|
||||||
|
3. **Сгенерируйте изображение**:
|
||||||
|
- Нажмите кнопку "Сгенерировать изображение"
|
||||||
|
- Дождитесь завершения генерации
|
||||||
|
- Изображение автоматически сохранится
|
||||||
|
|
||||||
|
### Просмотр галереи
|
||||||
|
|
||||||
|
1. **Откройте галерею**:
|
||||||
|
- В окне генерации нажмите кнопку "Открыть галерею"
|
||||||
|
- Откроется окно со всеми изображениями персонажа
|
||||||
|
|
||||||
|
2. **Управление изображениями**:
|
||||||
|
- Кликните по изображению для выбора
|
||||||
|
- Нажмите "Удалить выбранное" для удаления конкретного изображения
|
||||||
|
- Нажмите "Удалить всё" для очистки галереи
|
||||||
|
|
||||||
|
### Настройка стилей
|
||||||
|
|
||||||
|
Мод использует XML-конфигурацию для определения стилей. Вы можете создать свои собственные стили, редактируя файл `Defs/ArtStyleDefs.xml`.
|
||||||
|
|
||||||
|
Пример создания нового стиля:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<AIImages.ArtStyleDef>
|
||||||
|
<defName>ArtStyle_MyCustomStyle</defName>
|
||||||
|
<label>Мой Кастомный Стиль</label>
|
||||||
|
<description>Описание вашего стиля</description>
|
||||||
|
<positivePrompt>ваши ключевые слова здесь</positivePrompt>
|
||||||
|
<negativePrompt>чего избегать</negativePrompt>
|
||||||
|
<qualityTags>дополнительные теги качества</qualityTags>
|
||||||
|
<addBaseQualityTags>true</addBaseQualityTags>
|
||||||
|
<addBaseNegativePrompts>true</addBaseNegativePrompts>
|
||||||
|
<sortOrder>100</sortOrder>
|
||||||
|
</AIImages.ArtStyleDef>
|
||||||
|
```
|
||||||
|
|
||||||
|
Подробнее о конфигурации стилей в [Defs/README.md](Defs/README.md).
|
||||||
|
|
||||||
|
## 🎯 Особенности
|
||||||
|
|
||||||
|
### Умная генерация промптов
|
||||||
|
Мод автоматически анализирует персонажа и создает детальные промпты для Stable Diffusion:
|
||||||
|
- Описывает цвет кожи естественным языком
|
||||||
|
- Добавляет информацию об одежде с материалами и качествами
|
||||||
|
- Учитывает черты характера персонажа
|
||||||
|
- Включает данные о генах из Biotech DLC
|
||||||
|
- Адаптирует промпт в зависимости от типа изображения
|
||||||
|
|
||||||
|
### Оптимизация
|
||||||
|
- Асинхронная генерация без блокировки игры
|
||||||
|
- Возможность отмены генерации
|
||||||
|
- Прогресс-бар с реальным временем выполнения
|
||||||
|
- Эффективное управление памятью для текстур
|
||||||
|
|
||||||
|
### Обратная совместимость
|
||||||
|
Старые сохранения с одним портретом автоматически мигрируют в новую систему галереи.
|
||||||
|
|
||||||
|
## 🛠️ Разработка
|
||||||
|
|
||||||
|
### Структура проекта
|
||||||
|
```
|
||||||
|
ai-images/
|
||||||
|
├── About/ # Метаданные мода
|
||||||
|
├── Assemblies/ # Скомпилированные DLL
|
||||||
|
├── Defs/ # XML-конфигурации (стили, размеры)
|
||||||
|
├── Languages/ # Переводы
|
||||||
|
├── Source/ # Исходный код
|
||||||
|
│ └── AIImages/
|
||||||
|
│ ├── Components/ # Компоненты пешек
|
||||||
|
│ ├── Defs/ # Классы определений
|
||||||
|
│ ├── Helpers/ # Вспомогательные классы
|
||||||
|
│ ├── Models/ # Модели данных
|
||||||
|
│ ├── Patches/ # Harmony патчи
|
||||||
|
│ ├── Services/ # Бизнес-логика
|
||||||
|
│ ├── Settings/ # Настройки мода
|
||||||
|
│ ├── UI/ # Пользовательский интерфейс
|
||||||
|
│ └── Window_AIImage.cs # Главное окно
|
||||||
|
└── Textures/ # Текстуры UI
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сборка
|
||||||
|
```bash
|
||||||
|
cd Source/AIImages
|
||||||
|
dotnet build -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
### Языки
|
||||||
|
Мод поддерживает английский и русский языки. Переводы находятся в `Languages/`.
|
||||||
|
|
||||||
|
## 📝 История версий
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- Базовая генерация AI-изображений
|
||||||
|
- Интеграция с Stable Diffusion API
|
||||||
|
- 9 художественных стилей
|
||||||
|
- Галерея изображений
|
||||||
|
- Настройка размеров
|
||||||
|
- Двуязычный интерфейс
|
||||||
|
|
||||||
|
## 🤝 Вклад
|
||||||
|
|
||||||
|
Приветствуются любые вклады! Пожалуйста:
|
||||||
|
1. Форкните репозиторий
|
||||||
|
2. Создайте ветку для вашей функции
|
||||||
|
3. Закоммитьте изменения
|
||||||
|
4. Отправьте Pull Request
|
||||||
|
|
||||||
|
## 📄 Лицензия
|
||||||
|
|
||||||
|
См. файл [LICENSE](LICENSE) для деталей.
|
||||||
|
|
||||||
|
## 🙏 Благодарности
|
||||||
|
|
||||||
|
- Ludeon Studios за создание RimWorld
|
||||||
|
- Automattic1111 за Stable Diffusion WebUI
|
||||||
|
- Сообщество RimWorld за поддержку
|
||||||
|
|
||||||
|
## ⚠️ Известные ограничения
|
||||||
|
|
||||||
|
- Требует запущенный Stable Diffusion WebUI
|
||||||
|
- Генерация изображений может занимать время в зависимости от настроек
|
||||||
|
- Некоторые модели Stable Diffusion могут работать медленнее других
|
||||||
|
|
||||||
|
## 🐛 Сообщение об ошибках
|
||||||
|
|
||||||
|
Если вы нашли баг или хотите предложить улучшение, пожалуйста, создайте Issue на GitHub с подробным описанием.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Автор**: mrleo1nid
|
||||||
|
**Версия RimWorld**: 1.6
|
||||||
|
**Версия мода**: 1.0.0
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,84 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using AIImages.Helpers;
|
using AIImages.Helpers;
|
||||||
using Verse;
|
using Verse;
|
||||||
|
|
||||||
namespace AIImages.Components
|
namespace AIImages.Components
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Компонент для хранения данных AI-сгенерированного портрета пешки
|
/// Компонент для хранения данных AI-сгенерированных портретов пешки
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PawnPortraitComp : ThingComp
|
public class PawnPortraitComp : ThingComp
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Путь к сохраненному портрету
|
/// Список путей к сохраненным портретам (галерея)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PortraitPath { get; set; }
|
private List<string> portraitPaths = new List<string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Есть ли сохраненный портрет
|
/// Есть ли сохраненные портреты
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool HasPortrait => !string.IsNullOrEmpty(PortraitPath);
|
public bool HasPortrait => portraitPaths != null && portraitPaths.Count > 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Количество портретов в галерее
|
||||||
|
/// </summary>
|
||||||
|
public int PortraitCount => portraitPaths?.Count ?? 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить все пути к портретам
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GetAllPortraits() => portraitPaths?.ToList() ?? new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить последний портрет (для обратной совместимости)
|
||||||
|
/// </summary>
|
||||||
|
public string PortraitPath => HasPortrait ? portraitPaths.Last() : null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Добавить новый портрет в галерею
|
||||||
|
/// </summary>
|
||||||
|
public void AddPortrait(string path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(path))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (portraitPaths == null)
|
||||||
|
portraitPaths = new List<string>();
|
||||||
|
|
||||||
|
portraitPaths.Add(path);
|
||||||
|
DebugLogger.Log($"[AI Images] Added portrait to gallery: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удалить портрет из галереи
|
||||||
|
/// </summary>
|
||||||
|
public bool RemovePortrait(string path)
|
||||||
|
{
|
||||||
|
if (portraitPaths == null || string.IsNullOrEmpty(path))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool removed = portraitPaths.Remove(path);
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
DebugLogger.Log($"[AI Images] Removed portrait from gallery: {path}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Очистить все портреты
|
||||||
|
/// </summary>
|
||||||
|
public void ClearPortraits()
|
||||||
|
{
|
||||||
|
if (portraitPaths != null)
|
||||||
|
{
|
||||||
|
int count = portraitPaths.Count;
|
||||||
|
portraitPaths.Clear();
|
||||||
|
DebugLogger.Log($"[AI Images] Cleared {count} portraits from gallery");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сохранение/загрузка данных
|
/// Сохранение/загрузка данных
|
||||||
@@ -25,33 +87,30 @@ namespace AIImages.Components
|
|||||||
{
|
{
|
||||||
base.PostExposeData();
|
base.PostExposeData();
|
||||||
|
|
||||||
string portraitPath = PortraitPath;
|
|
||||||
bool isSaving = Scribe.mode == LoadSaveMode.Saving;
|
bool isSaving = Scribe.mode == LoadSaveMode.Saving;
|
||||||
bool isLoading = Scribe.mode == LoadSaveMode.LoadingVars;
|
bool isLoading = Scribe.mode == LoadSaveMode.LoadingVars;
|
||||||
|
|
||||||
DebugLogger.Log(
|
DebugLogger.Log(
|
||||||
$"[AI Images] PostExposeData for {parent?.LabelShort} - Mode: {Scribe.mode}, Current path: '{PortraitPath}'"
|
$"[AI Images] PostExposeData for {parent?.LabelShort} - Mode: {Scribe.mode}, Portrait count: {PortraitCount}"
|
||||||
);
|
);
|
||||||
|
|
||||||
Scribe_Values.Look(ref portraitPath, "aiPortraitPath", null);
|
// Сохраняем список портретов
|
||||||
|
Scribe_Collections.Look(ref portraitPaths, "aiPortraitPaths", LookMode.Value);
|
||||||
|
|
||||||
if (isSaving)
|
// Обратная совместимость: если есть старый формат с одним портретом, добавляем его в список
|
||||||
|
if (isLoading && (portraitPaths == null || portraitPaths.Count == 0))
|
||||||
{
|
{
|
||||||
DebugLogger.Log(
|
string oldPortraitPath = null;
|
||||||
$"[AI Images] Saving portrait path for {parent?.LabelShort}: '{portraitPath}'"
|
Scribe_Values.Look(ref oldPortraitPath, "aiPortraitPath", null);
|
||||||
);
|
if (!string.IsNullOrEmpty(oldPortraitPath))
|
||||||
}
|
|
||||||
else if (isLoading)
|
|
||||||
{
|
{
|
||||||
DebugLogger.Log(
|
portraitPaths = new List<string> { oldPortraitPath };
|
||||||
$"[AI Images] Loading portrait path for {parent?.LabelShort}: '{portraitPath}'"
|
DebugLogger.Log($"[AI Images] Migrated old single portrait to gallery: {oldPortraitPath}");
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PortraitPath = portraitPath;
|
|
||||||
|
|
||||||
DebugLogger.Log(
|
DebugLogger.Log(
|
||||||
$"[AI Images] PostExposeData completed for {parent?.LabelShort} - Final path: '{PortraitPath}', HasPortrait: {HasPortrait}"
|
$"[AI Images] PostExposeData completed for {parent?.LabelShort} - Portrait count: {PortraitCount}, HasPortrait: {HasPortrait}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AIImages.Components;
|
using AIImages.Components;
|
||||||
@@ -30,7 +31,7 @@ namespace AIImages.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Сохранить путь к портрету на пешке
|
/// Сохранить путь к портрету на пешке (добавляет в галерею)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SavePortraitPath(Pawn pawn, string path)
|
public static void SavePortraitPath(Pawn pawn, string path)
|
||||||
{
|
{
|
||||||
@@ -41,14 +42,11 @@ namespace AIImages.Helpers
|
|||||||
var comp = GetPortraitComp(pawn);
|
var comp = GetPortraitComp(pawn);
|
||||||
if (comp != null)
|
if (comp != null)
|
||||||
{
|
{
|
||||||
|
comp.AddPortrait(path);
|
||||||
DebugLogger.Log(
|
DebugLogger.Log(
|
||||||
$"[AI Images] Found portrait component for {pawn.Name}, setting path from '{comp.PortraitPath}' to '{path}'"
|
$"[AI Images] Successfully added portrait path for {pawn.Name}: {path}"
|
||||||
);
|
);
|
||||||
comp.PortraitPath = path;
|
DebugLogger.Log($"[AI Images] Component now has {comp.PortraitCount} portraits");
|
||||||
DebugLogger.Log(
|
|
||||||
$"[AI Images] Successfully saved portrait path for {pawn.Name}: {path}"
|
|
||||||
);
|
|
||||||
DebugLogger.Log($"[AI Images] Component now has portrait: {comp.HasPortrait}");
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -147,17 +145,44 @@ namespace AIImages.Helpers
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Очистить портрет пешки
|
/// Очистить портрет пешки (удаляет все портреты)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void ClearPortrait(Pawn pawn)
|
public static void ClearPortrait(Pawn pawn)
|
||||||
{
|
{
|
||||||
var comp = GetPortraitComp(pawn);
|
var comp = GetPortraitComp(pawn);
|
||||||
if (comp != null)
|
if (comp != null)
|
||||||
{
|
{
|
||||||
comp.PortraitPath = null;
|
comp.ClearPortraits();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить все пути к портретам пешки (галерея)
|
||||||
|
/// </summary>
|
||||||
|
public static List<string> GetAllPortraits(Pawn pawn)
|
||||||
|
{
|
||||||
|
var comp = GetPortraitComp(pawn);
|
||||||
|
return comp?.GetAllPortraits() ?? new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить количество портретов в галерее
|
||||||
|
/// </summary>
|
||||||
|
public static int GetPortraitCount(Pawn pawn)
|
||||||
|
{
|
||||||
|
var comp = GetPortraitComp(pawn);
|
||||||
|
return comp?.PortraitCount ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удалить конкретный портрет из галереи
|
||||||
|
/// </summary>
|
||||||
|
public static bool RemovePortrait(Pawn pawn, string path)
|
||||||
|
{
|
||||||
|
var comp = GetPortraitComp(pawn);
|
||||||
|
return comp?.RemovePortrait(path) ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Очистить все сгенерированные портреты
|
/// Очистить все сгенерированные портреты
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -179,7 +204,7 @@ namespace AIImages.Helpers
|
|||||||
var comp = GetPortraitComp(pawn);
|
var comp = GetPortraitComp(pawn);
|
||||||
if (comp != null && comp.HasPortrait)
|
if (comp != null && comp.HasPortrait)
|
||||||
{
|
{
|
||||||
comp.PortraitPath = null;
|
comp.ClearPortraits();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
413
Source/AIImages/Window_AIGallery.cs
Normal file
413
Source/AIImages/Window_AIGallery.cs
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using AIImages.Helpers;
|
||||||
|
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_AIGallery : Window
|
||||||
|
{
|
||||||
|
private Pawn pawn;
|
||||||
|
private List<string> portraitPaths = new List<string>();
|
||||||
|
private List<Texture2D> portraitTextures = new List<Texture2D>();
|
||||||
|
private Vector2 mainScrollPosition = Vector2.zero;
|
||||||
|
private int selectedIndex = 0;
|
||||||
|
|
||||||
|
public Window_AIGallery(Pawn pawn)
|
||||||
|
{
|
||||||
|
this.pawn = pawn;
|
||||||
|
this.doCloseX = true;
|
||||||
|
this.doCloseButton = true;
|
||||||
|
this.forcePause = false;
|
||||||
|
this.absorbInputAroundWindow = false;
|
||||||
|
this.draggable = true;
|
||||||
|
this.preventCameraMotion = false;
|
||||||
|
|
||||||
|
LoadGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Vector2 InitialSize => new Vector2(900f, 700f);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Загружает галерею изображений персонажа
|
||||||
|
/// </summary>
|
||||||
|
private void LoadGallery()
|
||||||
|
{
|
||||||
|
DebugLogger.Log($"[AI Gallery] Loading gallery for {pawn?.Name}");
|
||||||
|
|
||||||
|
// Очищаем старые текстуры
|
||||||
|
UnloadTextures();
|
||||||
|
|
||||||
|
// Загружаем пути к портретам
|
||||||
|
portraitPaths = PawnPortraitHelper.GetAllPortraits(pawn);
|
||||||
|
|
||||||
|
// Загружаем текстуры
|
||||||
|
foreach (var path in portraitPaths)
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
byte[] imageData = File.ReadAllBytes(path);
|
||||||
|
Texture2D texture = new Texture2D(2, 2);
|
||||||
|
texture.LoadImage(imageData);
|
||||||
|
portraitTextures.Add(texture);
|
||||||
|
DebugLogger.Log($"[AI Gallery] Loaded texture from: {path}");
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
DebugLogger.Warning($"[AI Gallery] Failed to load texture: {path}, Error: {ex.Message}");
|
||||||
|
portraitTextures.Add(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DebugLogger.Warning($"[AI Gallery] File not found: {path}");
|
||||||
|
portraitTextures.Add(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugLogger.Log($"[AI Gallery] Loaded {portraitTextures.Count} textures for {pawn?.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Освобождает ресурсы текстур
|
||||||
|
/// </summary>
|
||||||
|
private void UnloadTextures()
|
||||||
|
{
|
||||||
|
foreach (var texture in portraitTextures)
|
||||||
|
{
|
||||||
|
if (texture != null)
|
||||||
|
{
|
||||||
|
Object.Destroy(texture);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
portraitTextures.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Освобождает ресурсы при закрытии окна
|
||||||
|
/// </summary>
|
||||||
|
public override void PreClose()
|
||||||
|
{
|
||||||
|
base.PreClose();
|
||||||
|
UnloadTextures();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отрисовка окна
|
||||||
|
/// </summary>
|
||||||
|
public override void DoWindowContents(Rect inRect)
|
||||||
|
{
|
||||||
|
// Заголовок
|
||||||
|
Text.Font = GameFont.Medium;
|
||||||
|
Widgets.Label(
|
||||||
|
new Rect(0f, 0f, inRect.width, 40f),
|
||||||
|
"AIImages.Gallery.Title".Translate()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Имя персонажа
|
||||||
|
Text.Font = GameFont.Small;
|
||||||
|
Widgets.Label(
|
||||||
|
new Rect(0f, 40f, inRect.width, 30f),
|
||||||
|
"AIImages.Window.PawnLabel".Translate(pawn.NameShortColored.Resolve())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Количество изображений
|
||||||
|
Widgets.Label(
|
||||||
|
new Rect(0f, 70f, inRect.width, 25f),
|
||||||
|
"AIImages.Gallery.Count".Translate(portraitTextures.Count)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Разделитель
|
||||||
|
Widgets.DrawLineHorizontal(0f, 100f, inRect.width);
|
||||||
|
|
||||||
|
// Область для галереи
|
||||||
|
Rect galleryRect = new Rect(0f, 110f, inRect.width, inRect.height - 180f);
|
||||||
|
DrawGallery(galleryRect);
|
||||||
|
|
||||||
|
// Кнопка обновления
|
||||||
|
if (
|
||||||
|
Widgets.ButtonText(
|
||||||
|
new Rect(0f, inRect.height - 70f, inRect.width * 0.3f, 35f),
|
||||||
|
"AIImages.Window.Refresh".Translate()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
LoadGallery();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка удаления выбранного изображения
|
||||||
|
if (
|
||||||
|
portraitTextures.Count > 0
|
||||||
|
&& selectedIndex >= 0
|
||||||
|
&& selectedIndex < portraitTextures.Count
|
||||||
|
&& Widgets.ButtonText(
|
||||||
|
new Rect(inRect.width * 0.32f, inRect.height - 70f, inRect.width * 0.3f, 35f),
|
||||||
|
"AIImages.Gallery.DeleteSelected".Translate()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DeleteSelectedImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кнопка удаления всех изображений
|
||||||
|
if (
|
||||||
|
portraitTextures.Count > 0
|
||||||
|
&& Widgets.ButtonText(
|
||||||
|
new Rect(inRect.width * 0.64f, inRect.height - 70f, inRect.width * 0.36f, 35f),
|
||||||
|
"AIImages.Gallery.DeleteAll".Translate(portraitTextures.Count)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
DeleteAllImages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отрисовка галереи изображений
|
||||||
|
/// </summary>
|
||||||
|
private void DrawGallery(Rect rect)
|
||||||
|
{
|
||||||
|
if (portraitTextures.Count == 0)
|
||||||
|
{
|
||||||
|
Text.Anchor = TextAnchor.MiddleCenter;
|
||||||
|
GUI.color = new Color(1f, 1f, 1f, 0.5f);
|
||||||
|
Widgets.Label(rect, "AIImages.Gallery.Empty".Translate());
|
||||||
|
GUI.color = Color.white;
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Параметры сетки
|
||||||
|
int columns = 3;
|
||||||
|
int rows = Mathf.CeilToInt((float)portraitTextures.Count / columns);
|
||||||
|
float cellWidth = (rect.width - 40f) / columns;
|
||||||
|
float cellHeight = cellWidth + 40f; // Высота ячейки (изображение + кнопки)
|
||||||
|
float spacing = 10f;
|
||||||
|
float viewHeight = rows * cellHeight + spacing;
|
||||||
|
|
||||||
|
Rect viewRect = new Rect(0f, 0f, rect.width - 20f, viewHeight);
|
||||||
|
|
||||||
|
Widgets.BeginScrollView(rect, ref mainScrollPosition, viewRect);
|
||||||
|
|
||||||
|
for (int i = 0; i < portraitTextures.Count; i++)
|
||||||
|
{
|
||||||
|
int row = i / columns;
|
||||||
|
int col = i % columns;
|
||||||
|
|
||||||
|
float x = col * (cellWidth + spacing);
|
||||||
|
float y = row * cellHeight;
|
||||||
|
|
||||||
|
Rect cellRect = new Rect(x, y, cellWidth, cellHeight);
|
||||||
|
DrawGalleryItem(cellRect, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widgets.EndScrollView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Отрисовка одного элемента галереи
|
||||||
|
/// </summary>
|
||||||
|
private void DrawGalleryItem(Rect rect, int index)
|
||||||
|
{
|
||||||
|
// Подсветка выбранного элемента
|
||||||
|
if (index == selectedIndex)
|
||||||
|
{
|
||||||
|
Widgets.DrawBoxSolid(rect, new Color(0.3f, 0.5f, 0.8f, 0.2f));
|
||||||
|
Widgets.DrawBox(rect, 2);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Widgets.DrawBox(rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Изображение
|
||||||
|
Texture2D texture = portraitTextures[index];
|
||||||
|
if (texture != null)
|
||||||
|
{
|
||||||
|
Rect imageRect = new Rect(
|
||||||
|
rect.x + 5f,
|
||||||
|
rect.y + 5f,
|
||||||
|
rect.width - 10f,
|
||||||
|
rect.width - 10f
|
||||||
|
);
|
||||||
|
|
||||||
|
// Клик по изображению для выбора
|
||||||
|
if (Widgets.ButtonInvisible(imageRect))
|
||||||
|
{
|
||||||
|
selectedIndex = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
GUI.DrawTexture(imageRect, texture);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Rect errorRect = new Rect(rect.x + 5f, rect.y + 5f, rect.width - 10f, rect.width - 10f);
|
||||||
|
Widgets.DrawBoxSolid(errorRect, new Color(0.5f, 0f, 0f, 0.5f));
|
||||||
|
Text.Anchor = TextAnchor.MiddleCenter;
|
||||||
|
GUI.color = Color.white;
|
||||||
|
Widgets.Label(errorRect, "AIImages.Gallery.LoadError".Translate());
|
||||||
|
GUI.color = Color.white;
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Информация под изображением
|
||||||
|
Text.Font = GameFont.Tiny;
|
||||||
|
Text.Anchor = TextAnchor.UpperLeft;
|
||||||
|
float infoY = rect.y + rect.width - 5f;
|
||||||
|
|
||||||
|
// Имя файла
|
||||||
|
string fileName = Path.GetFileName(portraitPaths[index]);
|
||||||
|
if (fileName.Length > 30)
|
||||||
|
{
|
||||||
|
fileName = fileName.Substring(0, 27) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
Widgets.Label(
|
||||||
|
new Rect(rect.x + 5f, infoY, rect.width - 10f, 20f),
|
||||||
|
fileName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Дата создания
|
||||||
|
if (File.Exists(portraitPaths[index]))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var fileInfo = new FileInfo(portraitPaths[index]);
|
||||||
|
string dateStr = fileInfo.CreationTime.ToString("dd.MM.yyyy HH:mm");
|
||||||
|
Widgets.Label(
|
||||||
|
new Rect(rect.x + 5f, infoY + 15f, rect.width - 10f, 20f),
|
||||||
|
dateStr
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Игнорируем ошибки чтения даты
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет выбранное изображение
|
||||||
|
/// </summary>
|
||||||
|
private void DeleteSelectedImage()
|
||||||
|
{
|
||||||
|
if (selectedIndex < 0 || selectedIndex >= portraitPaths.Count)
|
||||||
|
return;
|
||||||
|
|
||||||
|
string path = portraitPaths[selectedIndex];
|
||||||
|
|
||||||
|
// Подтверждение
|
||||||
|
Find.WindowStack.Add(
|
||||||
|
Dialog_MessageBox.CreateConfirmation(
|
||||||
|
"AIImages.Gallery.ConfirmDelete".Translate(),
|
||||||
|
delegate
|
||||||
|
{
|
||||||
|
// Удаляем из файловой системы
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
DebugLogger.Log($"[AI Gallery] Deleted file: {path}");
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
Messages.Message(
|
||||||
|
"AIImages.Gallery.DeleteError".Translate(ex.Message),
|
||||||
|
MessageTypeDefOf.RejectInput
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем из компонента пешки
|
||||||
|
PawnPortraitHelper.RemovePortrait(pawn, path);
|
||||||
|
|
||||||
|
// Обновляем галерею
|
||||||
|
LoadGallery();
|
||||||
|
|
||||||
|
// Сбрасываем выбор
|
||||||
|
if (selectedIndex >= portraitTextures.Count)
|
||||||
|
{
|
||||||
|
selectedIndex = portraitTextures.Count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Messages.Message(
|
||||||
|
"AIImages.Gallery.Deleted".Translate(),
|
||||||
|
MessageTypeDefOf.PositiveEvent
|
||||||
|
);
|
||||||
|
},
|
||||||
|
destructive: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Удаляет все изображения
|
||||||
|
/// </summary>
|
||||||
|
private void DeleteAllImages()
|
||||||
|
{
|
||||||
|
// Подтверждение
|
||||||
|
Find.WindowStack.Add(
|
||||||
|
Dialog_MessageBox.CreateConfirmation(
|
||||||
|
"AIImages.Gallery.ConfirmDeleteAll".Translate(portraitPaths.Count),
|
||||||
|
delegate
|
||||||
|
{
|
||||||
|
// Удаляем все файлы
|
||||||
|
int deletedCount = 0;
|
||||||
|
foreach (var path in portraitPaths)
|
||||||
|
{
|
||||||
|
if (File.Exists(path))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Delete(path);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
catch (System.Exception ex)
|
||||||
|
{
|
||||||
|
DebugLogger.Warning(
|
||||||
|
$"[AI Gallery] Failed to delete file: {path}, Error: {ex.Message}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем компонент пешки
|
||||||
|
PawnPortraitHelper.ClearPortrait(pawn);
|
||||||
|
|
||||||
|
// Обновляем галерею
|
||||||
|
LoadGallery();
|
||||||
|
selectedIndex = 0;
|
||||||
|
|
||||||
|
Messages.Message(
|
||||||
|
"AIImages.Gallery.AllDeleted".Translate(deletedCount),
|
||||||
|
MessageTypeDefOf.PositiveEvent
|
||||||
|
);
|
||||||
|
},
|
||||||
|
destructive: true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1293,8 +1293,8 @@ namespace AIImages
|
|||||||
? "AIImages.Generation.Cancel".Translate()
|
? "AIImages.Generation.Cancel".Translate()
|
||||||
: "AIImages.Generation.Generate".Translate();
|
: "AIImages.Generation.Generate".Translate();
|
||||||
|
|
||||||
// Основная кнопка генерации (занимает 70% ширины)
|
// Основная кнопка генерации (занимает 40% ширины)
|
||||||
float buttonWidth = rect.width * 0.7f;
|
float buttonWidth = rect.width * 0.4f;
|
||||||
if (Widgets.ButtonText(new Rect(rect.x, curY, buttonWidth, 35f), buttonLabel))
|
if (Widgets.ButtonText(new Rect(rect.x, curY, buttonWidth, 35f), buttonLabel))
|
||||||
{
|
{
|
||||||
if (isGenerating)
|
if (isGenerating)
|
||||||
@@ -1303,9 +1303,25 @@ namespace AIImages
|
|||||||
StartGeneration();
|
StartGeneration();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отладочная кнопка (занимает 25% ширины)
|
// Кнопка галереи
|
||||||
float debugButtonWidth = rect.width * 0.25f;
|
float galleryButtonWidth = rect.width * 0.35f;
|
||||||
float debugButtonX = rect.x + buttonWidth + 10f;
|
float galleryButtonX = rect.x + buttonWidth + 10f;
|
||||||
|
int imageCount = PawnPortraitHelper.GetPortraitCount(pawn);
|
||||||
|
string galleryLabel = "AIImages.Gallery.OpenGallery".Translate();
|
||||||
|
if (imageCount > 0)
|
||||||
|
{
|
||||||
|
galleryLabel += " " + "AIImages.Gallery.ImagesCount".Translate(imageCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Widgets.ButtonText(new Rect(galleryButtonX, curY, galleryButtonWidth, 35f), galleryLabel))
|
||||||
|
{
|
||||||
|
var galleryWindow = new Window_AIGallery(pawn);
|
||||||
|
Find.WindowStack.Add(galleryWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отладочная кнопка (занимает 20% ширины)
|
||||||
|
float debugButtonWidth = rect.width * 0.2f;
|
||||||
|
float debugButtonX = galleryButtonX + galleryButtonWidth + 10f;
|
||||||
if (Widgets.ButtonText(new Rect(debugButtonX, curY, debugButtonWidth, 35f), "Debug"))
|
if (Widgets.ButtonText(new Rect(debugButtonX, curY, debugButtonWidth, 35f), "Debug"))
|
||||||
{
|
{
|
||||||
DebugAllPawns();
|
DebugAllPawns();
|
||||||
|
|||||||
Reference in New Issue
Block a user