Enhance AIImages mod with settings support and improved UI for image generation. Update localized strings in English and Russian for better clarity. Refactor code for better organization and maintainability.

This commit is contained in:
Leonid Pershin
2025-10-26 18:09:30 +03:00
parent 990c8695b7
commit 0f60721162
17 changed files with 1907 additions and 335 deletions

Binary file not shown.

View File

@@ -2,19 +2,21 @@
<LanguageData>
<!-- Gizmo (button) -->
<AIImages.Gizmo.Label>AI Image</AIImages.Gizmo.Label>
<AIImages.Gizmo.Description>Open AI Image window</AIImages.Gizmo.Description>
<AIImages.Gizmo.Description>Open AI Image window to generate character portraits</AIImages.Gizmo.Description>
<!-- Window -->
<AIImages.Window.Title>AI Image Window</AIImages.Window.Title>
<AIImages.Window.Title>AI Image Generator</AIImages.Window.Title>
<AIImages.Window.PawnLabel>Character: {0}</AIImages.Window.PawnLabel>
<AIImages.Window.Refresh>Refresh</AIImages.Window.Refresh>
<!-- Appearance -->
<AIImages.Appearance.SectionTitle>Appearance</AIImages.Appearance.SectionTitle>
<AIImages.Appearance.NoInfo>Appearance information unavailable</AIImages.Appearance.NoInfo>
<AIImages.Appearance.Gender>Gender: {0}</AIImages.Appearance.Gender>
<AIImages.Appearance.Age>Age: {0} years</AIImages.Appearance.Age>
<AIImages.Appearance.BodyType>Body type: {0}</AIImages.Appearance.BodyType>
<AIImages.Appearance.SkinColor>Skin color: RGB({0}, {1}, {2})</AIImages.Appearance.SkinColor>
<AIImages.Appearance.SkinTone>Skin tone: {0}</AIImages.Appearance.SkinTone>
<AIImages.Appearance.Hairstyle>Hairstyle: {0}</AIImages.Appearance.Hairstyle>
<AIImages.Appearance.HairColor>Hair color: RGB({0}, {1}, {2})</AIImages.Appearance.HairColor>
<AIImages.Appearance.HairColorDesc>Hair color: {0}</AIImages.Appearance.HairColorDesc>
<AIImages.Appearance.Beard>Beard: {0}</AIImages.Appearance.Beard>
<AIImages.Appearance.Traits>Traits:</AIImages.Appearance.Traits>
<!-- Apparel -->
<AIImages.Apparel.SectionTitle>Apparel</AIImages.Apparel.SectionTitle>
@@ -24,9 +26,45 @@
<AIImages.Apparel.Quality> Quality: {0}</AIImages.Apparel.Quality>
<AIImages.Apparel.Material> Material: {0}</AIImages.Apparel.Material>
<AIImages.Apparel.Durability> Durability: {0}/{1} ({2}%)</AIImages.Apparel.Durability>
<AIImages.Apparel.Color> Color: RGB({0}, {1}, {2})</AIImages.Apparel.Color>
<AIImages.Apparel.ColorDesc> Color: {0}</AIImages.Apparel.ColorDesc>
<!-- Stable Diffusion Prompt -->
<AIImages.Prompt.SectionTitle>Stable Diffusion Prompt</AIImages.Prompt.SectionTitle>
<AIImages.Prompt.CopyButton>Copy Prompt</AIImages.Prompt.CopyButton>
<AIImages.Prompt.Copied>Copied!</AIImages.Prompt.Copied>
<!-- Generation -->
<AIImages.Generation.Generate>Generate Image</AIImages.Generation.Generate>
<AIImages.Generation.Generating>Generating...</AIImages.Generation.Generating>
<AIImages.Generation.InProgress>Generating image, please wait...</AIImages.Generation.InProgress>
<AIImages.Generation.Success>Image generated successfully!</AIImages.Generation.Success>
<AIImages.Generation.Failed>Generation failed</AIImages.Generation.Failed>
<AIImages.Generation.SavedTo>Image saved to: {0}</AIImages.Generation.SavedTo>
<AIImages.Generation.NoImage>No image generated yet.\nClick "Generate Image" to start.</AIImages.Generation.NoImage>
<!-- Settings -->
<AIImages.Settings.ApiSection>API Settings</AIImages.Settings.ApiSection>
<AIImages.Settings.ApiSectionTooltip>Configure connection to Stable Diffusion API</AIImages.Settings.ApiSectionTooltip>
<AIImages.Settings.ApiEndpoint>API Endpoint</AIImages.Settings.ApiEndpoint>
<AIImages.Settings.TestConnection>Test Connection</AIImages.Settings.TestConnection>
<AIImages.Settings.LoadModels>Load Available Models</AIImages.Settings.LoadModels>
<AIImages.Settings.ConnectionSuccess>Successfully connected to API!</AIImages.Settings.ConnectionSuccess>
<AIImages.Settings.ConnectionFailed>Failed to connect to API. Check endpoint and ensure Stable Diffusion WebUI is running.</AIImages.Settings.ConnectionFailed>
<AIImages.Settings.ModelsLoaded>Loaded {0} models from API</AIImages.Settings.ModelsLoaded>
<AIImages.Settings.NoModelsFound>No models found. Check API connection.</AIImages.Settings.NoModelsFound>
<AIImages.Settings.GenerationSection>Generation Settings</AIImages.Settings.GenerationSection>
<AIImages.Settings.GenerationSectionTooltip>Configure image generation parameters</AIImages.Settings.GenerationSectionTooltip>
<AIImages.Settings.ArtStyle>Art Style</AIImages.Settings.ArtStyle>
<AIImages.Settings.ShotType>Shot Type</AIImages.Settings.ShotType>
<AIImages.Settings.Steps>Sampling Steps</AIImages.Settings.Steps>
<AIImages.Settings.CfgScale>CFG Scale</AIImages.Settings.CfgScale>
<AIImages.Settings.Width>Width</AIImages.Settings.Width>
<AIImages.Settings.Height>Height</AIImages.Settings.Height>
<AIImages.Settings.Sampler>Sampler</AIImages.Settings.Sampler>
<AIImages.Settings.PromptsSection>Prompts</AIImages.Settings.PromptsSection>
<AIImages.Settings.PromptsSectionTooltip>Base prompts that will be added to all generations</AIImages.Settings.PromptsSectionTooltip>
<AIImages.Settings.BasePositivePrompt>Base Positive Prompt</AIImages.Settings.BasePositivePrompt>
<AIImages.Settings.BaseNegativePrompt>Base Negative Prompt</AIImages.Settings.BaseNegativePrompt>
<AIImages.Settings.OptionsSection>Options</AIImages.Settings.OptionsSection>
<AIImages.Settings.AutoLoadModels>Auto-load models on startup</AIImages.Settings.AutoLoadModels>
<AIImages.Settings.ShowTechnicalInfo>Show technical information</AIImages.Settings.ShowTechnicalInfo>
<AIImages.Settings.SaveHistory>Save generation history</AIImages.Settings.SaveHistory>
<AIImages.Settings.SavePath>Save Path</AIImages.Settings.SavePath>
</LanguageData>

View File

@@ -1,22 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LanguageData>
<!-- Gizmo (кнопка) -->
<AIImages.Gizmo.Label>AI Изображение</AIImages.Gizmo.Label>
<AIImages.Gizmo.Description>Открыть окно AI изображения</AIImages.Gizmo.Description>
<!-- Окно -->
<AIImages.Window.Title>Окно AI изображения</AIImages.Window.Title>
<!-- Gizmo (button) -->
<AIImages.Gizmo.Label>AI Портрет</AIImages.Gizmo.Label>
<AIImages.Gizmo.Description>Открыть окно генерации AI портретов персонажа</AIImages.Gizmo.Description>
<!-- Window -->
<AIImages.Window.Title>Генератор AI Изображений</AIImages.Window.Title>
<AIImages.Window.PawnLabel>Персонаж: {0}</AIImages.Window.PawnLabel>
<!-- Внешность -->
<AIImages.Window.Refresh>Обновить</AIImages.Window.Refresh>
<!-- Appearance -->
<AIImages.Appearance.SectionTitle>Внешность</AIImages.Appearance.SectionTitle>
<AIImages.Appearance.NoInfo>Информация о внешности недоступна</AIImages.Appearance.NoInfo>
<AIImages.Appearance.Gender>Пол: {0}</AIImages.Appearance.Gender>
<AIImages.Appearance.Age>Возраст: {0} лет</AIImages.Appearance.Age>
<AIImages.Appearance.BodyType>Тип тела: {0}</AIImages.Appearance.BodyType>
<AIImages.Appearance.SkinColor>Цвет кожи: RGB({0}, {1}, {2})</AIImages.Appearance.SkinColor>
<AIImages.Appearance.SkinTone>Тон кожи: {0}</AIImages.Appearance.SkinTone>
<AIImages.Appearance.Hairstyle>Прическа: {0}</AIImages.Appearance.Hairstyle>
<AIImages.Appearance.HairColor>Цвет волос: RGB({0}, {1}, {2})</AIImages.Appearance.HairColor>
<AIImages.Appearance.HairColorDesc>Цвет волос: {0}</AIImages.Appearance.HairColorDesc>
<AIImages.Appearance.Beard>Борода: {0}</AIImages.Appearance.Beard>
<AIImages.Appearance.Traits>Черты характера:</AIImages.Appearance.Traits>
<!-- Одежда -->
<!-- Apparel -->
<AIImages.Apparel.SectionTitle>Одежда</AIImages.Apparel.SectionTitle>
<AIImages.Apparel.NoInfo>Информация об одежде недоступна</AIImages.Apparel.NoInfo>
<AIImages.Apparel.NoClothes>Персонаж ничего не носит</AIImages.Apparel.NoClothes>
@@ -24,9 +26,45 @@
<AIImages.Apparel.Quality> Качество: {0}</AIImages.Apparel.Quality>
<AIImages.Apparel.Material> Материал: {0}</AIImages.Apparel.Material>
<AIImages.Apparel.Durability> Прочность: {0}/{1} ({2}%)</AIImages.Apparel.Durability>
<AIImages.Apparel.Color> Цвет: RGB({0}, {1}, {2})</AIImages.Apparel.Color>
<!-- Stable Diffusion Промпт -->
<AIImages.Prompt.SectionTitle>Промпт для Stable Diffusion</AIImages.Prompt.SectionTitle>
<AIImages.Apparel.ColorDesc> Цвет: {0}</AIImages.Apparel.ColorDesc>
<!-- Stable Diffusion Prompt -->
<AIImages.Prompt.SectionTitle>Промпт Stable Diffusion</AIImages.Prompt.SectionTitle>
<AIImages.Prompt.CopyButton>Копировать промпт</AIImages.Prompt.CopyButton>
<AIImages.Prompt.Copied>Скопировано!</AIImages.Prompt.Copied>
<!-- Generation -->
<AIImages.Generation.Generate>Сгенерировать изображение</AIImages.Generation.Generate>
<AIImages.Generation.Generating>Генерация...</AIImages.Generation.Generating>
<AIImages.Generation.InProgress>Генерируется изображение, пожалуйста подождите...</AIImages.Generation.InProgress>
<AIImages.Generation.Success>Изображение успешно сгенерировано!</AIImages.Generation.Success>
<AIImages.Generation.Failed>Ошибка генерации</AIImages.Generation.Failed>
<AIImages.Generation.SavedTo>Изображение сохранено в: {0}</AIImages.Generation.SavedTo>
<AIImages.Generation.NoImage>Изображение еще не сгенерировано.\nНажмите "Сгенерировать изображение" для начала.</AIImages.Generation.NoImage>
<!-- Settings -->
<AIImages.Settings.ApiSection>Настройки API</AIImages.Settings.ApiSection>
<AIImages.Settings.ApiSectionTooltip>Настройка подключения к API Stable Diffusion</AIImages.Settings.ApiSectionTooltip>
<AIImages.Settings.ApiEndpoint>Адрес API</AIImages.Settings.ApiEndpoint>
<AIImages.Settings.TestConnection>Проверить соединение</AIImages.Settings.TestConnection>
<AIImages.Settings.LoadModels>Загрузить доступные модели</AIImages.Settings.LoadModels>
<AIImages.Settings.ConnectionSuccess>Успешное подключение к API!</AIImages.Settings.ConnectionSuccess>
<AIImages.Settings.ConnectionFailed>Не удалось подключиться к API. Проверьте адрес и убедитесь, что Stable Diffusion WebUI запущен.</AIImages.Settings.ConnectionFailed>
<AIImages.Settings.ModelsLoaded>Загружено {0} моделей из API</AIImages.Settings.ModelsLoaded>
<AIImages.Settings.NoModelsFound>Модели не найдены. Проверьте подключение к API.</AIImages.Settings.NoModelsFound>
<AIImages.Settings.GenerationSection>Настройки генерации</AIImages.Settings.GenerationSection>
<AIImages.Settings.GenerationSectionTooltip>Настройка параметров генерации изображений</AIImages.Settings.GenerationSectionTooltip>
<AIImages.Settings.ArtStyle>Художественный стиль</AIImages.Settings.ArtStyle>
<AIImages.Settings.ShotType>Тип кадра</AIImages.Settings.ShotType>
<AIImages.Settings.Steps>Количество шагов сэмплирования</AIImages.Settings.Steps>
<AIImages.Settings.CfgScale>CFG Scale</AIImages.Settings.CfgScale>
<AIImages.Settings.Width>Ширина</AIImages.Settings.Width>
<AIImages.Settings.Height>Высота</AIImages.Settings.Height>
<AIImages.Settings.Sampler>Сэмплер</AIImages.Settings.Sampler>
<AIImages.Settings.PromptsSection>Промпты</AIImages.Settings.PromptsSection>
<AIImages.Settings.PromptsSectionTooltip>Базовые промпты, которые будут добавлены ко всем генерациям</AIImages.Settings.PromptsSectionTooltip>
<AIImages.Settings.BasePositivePrompt>Базовый позитивный промпт</AIImages.Settings.BasePositivePrompt>
<AIImages.Settings.BaseNegativePrompt>Базовый негативный промпт</AIImages.Settings.BaseNegativePrompt>
<AIImages.Settings.OptionsSection>Опции</AIImages.Settings.OptionsSection>
<AIImages.Settings.AutoLoadModels>Автоматически загружать модели при запуске</AIImages.Settings.AutoLoadModels>
<AIImages.Settings.ShowTechnicalInfo>Показывать техническую информацию</AIImages.Settings.ShowTechnicalInfo>
<AIImages.Settings.SaveHistory>Сохранять историю генераций</AIImages.Settings.SaveHistory>
<AIImages.Settings.SavePath>Путь для сохранения</AIImages.Settings.SavePath>
</LanguageData>

View File

@@ -1,19 +1,59 @@
using AIImages.Services;
using AIImages.Settings;
using HarmonyLib;
using UnityEngine;
using Verse;
namespace AIImages
{
/// <summary>
/// Main mod class that initializes Harmony patches
/// Main mod class with settings support
/// </summary>
public class AIImagesMod : Mod
{
public static AIImagesModSettings Settings { get; private set; }
// Singleton сервисы
public static IPawnDescriptionService PawnDescriptionService { get; private set; }
public static IPromptGeneratorService PromptGeneratorService { get; private set; }
public static IStableDiffusionApiService ApiService { get; private set; }
public AIImagesMod(ModContentPack content)
: base(content)
{
Settings = GetSettings<AIImagesModSettings>();
// Инициализируем сервисы
PawnDescriptionService = new PawnDescriptionService();
PromptGeneratorService = new AdvancedPromptGenerator();
ApiService = new StableDiffusionApiService(Settings.savePath);
Log.Message("[AI Images] Mod initialized successfully with settings");
}
public override void DoSettingsWindowContents(Rect inRect)
{
AIImagesSettingsUI.DoSettingsWindowContents(inRect, Settings);
base.DoSettingsWindowContents(inRect);
}
public override string SettingsCategory()
{
return "AI Images";
}
}
/// <summary>
/// Static constructor for Harmony patches
/// </summary>
[StaticConstructorOnStartup]
public static class AIImagesMod
public static class AIImagesHarmonyPatcher
{
static AIImagesMod()
static AIImagesHarmonyPatcher()
{
var harmony = new Harmony("Mrleo1nid.aiimages");
harmony.PatchAll();
Log.Message("[AI Images] Mod initialized successfully");
Log.Message("[AI Images] Harmony patches applied successfully");
}
}
}

View File

@@ -0,0 +1,50 @@
namespace AIImages.Models
{
/// <summary>
/// Запрос на генерацию изображения
/// </summary>
public class GenerationRequest
{
public string Prompt { get; set; }
public string NegativePrompt { get; set; }
public int Steps { get; set; }
public float CfgScale { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string Sampler { get; set; }
public int Seed { get; set; }
public string Model { get; set; }
}
/// <summary>
/// Результат генерации изображения
/// </summary>
public class GenerationResult
{
public bool Success { get; set; }
public byte[] ImageData { get; set; }
public string ErrorMessage { get; set; }
public string SavedPath { get; set; }
public GenerationRequest Request { get; set; }
public static GenerationResult Failure(string error)
{
return new GenerationResult { Success = false, ErrorMessage = error };
}
public static GenerationResult SuccessResult(
byte[] imageData,
string savedPath,
GenerationRequest request
)
{
return new GenerationResult
{
Success = true,
ImageData = imageData,
SavedPath = savedPath,
Request = request,
};
}
}
}

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using RimWorld;
using UnityEngine;
using Verse;
namespace AIImages.Models
{
/// <summary>
/// Модель данных внешности персонажа для генерации промптов
/// </summary>
public class PawnAppearanceData
{
public string Name { get; set; }
public Gender Gender { get; set; }
public int Age { get; set; }
public string BodyType { get; set; }
public Color SkinColor { get; set; }
public string HairStyle { get; set; }
public Color HairColor { get; set; }
public List<Trait> Traits { get; set; }
public List<ApparelData> Apparel { get; set; }
public PawnAppearanceData()
{
Traits = new List<Trait>();
Apparel = new List<ApparelData>();
}
}
/// <summary>
/// Данные об одежде персонажа
/// </summary>
public class ApparelData
{
public string Label { get; set; }
public string Material { get; set; }
public QualityCategory? Quality { get; set; }
public Color Color { get; set; }
public string LayerType { get; set; }
public int Durability { get; set; }
public int MaxDurability { get; set; }
public float DurabilityPercent =>
MaxDurability > 0 ? (float)Durability / MaxDurability : 1f;
}
}

View File

@@ -0,0 +1,62 @@
namespace AIImages.Models
{
/// <summary>
/// Настройки для генерации изображений через Stable Diffusion
/// </summary>
public class StableDiffusionSettings
{
public string PositivePrompt { get; set; }
public string NegativePrompt { get; set; }
public int Steps { get; set; }
public float CfgScale { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string Sampler { get; set; }
public int Seed { get; set; }
public string Model { get; set; }
public ArtStyle ArtStyle { get; set; }
public ShotType ShotType { get; set; }
public StableDiffusionSettings()
{
// Значения по умолчанию
Steps = 30;
CfgScale = 7.5f;
Width = 512;
Height = 768;
Sampler = "Euler a";
Seed = -1; // Случайный seed
ArtStyle = ArtStyle.Realistic;
ShotType = ShotType.Portrait;
PositivePrompt = "";
NegativePrompt = "ugly, deformed, low quality, blurry, bad anatomy, worst quality";
}
}
/// <summary>
/// Художественный стиль изображения
/// </summary>
public enum ArtStyle
{
Realistic,
SemiRealistic,
Anime,
ConceptArt,
DigitalPainting,
OilPainting,
Sketch,
CellShaded,
}
/// <summary>
/// Тип кадра/композиции
/// </summary>
public enum ShotType
{
Portrait, // Портрет (голова и плечи)
HalfBody, // Половина тела
FullBody, // Полное тело
CloseUp, // Крупный план
ThreeQuarter, // Три четверти
}
}

View File

@@ -0,0 +1,353 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AIImages.Models;
using RimWorld;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Продвинутый генератор промптов для Stable Diffusion
/// </summary>
public class AdvancedPromptGenerator : IPromptGeneratorService
{
private static readonly Dictionary<string, string> TraitToMood = new Dictionary<
string,
string
>
{
{ "Kind", "warm smile, gentle expression, friendly eyes" },
{ "Bloodlust", "intense gaze, fierce expression, aggressive posture" },
{ "Psychopath", "cold eyes, emotionless face, calculating expression" },
{ "Pessimist", "sad eyes, worried expression, downcast gaze" },
{ "Optimist", "bright smile, hopeful expression, cheerful demeanor" },
{ "Nervous", "anxious expression, tense posture, worried eyes" },
{ "Careful", "focused expression, attentive gaze, composed posture" },
{ "Brave", "confident expression, determined gaze, strong posture" },
{ "Wimp", "fearful eyes, nervous expression, timid posture" },
{ "Greedy", "cunning expression, calculating eyes, sly smile" },
{ "Jealous", "envious gaze, bitter expression, tense face" },
{ "Ascetic", "serene expression, calm demeanor, peaceful eyes" },
{ "Beautiful", "stunning features, attractive appearance, graceful" },
{ "Ugly", "rough features, weathered appearance" },
{ "Pretty", "attractive features, pleasant appearance, charming" },
};
private static readonly Dictionary<ArtStyle, string> ArtStylePrompts = new Dictionary<
ArtStyle,
string
>
{
{ ArtStyle.Realistic, "photorealistic, hyperrealistic, realistic photo, photography" },
{ ArtStyle.SemiRealistic, "semi-realistic, detailed illustration, realistic art" },
{ ArtStyle.Anime, "anime style, manga style, anime character" },
{
ArtStyle.ConceptArt,
"concept art, digital art, artstation, professional concept design"
},
{ ArtStyle.DigitalPainting, "digital painting, painterly, brush strokes, artistic" },
{ ArtStyle.OilPainting, "oil painting, traditional painting, canvas, fine art" },
{ ArtStyle.Sketch, "pencil sketch, hand drawn, sketch art, line art" },
{ ArtStyle.CellShaded, "cell shaded, flat colors, toon shading, stylized" },
};
private static readonly Dictionary<ShotType, string> ShotTypePrompts = new Dictionary<
ShotType,
string
>
{
{ ShotType.Portrait, "portrait, head and shoulders" },
{ ShotType.HalfBody, "half body shot, waist up" },
{ ShotType.FullBody, "full body, full length" },
{ ShotType.CloseUp, "close up, face focus, detailed face" },
{ ShotType.ThreeQuarter, "three-quarter view, 3/4 view" },
};
public string GeneratePositivePrompt(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings
)
{
if (appearanceData == null)
return "portrait of a person";
StringBuilder prompt = new StringBuilder();
// 1. Художественный стиль
if (ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt))
{
prompt.Append(stylePrompt);
prompt.Append(", ");
}
// 2. Тип кадра
if (ShotTypePrompts.TryGetValue(settings.ShotType, out string shotPrompt))
{
prompt.Append(shotPrompt);
prompt.Append(" of ");
}
// 3. Базовое описание (возраст и пол)
prompt.Append(GetAgeAndGenderDescription(appearanceData));
prompt.Append(", ");
// 4. Тип тела
string bodyType = GetBodyTypeDescription(appearanceData.BodyType);
if (!string.IsNullOrEmpty(bodyType))
{
prompt.Append(bodyType);
prompt.Append(", ");
}
// 5. Цвет кожи
string skinTone = ColorDescriptionService.GetSkinToneDescription(
appearanceData.SkinColor
);
prompt.Append(skinTone);
prompt.Append(", ");
// 6. Волосы
string hairDescription = GetHairDescription(appearanceData);
if (!string.IsNullOrEmpty(hairDescription))
{
prompt.Append(hairDescription);
prompt.Append(", ");
}
// 7. Настроение и выражение на основе черт характера
string moodDescription = GetMoodFromTraits(appearanceData.Traits);
if (!string.IsNullOrEmpty(moodDescription))
{
prompt.Append(moodDescription);
prompt.Append(", ");
}
// 8. Одежда
string apparelDescription = GetApparelDescription(appearanceData.Apparel);
if (!string.IsNullOrEmpty(apparelDescription))
{
prompt.Append(apparelDescription);
prompt.Append(", ");
}
// 9. Базовый пользовательский промпт (если указан)
if (!string.IsNullOrEmpty(settings.PositivePrompt))
{
prompt.Append(settings.PositivePrompt);
prompt.Append(", ");
}
// 10. Качественные теги
prompt.Append(GetQualityTags(settings.ArtStyle));
return prompt.ToString().Trim().TrimEnd(',');
}
public string GenerateNegativePrompt(StableDiffusionSettings settings)
{
StringBuilder negativePrompt = new StringBuilder();
// Базовые негативные промпты
negativePrompt.Append(
"ugly, deformed, low quality, blurry, bad anatomy, worst quality, "
);
negativePrompt.Append(
"mutated, disfigured, bad proportions, extra limbs, missing limbs, "
);
// Специфичные для стиля негативы
if (
settings.ArtStyle == ArtStyle.Realistic
|| settings.ArtStyle == ArtStyle.SemiRealistic
)
{
negativePrompt.Append("cartoon, anime, painting, drawing, illustration, ");
}
else if (settings.ArtStyle == ArtStyle.Anime)
{
negativePrompt.Append("realistic, photo, photography, 3d, ");
}
// Пользовательский негативный промпт
if (!string.IsNullOrEmpty(settings.NegativePrompt))
{
negativePrompt.Append(settings.NegativePrompt);
}
return negativePrompt.ToString().Trim().TrimEnd(',');
}
public string GetFullPromptDescription(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings
)
{
StringBuilder description = new StringBuilder();
description.AppendLine("=== Positive Prompt ===");
description.AppendLine(GeneratePositivePrompt(appearanceData, settings));
description.AppendLine();
description.AppendLine("=== Negative Prompt ===");
description.AppendLine(GenerateNegativePrompt(settings));
description.AppendLine();
description.AppendLine("=== Technical Parameters ===");
description.AppendLine($"Steps: {settings.Steps}");
description.AppendLine($"CFG Scale: {settings.CfgScale}");
description.AppendLine($"Size: {settings.Width}x{settings.Height}");
description.AppendLine($"Sampler: {settings.Sampler}");
description.AppendLine(
$"Seed: {(settings.Seed == -1 ? "Random" : settings.Seed.ToString())}"
);
return description.ToString();
}
private string GetAgeAndGenderDescription(PawnAppearanceData data)
{
string ageGroup = data.Age switch
{
< 18 => "young",
< 25 => "young adult",
< 35 => "adult",
< 50 => "middle-aged",
< 65 => "mature",
_ => "elderly",
};
string genderLabel = data.Gender switch
{
Gender.Male => "man",
Gender.Female => "woman",
_ => "person",
};
return $"{ageGroup} {genderLabel}";
}
private string GetBodyTypeDescription(string bodyType)
{
if (string.IsNullOrEmpty(bodyType))
return "";
return bodyType.ToLower() switch
{
"thin" => "slender build, lean physique",
"hulk" => "muscular build, strong physique, athletic body",
"fat" => "heavyset build, stocky physique",
"female" => "feminine build",
"male" => "masculine build",
_ => "average build",
};
}
private string GetHairDescription(PawnAppearanceData data)
{
if (string.IsNullOrEmpty(data.HairStyle))
return "";
StringBuilder hair = new StringBuilder();
// Цвет волос
string hairColor = ColorDescriptionService.GetHairColorDescription(data.HairColor);
hair.Append(hairColor);
hair.Append(" ");
// Стиль прически (упрощаем сложные названия)
string style = data
.HairStyle.ToLower()
.Replace("_", " ")
.Replace("shaved", "very short")
.Replace("mohawk", "mohawk hairstyle");
hair.Append(style);
hair.Append(" hair");
return hair.ToString();
}
private string GetMoodFromTraits(List<Trait> traits)
{
if (traits == null || !traits.Any())
return "neutral expression";
// Ищем черты, которые влияют на внешний вид
foreach (var trait in traits)
{
string traitDefName = trait.def.defName;
if (TraitToMood.TryGetValue(traitDefName, out string mood))
{
return mood;
}
}
return "calm expression";
}
private string GetApparelDescription(List<ApparelData> apparel)
{
if (apparel == null || !apparel.Any())
return "simple clothes";
StringBuilder apparelDesc = new StringBuilder("wearing ");
// Берем топ 5 наиболее заметных предметов одежды
var visibleApparel = apparel.Take(5).ToList();
List<string> items = new List<string>();
foreach (var item in visibleApparel)
{
StringBuilder itemDesc = new StringBuilder();
// Цвет (если не белый)
if (item.Color != UnityEngine.Color.white)
{
string colorDesc = ColorDescriptionService.GetApparelColorDescription(
item.Color
);
itemDesc.Append(colorDesc);
itemDesc.Append(" ");
}
// Материал
if (!string.IsNullOrEmpty(item.Material))
{
itemDesc.Append(item.Material.ToLower());
itemDesc.Append(" ");
}
// Название предмета
itemDesc.Append(item.Label.ToLower());
items.Add(itemDesc.ToString());
}
apparelDesc.Append(string.Join(", ", items));
return apparelDesc.ToString();
}
private string GetQualityTags(ArtStyle style)
{
var baseTags = "highly detailed, professional, masterpiece, best quality";
if (style == ArtStyle.Realistic || style == ArtStyle.SemiRealistic)
{
return $"{baseTags}, professional photography, 8k uhd, dslr, high quality, sharp focus";
}
else if (style == ArtStyle.Anime)
{
return $"{baseTags}, anime masterpiece, high resolution, vibrant colors";
}
else if (style == ArtStyle.ConceptArt)
{
return $"{baseTags}, trending on artstation, professional digital art";
}
else
{
return baseTags;
}
}
}
}

View File

@@ -0,0 +1,165 @@
using UnityEngine;
namespace AIImages.Services
{
/// <summary>
/// Сервис для умного определения цветов (вместо RGB значений)
/// </summary>
public static class ColorDescriptionService
{
/// <summary>
/// Получает текстовое описание цвета волос
/// </summary>
public static string GetHairColorDescription(Color color)
{
float h,
s,
v;
Color.RGBToHSV(color, out h, out s, out v);
// Проверяем на оттенки серого
if (s < 0.15f)
{
return GetGrayscaleDescription(v);
}
// Определяем оттенок
string hueDescription = GetHueDescription(h);
string brightnessModifier = GetBrightnessModifier(v, s);
return $"{brightnessModifier}{hueDescription}".Trim();
}
/// <summary>
/// Получает текстовое описание цвета кожи
/// </summary>
public static string GetSkinToneDescription(Color color)
{
// Вычисляем яркость
float brightness = (color.r + color.g + color.b) / 3f;
// Определяем оттенок кожи
if (brightness >= 0.85f)
return "very fair skin";
else if (brightness >= 0.75f)
return "fair skin";
else if (brightness >= 0.65f)
return "light skin";
else if (brightness >= 0.55f)
return "medium skin";
else if (brightness >= 0.45f)
return "olive skin";
else if (brightness >= 0.35f)
return "tan skin";
else if (brightness >= 0.25f)
return "brown skin";
else if (brightness >= 0.15f)
return "dark brown skin";
else
return "very dark skin";
}
/// <summary>
/// Получает описание цвета одежды
/// </summary>
public static string GetApparelColorDescription(Color color)
{
float h,
s,
v;
Color.RGBToHSV(color, out h, out s, out v);
// Проверяем на оттенки серого
if (s < 0.1f)
{
return GetGrayscaleDescription(v);
}
// Определяем оттенок
string hueDescription = GetHueDescription(h);
// Модификаторы насыщенности и яркости
string saturationModifier = "";
if (s < 0.3f)
saturationModifier = "pale ";
else if (s > 0.8f)
saturationModifier = "vivid ";
string brightnessModifier = "";
if (v < 0.3f)
brightnessModifier = "dark ";
else if (v > 0.8f)
brightnessModifier = "bright ";
return $"{brightnessModifier}{saturationModifier}{hueDescription}".Trim();
}
private static string GetGrayscaleDescription(float value)
{
if (value >= 0.95f)
return "white";
else if (value >= 0.8f)
return "light gray";
else if (value >= 0.6f)
return "gray";
else if (value >= 0.4f)
return "dark gray";
else if (value >= 0.2f)
return "charcoal";
else
return "black";
}
private static string GetHueDescription(float hue)
{
// Hue from 0 to 1
if (hue < 0.05f || hue >= 0.95f)
return "red";
if (hue < 0.083f)
return "orange-red";
if (hue < 0.15f)
return "orange";
if (hue < 0.19f)
return "golden";
if (hue < 0.22f)
return "yellow";
if (hue < 0.35f)
return "yellow-green";
if (hue < 0.45f)
return "green";
if (hue < 0.52f)
return "cyan";
if (hue < 0.58f)
return "light blue";
if (hue < 0.65f)
return "blue";
if (hue < 0.72f)
return "deep blue";
if (hue < 0.78f)
return "purple";
if (hue < 0.85f)
return "violet";
if (hue < 0.92f)
return "magenta";
return "pink";
}
private static string GetBrightnessModifier(float value, float saturation)
{
// Для волос используем специальные термины
if (value < 0.2f)
return "jet black ";
else if (value < 0.3f)
return "black ";
else if (value < 0.45f)
return "dark ";
else if (value > 0.85f && saturation < 0.3f)
return "platinum ";
else if (value > 0.75f)
return "light ";
else
return "";
}
}
}

View File

@@ -0,0 +1,26 @@
using AIImages.Models;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Интерфейс сервиса для извлечения данных о внешности персонажа
/// </summary>
public interface IPawnDescriptionService
{
/// <summary>
/// Извлекает данные о внешности персонажа
/// </summary>
PawnAppearanceData ExtractAppearanceData(Pawn pawn);
/// <summary>
/// Получает текстовое описание внешности для отображения в UI
/// </summary>
string GetAppearanceDescription(Pawn pawn);
/// <summary>
/// Получает текстовое описание одежды для отображения в UI
/// </summary>
string GetApparelDescription(Pawn pawn);
}
}

View File

@@ -0,0 +1,31 @@
using AIImages.Models;
namespace AIImages.Services
{
/// <summary>
/// Интерфейс сервиса для генерации промптов Stable Diffusion
/// </summary>
public interface IPromptGeneratorService
{
/// <summary>
/// Генерирует позитивный промпт на основе данных о персонаже
/// </summary>
string GeneratePositivePrompt(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings
);
/// <summary>
/// Генерирует негативный промпт на основе настроек
/// </summary>
string GenerateNegativePrompt(StableDiffusionSettings settings);
/// <summary>
/// Получает полное описание промпта (позитивный + негативный) для отображения
/// </summary>
string GetFullPromptDescription(
PawnAppearanceData appearanceData,
StableDiffusionSettings settings
);
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using AIImages.Models;
namespace AIImages.Services
{
/// <summary>
/// Интерфейс сервиса для работы с Stable Diffusion API
/// </summary>
public interface IStableDiffusionApiService
{
/// <summary>
/// Генерирует изображение на основе запроса
/// </summary>
Task<GenerationResult> GenerateImageAsync(GenerationRequest request);
/// <summary>
/// Проверяет доступность API
/// </summary>
Task<bool> CheckApiAvailability(string apiEndpoint);
/// <summary>
/// Получает список доступных моделей с API
/// </summary>
Task<List<string>> GetAvailableModels(string apiEndpoint);
/// <summary>
/// Получает список доступных сэмплеров
/// </summary>
Task<List<string>> GetAvailableSamplers(string apiEndpoint);
}
}

View File

@@ -0,0 +1,183 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using AIImages.Models;
using RimWorld;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Сервис для извлечения и описания данных о внешности персонажа
/// </summary>
public class PawnDescriptionService : IPawnDescriptionService
{
public PawnAppearanceData ExtractAppearanceData(Pawn pawn)
{
if (pawn?.story == null)
return null;
var data = new PawnAppearanceData
{
Name = pawn.Name?.ToStringShort ?? "Unknown",
Gender = pawn.gender,
Age = pawn.ageTracker.AgeBiologicalYears,
BodyType = pawn.story.bodyType?.defName,
SkinColor = pawn.story.SkinColor,
HairStyle = pawn.story.hairDef?.label,
HairColor = pawn.story.HairColor,
};
// Извлекаем черты характера
if (pawn.story.traits?.allTraits != null)
{
data.Traits.AddRange(pawn.story.traits.allTraits);
}
// Извлекаем одежду
if (pawn.apparel?.WornApparel != null)
{
foreach (var apparel in pawn.apparel.WornApparel)
{
var apparelData = new ApparelData
{
Label = apparel.def.label,
Material = apparel.Stuff?.label,
Color = apparel.DrawColor,
LayerType = apparel.def.apparel?.LastLayer.ToString(),
Durability = apparel.HitPoints,
MaxDurability = apparel.MaxHitPoints,
};
if (apparel.TryGetQuality(out QualityCategory quality))
{
apparelData.Quality = quality;
}
data.Apparel.Add(apparelData);
}
}
return data;
}
public string GetAppearanceDescription(Pawn pawn)
{
if (pawn?.story == null)
return "AIImages.Appearance.NoInfo".Translate();
StringBuilder sb = new StringBuilder();
// Пол
sb.AppendLine("AIImages.Appearance.Gender".Translate(pawn.gender.GetLabel()));
// Возраст
sb.AppendLine("AIImages.Appearance.Age".Translate(pawn.ageTracker.AgeBiologicalYears));
// Тип тела
if (pawn.story.bodyType != null)
{
sb.AppendLine(
"AIImages.Appearance.BodyType".Translate(pawn.story.bodyType.defName)
);
}
// Цвет кожи (с умным описанием)
if (pawn.story.SkinColor != null)
{
string skinDescription = ColorDescriptionService.GetSkinToneDescription(
pawn.story.SkinColor
);
sb.AppendLine("AIImages.Appearance.SkinTone".Translate(skinDescription));
}
// Волосы
if (pawn.story.hairDef != null)
{
sb.AppendLine("AIImages.Appearance.Hairstyle".Translate(pawn.story.hairDef.label));
if (pawn.story.HairColor != null)
{
string hairColorDescription = ColorDescriptionService.GetHairColorDescription(
pawn.story.HairColor
);
sb.AppendLine(
"AIImages.Appearance.HairColorDesc".Translate(hairColorDescription)
);
}
}
// Черты характера
if (pawn.story.traits?.allTraits != null && pawn.story.traits.allTraits.Any())
{
sb.AppendLine("\n" + "AIImages.Appearance.Traits".Translate());
foreach (var trait in pawn.story.traits.allTraits)
{
sb.AppendLine($" • {trait.LabelCap}");
}
}
return sb.ToString();
}
public string GetApparelDescription(Pawn pawn)
{
if (pawn?.apparel == null)
return "AIImages.Apparel.NoInfo".Translate();
StringBuilder sb = new StringBuilder();
List<Apparel> wornApparel = pawn.apparel.WornApparel;
if (wornApparel == null || !wornApparel.Any())
{
sb.AppendLine("AIImages.Apparel.NoClothes".Translate());
}
else
{
sb.AppendLine("AIImages.Apparel.ListHeader".Translate(wornApparel.Count) + "\n");
foreach (Apparel apparel in wornApparel)
{
FormatApparelItem(sb, apparel);
}
}
return sb.ToString();
}
private void FormatApparelItem(StringBuilder sb, Apparel apparel)
{
sb.AppendLine($"• {apparel.LabelCap}");
if (apparel.TryGetQuality(out QualityCategory quality))
{
sb.AppendLine("AIImages.Apparel.Quality".Translate(quality.GetLabel()));
}
if (apparel.Stuff != null)
{
sb.AppendLine("AIImages.Apparel.Material".Translate(apparel.Stuff.LabelCap));
}
if (apparel.HitPoints < apparel.MaxHitPoints)
{
int percentage = (int)((float)apparel.HitPoints / apparel.MaxHitPoints * 100);
sb.AppendLine(
"AIImages.Apparel.Durability".Translate(
apparel.HitPoints,
apparel.MaxHitPoints,
percentage
)
);
}
if (apparel.DrawColor != UnityEngine.Color.white)
{
string colorDesc = ColorDescriptionService.GetApparelColorDescription(
apparel.DrawColor
);
sb.AppendLine("AIImages.Apparel.ColorDesc".Translate(colorDesc));
}
sb.AppendLine();
}
}
}

View File

@@ -0,0 +1,234 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using AIImages.Models;
using Newtonsoft.Json;
using Verse;
namespace AIImages.Services
{
/// <summary>
/// Сервис для работы с Stable Diffusion API (AUTOMATIC1111 WebUI)
/// </summary>
public class StableDiffusionApiService : IStableDiffusionApiService
{
private readonly HttpClient httpClient;
private readonly string saveFolderPath;
public StableDiffusionApiService(string savePath = "AIImages/Generated")
{
httpClient = new HttpClient { Timeout = TimeSpan.FromMinutes(5) };
// Определяем путь для сохранения
saveFolderPath = Path.Combine(GenFilePaths.SaveDataFolderPath, savePath);
// Создаем папку, если не существует
if (!Directory.Exists(saveFolderPath))
{
Directory.CreateDirectory(saveFolderPath);
}
}
public async Task<GenerationResult> GenerateImageAsync(GenerationRequest request)
{
try
{
Log.Message(
$"[AI Images] Starting image generation with prompt: {request.Prompt.Substring(0, Math.Min(50, request.Prompt.Length))}..."
);
// Формируем JSON запрос для AUTOMATIC1111 API
var apiRequest = new
{
prompt = request.Prompt,
negative_prompt = request.NegativePrompt,
steps = request.Steps,
cfg_scale = request.CfgScale,
width = request.Width,
height = request.Height,
sampler_name = request.Sampler,
seed = request.Seed,
save_images = false,
send_images = true,
};
string jsonRequest = JsonConvert.SerializeObject(apiRequest);
var content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
// Отправляем запрос
string endpoint = $"{request.Model}/sdapi/v1/txt2img";
HttpResponseMessage response = await httpClient.PostAsync(endpoint, content);
if (!response.IsSuccessStatusCode)
{
string errorContent = await response.Content.ReadAsStringAsync();
Log.Error(
$"[AI Images] API request failed: {response.StatusCode} - {errorContent}"
);
return GenerationResult.Failure($"API Error: {response.StatusCode}");
}
string jsonResponse = await response.Content.ReadAsStringAsync();
var apiResponse = JsonConvert.DeserializeObject<Txt2ImgResponse>(jsonResponse);
if (apiResponse?.images == null || apiResponse.images.Length == 0)
{
return GenerationResult.Failure("No images returned from API");
}
// Декодируем изображение из base64
byte[] imageData = Convert.FromBase64String(apiResponse.images[0]);
// Сохраняем изображение
string fileName = $"pawn_{DateTime.Now:yyyyMMdd_HHmmss}.png";
string fullPath = Path.Combine(saveFolderPath, fileName);
await File.WriteAllBytesAsync(fullPath, imageData);
Log.Message($"[AI Images] Image generated successfully and saved to: {fullPath}");
return GenerationResult.SuccessResult(imageData, fullPath, request);
}
catch (TaskCanceledException)
{
return GenerationResult.Failure("Request timeout. Generation took too long.");
}
catch (HttpRequestException ex)
{
Log.Error($"[AI Images] HTTP error: {ex.Message}");
return GenerationResult.Failure($"Connection error: {ex.Message}");
}
catch (Exception ex)
{
Log.Error($"[AI Images] Unexpected error: {ex.Message}\n{ex.StackTrace}");
return GenerationResult.Failure($"Error: {ex.Message}");
}
}
public async Task<bool> CheckApiAvailability(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
return response.IsSuccessStatusCode;
}
catch (Exception ex)
{
Log.Warning($"[AI Images] API check failed: {ex.Message}");
return false;
}
}
public async Task<List<string>> GetAvailableModels(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/sd-models";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
if (!response.IsSuccessStatusCode)
return new List<string>();
string jsonResponse = await response.Content.ReadAsStringAsync();
var models = JsonConvert.DeserializeObject<List<SdModel>>(jsonResponse);
var modelNames = new List<string>();
if (models != null)
{
foreach (var model in models)
{
modelNames.Add(model.title ?? model.model_name);
}
}
Log.Message($"[AI Images] Found {modelNames.Count} models");
return modelNames;
}
catch (Exception ex)
{
Log.Error($"[AI Images] Failed to load models: {ex.Message}");
return new List<string>();
}
}
public async Task<List<string>> GetAvailableSamplers(string apiEndpoint)
{
try
{
string endpoint = $"{apiEndpoint}/sdapi/v1/samplers";
HttpResponseMessage response = await httpClient.GetAsync(endpoint);
if (!response.IsSuccessStatusCode)
return GetDefaultSamplers();
string jsonResponse = await response.Content.ReadAsStringAsync();
var samplers = JsonConvert.DeserializeObject<List<SdSampler>>(jsonResponse);
var samplerNames = new List<string>();
if (samplers != null)
{
foreach (var sampler in samplers)
{
samplerNames.Add(sampler.name);
}
}
Log.Message($"[AI Images] Found {samplerNames.Count} samplers");
return samplerNames;
}
catch (Exception ex)
{
Log.Warning($"[AI Images] Failed to load samplers: {ex.Message}");
return GetDefaultSamplers();
}
}
private List<string> GetDefaultSamplers()
{
return new List<string>
{
"Euler a",
"Euler",
"LMS",
"Heun",
"DPM2",
"DPM2 a",
"DPM++ 2S a",
"DPM++ 2M",
"DPM++ SDE",
"DPM fast",
"DPM adaptive",
"LMS Karras",
"DPM2 Karras",
"DPM2 a Karras",
"DPM++ 2S a Karras",
"DPM++ 2M Karras",
"DPM++ SDE Karras",
"DDIM",
"PLMS",
};
}
// Вспомогательные классы для десериализации JSON ответов
#pragma warning disable S3459, S1144 // Properties set by JSON deserializer
private sealed class Txt2ImgResponse
{
public string[] images { get; set; }
}
private sealed class SdModel
{
public string title { get; set; }
public string model_name { get; set; }
}
private sealed class SdSampler
{
public string name { get; set; }
}
#pragma warning restore S3459, S1144
}
}

View File

@@ -0,0 +1,94 @@
using AIImages.Models;
using Verse;
namespace AIImages.Settings
{
/// <summary>
/// Настройки мода AI Images
/// </summary>
#pragma warning disable S1104 // Fields required for RimWorld's Scribe serialization system
public class AIImagesModSettings : ModSettings
{
// API настройки
public string apiEndpoint = "http://127.0.0.1:7860";
public string selectedModel = "";
public string selectedSampler = "Euler a";
// Настройки генерации
public int steps = 30;
public float cfgScale = 7.5f;
public int width = 512;
public int height = 768;
public int seed = -1;
// Промпты
public string basePositivePrompt = "";
public string baseNegativePrompt =
"ugly, deformed, low quality, blurry, bad anatomy, worst quality";
// Художественный стиль
public ArtStyle artStyle = ArtStyle.Realistic;
public ShotType shotType = ShotType.Portrait;
// Путь для сохранения
public string savePath = "AIImages/Generated";
// Флаги
public bool autoLoadModels = true;
public bool showTechnicalInfo = true;
public bool saveGenerationHistory = true;
public override void ExposeData()
{
Scribe_Values.Look(ref apiEndpoint, "apiEndpoint", "http://127.0.0.1:7860");
Scribe_Values.Look(ref selectedModel, "selectedModel", "");
Scribe_Values.Look(ref selectedSampler, "selectedSampler", "Euler a");
Scribe_Values.Look(ref steps, "steps", 30);
Scribe_Values.Look(ref cfgScale, "cfgScale", 7.5f);
Scribe_Values.Look(ref width, "width", 512);
Scribe_Values.Look(ref height, "height", 768);
Scribe_Values.Look(ref seed, "seed", -1);
Scribe_Values.Look(ref basePositivePrompt, "basePositivePrompt", "");
Scribe_Values.Look(
ref baseNegativePrompt,
"baseNegativePrompt",
"ugly, deformed, low quality, blurry, bad anatomy, worst quality"
);
Scribe_Values.Look(ref artStyle, "artStyle", ArtStyle.Realistic);
Scribe_Values.Look(ref shotType, "shotType", ShotType.Portrait);
Scribe_Values.Look(ref savePath, "savePath", "AIImages/Generated");
Scribe_Values.Look(ref autoLoadModels, "autoLoadModels", true);
Scribe_Values.Look(ref showTechnicalInfo, "showTechnicalInfo", true);
Scribe_Values.Look(ref saveGenerationHistory, "saveGenerationHistory", true);
base.ExposeData();
}
/// <summary>
/// Создает объект StableDiffusionSettings из настроек мода
/// </summary>
public StableDiffusionSettings ToStableDiffusionSettings()
{
return new StableDiffusionSettings
{
Steps = steps,
CfgScale = cfgScale,
Width = width,
Height = height,
Sampler = selectedSampler,
Seed = seed,
Model = selectedModel,
ArtStyle = artStyle,
ShotType = shotType,
PositivePrompt = basePositivePrompt,
NegativePrompt = baseNegativePrompt,
};
}
}
#pragma warning restore S1104
}

View File

@@ -0,0 +1,284 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AIImages.Models;
using AIImages.Settings;
using RimWorld;
using UnityEngine;
using Verse;
namespace AIImages
{
/// <summary>
/// UI для настроек мода в меню настроек RimWorld
/// </summary>
public static class AIImagesSettingsUI
{
private static Vector2 scrollPosition = Vector2.zero;
private static string stepsBuffer;
private static string widthBuffer;
private static string heightBuffer;
public static void DoSettingsWindowContents(Rect inRect, AIImagesModSettings settings)
{
// Инициализируем буферы при первом вызове
if (string.IsNullOrEmpty(stepsBuffer))
{
stepsBuffer = settings.steps.ToString();
widthBuffer = settings.width.ToString();
heightBuffer = settings.height.ToString();
}
Listing_Standard listingStandard = new Listing_Standard();
Rect viewRect = new Rect(0f, 0f, inRect.width - 20f, 1200f);
Widgets.BeginScrollView(inRect, ref scrollPosition, viewRect);
listingStandard.Begin(viewRect);
// === API Settings ===
listingStandard.Label(
"AIImages.Settings.ApiSection".Translate(),
-1f,
"AIImages.Settings.ApiSectionTooltip".Translate()
);
listingStandard.GapLine();
listingStandard.Label("AIImages.Settings.ApiEndpoint".Translate() + ":");
settings.apiEndpoint = listingStandard.TextEntry(settings.apiEndpoint);
listingStandard.Gap(8f);
// Кнопка проверки подключения
if (listingStandard.ButtonText("AIImages.Settings.TestConnection".Translate()))
{
_ = TestApiConnection(settings.apiEndpoint);
}
// Кнопка загрузки моделей
if (listingStandard.ButtonText("AIImages.Settings.LoadModels".Translate()))
{
_ = LoadModelsFromApi(settings);
}
listingStandard.Gap(12f);
// === Generation Settings ===
listingStandard.Label(
"AIImages.Settings.GenerationSection".Translate(),
-1f,
"AIImages.Settings.GenerationSectionTooltip".Translate()
);
listingStandard.GapLine();
// Art Style
if (
listingStandard.ButtonTextLabeled(
"AIImages.Settings.ArtStyle".Translate(),
settings.artStyle.ToString()
)
)
{
List<FloatMenuOption> styleOptions = new List<FloatMenuOption>();
foreach (ArtStyle style in Enum.GetValues(typeof(ArtStyle)))
{
ArtStyle localStyle = style;
styleOptions.Add(
new FloatMenuOption(style.ToString(), () => settings.artStyle = localStyle)
);
}
Find.WindowStack.Add(new FloatMenu(styleOptions));
}
// Shot Type
if (
listingStandard.ButtonTextLabeled(
"AIImages.Settings.ShotType".Translate(),
settings.shotType.ToString()
)
)
{
List<FloatMenuOption> shotOptions = new List<FloatMenuOption>();
foreach (ShotType shot in Enum.GetValues(typeof(ShotType)))
{
ShotType localShot = shot;
shotOptions.Add(
new FloatMenuOption(shot.ToString(), () => settings.shotType = localShot)
);
}
Find.WindowStack.Add(new FloatMenu(shotOptions));
}
listingStandard.Gap(8f);
// Steps
listingStandard.Label("AIImages.Settings.Steps".Translate() + $": {settings.steps}");
settings.steps = (int)listingStandard.Slider(settings.steps, 1, 150);
listingStandard.Gap(8f);
// CFG Scale
listingStandard.Label(
"AIImages.Settings.CfgScale".Translate() + $": {settings.cfgScale:F1}"
);
settings.cfgScale = listingStandard.Slider(settings.cfgScale, 1f, 30f);
listingStandard.Gap(8f);
// Width
listingStandard.Label("AIImages.Settings.Width".Translate() + ":");
widthBuffer = listingStandard.TextEntry(widthBuffer);
if (int.TryParse(widthBuffer, out int width))
{
settings.width = Mathf.Clamp(width, 64, 2048);
}
// Height
listingStandard.Label("AIImages.Settings.Height".Translate() + ":");
heightBuffer = listingStandard.TextEntry(heightBuffer);
if (int.TryParse(heightBuffer, out int height))
{
settings.height = Mathf.Clamp(height, 64, 2048);
}
// Common size presets
listingStandard.Gap(4f);
Rect presetRect = listingStandard.GetRect(30f);
if (Widgets.ButtonText(new Rect(presetRect.x, presetRect.y, 80f, 30f), "512x512"))
{
settings.width = 512;
settings.height = 512;
widthBuffer = "512";
heightBuffer = "512";
}
if (Widgets.ButtonText(new Rect(presetRect.x + 85f, presetRect.y, 80f, 30f), "512x768"))
{
settings.width = 512;
settings.height = 768;
widthBuffer = "512";
heightBuffer = "768";
}
if (
Widgets.ButtonText(new Rect(presetRect.x + 170f, presetRect.y, 80f, 30f), "768x768")
)
{
settings.width = 768;
settings.height = 768;
widthBuffer = "768";
heightBuffer = "768";
}
listingStandard.Gap(12f);
// Sampler
listingStandard.Label("AIImages.Settings.Sampler".Translate() + ":");
settings.selectedSampler = listingStandard.TextEntry(settings.selectedSampler);
listingStandard.Gap(12f);
// === Prompts ===
listingStandard.Label(
"AIImages.Settings.PromptsSection".Translate(),
-1f,
"AIImages.Settings.PromptsSectionTooltip".Translate()
);
listingStandard.GapLine();
listingStandard.Label("AIImages.Settings.BasePositivePrompt".Translate() + ":");
settings.basePositivePrompt = listingStandard.TextEntry(settings.basePositivePrompt, 3);
listingStandard.Gap(8f);
listingStandard.Label("AIImages.Settings.BaseNegativePrompt".Translate() + ":");
settings.baseNegativePrompt = listingStandard.TextEntry(settings.baseNegativePrompt, 3);
listingStandard.Gap(12f);
// === Options ===
listingStandard.Label("AIImages.Settings.OptionsSection".Translate());
listingStandard.GapLine();
listingStandard.CheckboxLabeled(
"AIImages.Settings.AutoLoadModels".Translate(),
ref settings.autoLoadModels
);
listingStandard.CheckboxLabeled(
"AIImages.Settings.ShowTechnicalInfo".Translate(),
ref settings.showTechnicalInfo
);
listingStandard.CheckboxLabeled(
"AIImages.Settings.SaveHistory".Translate(),
ref settings.saveGenerationHistory
);
listingStandard.Gap(12f);
// Save path
listingStandard.Label("AIImages.Settings.SavePath".Translate() + ":");
settings.savePath = listingStandard.TextEntry(settings.savePath);
listingStandard.End();
Widgets.EndScrollView();
}
private static async System.Threading.Tasks.Task TestApiConnection(string endpoint)
{
try
{
Log.Message($"[AI Images] Testing connection to {endpoint}...");
bool available = await AIImagesMod.ApiService.CheckApiAvailability(endpoint);
if (available)
{
Messages.Message(
"AIImages.Settings.ConnectionSuccess".Translate(),
MessageTypeDefOf.PositiveEvent
);
}
else
{
Messages.Message(
"AIImages.Settings.ConnectionFailed".Translate(),
MessageTypeDefOf.RejectInput
);
}
}
catch (Exception ex)
{
Messages.Message($"Error: {ex.Message}", MessageTypeDefOf.RejectInput);
}
}
private static async System.Threading.Tasks.Task LoadModelsFromApi(
AIImagesModSettings settings
)
{
try
{
Log.Message("[AI Images] Loading models from API...");
var models = await AIImagesMod.ApiService.GetAvailableModels(settings.apiEndpoint);
if (models.Count > 0)
{
Messages.Message(
"AIImages.Settings.ModelsLoaded".Translate(models.Count),
MessageTypeDefOf.PositiveEvent
);
// Если модель не выбрана, выбираем первую
if (string.IsNullOrEmpty(settings.selectedModel) && models.Count > 0)
{
settings.selectedModel = models[0];
}
}
else
{
Messages.Message(
"AIImages.Settings.NoModelsFound".Translate(),
MessageTypeDefOf.RejectInput
);
}
}
catch (Exception ex)
{
Messages.Message(
$"Error loading models: {ex.Message}",
MessageTypeDefOf.RejectInput
);
}
}
}
}

View File

@@ -1,6 +1,6 @@
using System.Collections.Generic;
using System;
using System.Linq;
using System.Text;
using AIImages.Models;
using RimWorld;
using UnityEngine;
using Verse;
@@ -10,7 +10,7 @@ using Verse;
namespace AIImages
{
/// <summary>
/// Empty window that opens when clicking the pawn button
/// Окно для просмотра персонажа и генерации AI изображений
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage(
"Style",
@@ -25,29 +25,48 @@ namespace AIImages
public class Window_AIImage : Window
{
private Pawn pawn;
private PawnAppearanceData appearanceData;
private StableDiffusionSettings generationSettings;
private Texture2D generatedImage;
private bool isGenerating = false;
private string generationStatus = "";
public Window_AIImage(Pawn pawn)
{
this.pawn = pawn;
this.doCloseX = true;
this.doCloseButton = true;
this.forcePause = false; // Не ставим игру на паузу
this.absorbInputAroundWindow = false; // Не блокируем клики вне окна
this.draggable = true; // Делаем окно перемещаемым
this.preventCameraMotion = false; // Не блокируем управление камерой
this.forcePause = false;
this.absorbInputAroundWindow = false;
this.draggable = true;
this.preventCameraMotion = false;
// Извлекаем данные персонажа
RefreshPawnData();
}
public override Vector2 InitialSize => new Vector2(700f, 700f);
public override Vector2 InitialSize => new Vector2(900f, 800f);
private Vector2 scrollPosition = Vector2.zero;
private float copiedMessageTime = 0f;
/// <summary>
/// Обновляет данные персонажа
/// </summary>
private void RefreshPawnData()
{
appearanceData = AIImagesMod.PawnDescriptionService.ExtractAppearanceData(pawn);
generationSettings = AIImagesMod.Settings.ToStableDiffusionSettings();
}
/// <summary>
/// Обновляет текущую пешку в окне
/// </summary>
public void UpdatePawn(Pawn newPawn)
{
this.pawn = newPawn;
RefreshPawnData();
}
/// <summary>
@@ -70,7 +89,7 @@ namespace AIImages
// Если выбрана новая колонистская пешка, обновляем окно
if (selectedPawn != null && selectedPawn != pawn)
{
pawn = selectedPawn;
UpdatePawn(selectedPawn);
}
// Уменьшаем таймер сообщения о копировании
@@ -81,274 +100,71 @@ namespace AIImages
}
/// <summary>
/// Получает описание внешности персонажа
/// Асинхронная генерация изображения
/// </summary>
private string GetAppearanceDescription()
private async System.Threading.Tasks.Task GenerateImage()
{
if (pawn?.story == null)
return "AIImages.Appearance.NoInfo".Translate();
if (isGenerating)
return;
StringBuilder sb = new StringBuilder();
isGenerating = true;
generationStatus = "AIImages.Generation.InProgress".Translate();
// Пол
sb.AppendLine("AIImages.Appearance.Gender".Translate(pawn.gender.GetLabel()));
// Возраст
sb.AppendLine("AIImages.Appearance.Age".Translate(pawn.ageTracker.AgeBiologicalYears));
// Тип тела
if (pawn.story.bodyType != null)
try
{
sb.AppendLine(
"AIImages.Appearance.BodyType".Translate(pawn.story.bodyType.defName)
// Генерируем промпты
string positivePrompt = AIImagesMod.PromptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
);
}
// Цвет кожи
if (pawn.story.SkinColor != null)
{
Color skinColor = pawn.story.SkinColor;
sb.AppendLine(
"AIImages.Appearance.SkinColor".Translate(
skinColor.r.ToString("F2"),
skinColor.g.ToString("F2"),
skinColor.b.ToString("F2")
)
string negativePrompt = AIImagesMod.PromptGeneratorService.GenerateNegativePrompt(
generationSettings
);
}
// Волосы
if (pawn.story.hairDef != null)
{
sb.AppendLine("AIImages.Appearance.Hairstyle".Translate(pawn.story.hairDef.label));
if (pawn.story.HairColor != null)
// Создаем запрос
var request = new GenerationRequest
{
sb.AppendLine(
"AIImages.Appearance.HairColor".Translate(
pawn.story.HairColor.r.ToString("F2"),
pawn.story.HairColor.g.ToString("F2"),
pawn.story.HairColor.b.ToString("F2")
)
Prompt = positivePrompt,
NegativePrompt = negativePrompt,
Steps = generationSettings.Steps,
CfgScale = generationSettings.CfgScale,
Width = generationSettings.Width,
Height = generationSettings.Height,
Sampler = generationSettings.Sampler,
Seed = generationSettings.Seed,
Model = AIImagesMod.Settings.apiEndpoint,
};
// Генерируем изображение
var result = await AIImagesMod.ApiService.GenerateImageAsync(request);
if (result.Success)
{
// Загружаем текстуру
generatedImage = new Texture2D(2, 2);
generatedImage.LoadImage(result.ImageData);
generationStatus = "AIImages.Generation.Success".Translate();
Messages.Message(
"AIImages.Generation.SavedTo".Translate(result.SavedPath),
MessageTypeDefOf.PositiveEvent
);
}
}
// Черты характера
if (pawn.story.traits?.allTraits != null && pawn.story.traits.allTraits.Any())
{
sb.AppendLine("\n" + "AIImages.Appearance.Traits".Translate());
foreach (var trait in pawn.story.traits.allTraits)
else
{
sb.AppendLine($" • {trait.LabelCap}");
generationStatus =
$"AIImages.Generation.Failed".Translate() + ": {result.ErrorMessage}";
Messages.Message(generationStatus, MessageTypeDefOf.RejectInput);
}
}
return sb.ToString();
}
/// <summary>
/// Получает описание одежды персонажа
/// </summary>
private string GetApparelDescription()
{
if (pawn?.apparel == null)
return "AIImages.Apparel.NoInfo".Translate();
StringBuilder sb = new StringBuilder();
List<Apparel> wornApparel = pawn.apparel.WornApparel;
if (wornApparel == null || !wornApparel.Any())
catch (Exception ex)
{
sb.AppendLine("AIImages.Apparel.NoClothes".Translate());
generationStatus = $"Error: {ex.Message}";
Log.Error($"[AI Images] Generation error: {ex}");
}
else
finally
{
sb.AppendLine("AIImages.Apparel.ListHeader".Translate(wornApparel.Count) + "\n");
foreach (Apparel apparel in wornApparel)
{
FormatApparelItem(sb, apparel);
}
}
return sb.ToString();
}
/// <summary>
/// Форматирует информацию об одном предмете одежды
/// </summary>
private void FormatApparelItem(StringBuilder sb, Apparel apparel)
{
sb.AppendLine($"• {apparel.LabelCap}");
if (apparel.TryGetQuality(out QualityCategory quality))
{
sb.AppendLine("AIImages.Apparel.Quality".Translate(quality.GetLabel()));
}
if (apparel.Stuff != null)
{
sb.AppendLine("AIImages.Apparel.Material".Translate(apparel.Stuff.LabelCap));
}
if (apparel.HitPoints < apparel.MaxHitPoints)
{
int percentage = (int)((float)apparel.HitPoints / apparel.MaxHitPoints * 100);
sb.AppendLine(
"AIImages.Apparel.Durability".Translate(
apparel.HitPoints,
apparel.MaxHitPoints,
percentage
)
);
}
if (apparel.DrawColor != Color.white)
{
sb.AppendLine(
"AIImages.Apparel.Color".Translate(
apparel.DrawColor.r.ToString("F2"),
apparel.DrawColor.g.ToString("F2"),
apparel.DrawColor.b.ToString("F2")
)
);
}
sb.AppendLine();
}
/// <summary>
/// Генерирует промпт для Stable Diffusion на основе внешности персонажа
/// </summary>
private string GenerateStableDiffusionPrompt()
{
if (pawn?.story == null)
return "portrait of a person";
StringBuilder prompt = new StringBuilder("portrait of a ");
prompt.Append(GetAgeAndGenderDescription());
prompt.Append(GetBodyTypeDescription());
prompt.Append(GetSkinToneDescription());
prompt.Append(GetHairDescription());
prompt.Append(GetApparelPromptDescription());
prompt.Append(
"realistic, detailed, high quality, professional lighting, 8k, photorealistic"
);
return prompt.ToString();
}
private string GetAgeAndGenderDescription()
{
string ageGroup = pawn.ageTracker.AgeBiologicalYears switch
{
< 18 => "young",
< 30 => "young adult",
< 50 => "middle-aged",
_ => "mature",
};
return $"{ageGroup} {pawn.gender.GetLabel()}, ";
}
private string GetBodyTypeDescription()
{
if (pawn.story.bodyType == null)
return "";
string bodyDesc = pawn.story.bodyType.defName.ToLower() switch
{
"thin" => "slender build",
"hulk" => "muscular build",
"fat" => "heavyset build",
_ => "average build",
};
return $"{bodyDesc}, ";
}
private string GetSkinToneDescription()
{
if (pawn.story.SkinColor == null)
return "";
float brightness =
(pawn.story.SkinColor.r + pawn.story.SkinColor.g + pawn.story.SkinColor.b) / 3f;
string skinTone = brightness switch
{
>= 0.8f => "fair skin",
>= 0.6f => "light skin",
>= 0.4f => "olive skin",
>= 0.2f => "brown skin",
_ => "dark skin",
};
return $"{skinTone}, ";
}
private string GetHairDescription()
{
if (pawn.story.hairDef == null)
return "";
string result = $"{pawn.story.hairDef.label.ToLower()} hair";
if (pawn.story.HairColor != null)
{
result += $", {GetColorDescription(pawn.story.HairColor)} hair color";
}
return result + ", ";
}
private string GetApparelPromptDescription()
{
if (pawn.apparel?.WornApparel == null || !pawn.apparel.WornApparel.Any())
return "";
List<string> items = pawn
.apparel.WornApparel.Take(3)
.Select(a =>
a.Stuff != null
? $"{a.Stuff.label.ToLower()} {a.def.label.ToLower()}"
: a.def.label.ToLower()
)
.ToList();
return $"wearing {string.Join(", ", items)}, ";
}
/// <summary>
/// Получает текстовое описание цвета
/// </summary>
private string GetColorDescription(Color color)
{
// Определяем доминирующий цвет
float max = Mathf.Max(color.r, color.g, color.b);
float min = Mathf.Min(color.r, color.g, color.b);
float diff = max - min;
if (diff < 0.1f)
{
// Оттенки серого
return max switch
{
> 0.8f => "white",
> 0.6f => "light gray",
> 0.4f => "gray",
> 0.2f => "dark gray",
_ => "black",
};
}
// Цветные
const float epsilon = 0.001f;
if (Mathf.Abs(color.r - max) < epsilon)
{
return color.g > color.b ? "orange" : "red";
}
else if (Mathf.Abs(color.g - max) < epsilon)
{
return color.r > color.b ? "yellow" : "green";
}
else
{
return color.r > color.g ? "purple" : "blue";
isGenerating = false;
}
}
@@ -376,16 +192,29 @@ namespace AIImages
Widgets.DrawLineHorizontal(0f, curY, inRect.width);
curY += 10f;
// Область для прокрутки контента
Rect scrollRect = new Rect(0f, curY, inRect.width, inRect.height - curY);
Rect scrollViewRect = new Rect(
0f,
0f,
scrollRect.width - 20f,
CalculateContentHeight()
);
// Разделяем на две колонки: левая - информация, правая - изображение
float leftColumnWidth = inRect.width * 0.55f;
float rightColumnWidth = inRect.width * 0.42f;
float columnGap = inRect.width * 0.03f;
Widgets.BeginScrollView(scrollRect, ref scrollPosition, scrollViewRect);
// Левая колонка - прокручиваемая информация
Rect leftColumnRect = new Rect(0f, curY, leftColumnWidth, inRect.height - curY);
DrawLeftColumn(leftColumnRect);
// Правая колонка - превью и управление
Rect rightColumnRect = new Rect(
leftColumnWidth + columnGap,
curY,
rightColumnWidth,
inRect.height - curY
);
DrawRightColumn(rightColumnRect);
}
private void DrawLeftColumn(Rect rect)
{
Rect scrollViewRect = new Rect(0f, 0f, rect.width - 20f, CalculateContentHeight());
Widgets.BeginScrollView(rect, ref scrollPosition, scrollViewRect);
float contentY = 0f;
@@ -398,7 +227,9 @@ namespace AIImages
contentY += 35f;
Text.Font = GameFont.Small;
string appearanceText = GetAppearanceDescription();
string appearanceText = AIImagesMod.PawnDescriptionService.GetAppearanceDescription(
pawn
);
float appearanceHeight = Text.CalcHeight(appearanceText, scrollViewRect.width - 30f);
Widgets.Label(
new Rect(20f, contentY, scrollViewRect.width - 30f, appearanceHeight),
@@ -419,60 +250,133 @@ namespace AIImages
contentY += 35f;
Text.Font = GameFont.Small;
string apparelText = GetApparelDescription();
string apparelText = AIImagesMod.PawnDescriptionService.GetApparelDescription(pawn);
float apparelHeight = Text.CalcHeight(apparelText, scrollViewRect.width - 30f);
Widgets.Label(
new Rect(20f, contentY, scrollViewRect.width - 30f, apparelHeight),
apparelText
);
contentY += apparelHeight + 20f;
// Разделитель
Widgets.DrawLineHorizontal(10f, contentY, scrollViewRect.width - 20f);
contentY += 15f;
Widgets.EndScrollView();
}
// Секция "Stable Diffusion Промпт"
private void DrawRightColumn(Rect rect)
{
float curY = 0f;
// Превью изображения
if (generatedImage != null)
{
float imageSize = Mathf.Min(rect.width, 400f);
Rect imageRect = new Rect(
(rect.width - imageSize) / 2f,
curY,
imageSize,
imageSize
);
GUI.DrawTexture(imageRect, generatedImage);
curY += imageSize + 10f;
}
else
{
// Placeholder для изображения
float placeholderSize = Mathf.Min(rect.width, 300f);
Rect placeholderRect = new Rect(
(rect.width - placeholderSize) / 2f,
curY,
placeholderSize,
placeholderSize
);
Widgets.DrawBoxSolid(placeholderRect, new Color(0.2f, 0.2f, 0.2f));
Text.Anchor = TextAnchor.MiddleCenter;
Widgets.Label(placeholderRect, "AIImages.Generation.NoImage".Translate());
Text.Anchor = TextAnchor.UpperLeft;
curY += placeholderSize + 10f;
}
// Статус генерации
if (!string.IsNullOrEmpty(generationStatus))
{
Text.Font = GameFont.Small;
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
Widgets.Label(new Rect(0f, curY, rect.width, statusHeight), generationStatus);
curY += statusHeight + 10f;
}
// Кнопка генерации
Text.Font = GameFont.Small;
if (
Widgets.ButtonText(
new Rect(0f, curY, rect.width, 35f),
isGenerating
? "AIImages.Generation.Generating".Translate()
: "AIImages.Generation.Generate".Translate()
) && !isGenerating
)
{
_ = GenerateImage();
}
curY += 40f;
// Промпт секция
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(10f, contentY, scrollViewRect.width - 20f, 30f),
new Rect(0f, curY, rect.width, 30f),
"AIImages.Prompt.SectionTitle".Translate()
);
contentY += 35f;
curY += 35f;
// Промпт текст
Text.Font = GameFont.Small;
string promptText = GenerateStableDiffusionPrompt();
float promptHeight = Text.CalcHeight(promptText, scrollViewRect.width - 30f);
Widgets.Label(
new Rect(20f, contentY, scrollViewRect.width - 30f, promptHeight),
promptText
// Получаем промпт
Text.Font = GameFont.Tiny;
string promptText = AIImagesMod.PromptGeneratorService.GeneratePositivePrompt(
appearanceData,
generationSettings
);
contentY += promptHeight + 10f;
// Кнопка копирования
Rect copyButtonRect = new Rect(20f, contentY, 150f, 30f);
float promptHeight = Mathf.Min(Text.CalcHeight(promptText, rect.width), 150f);
Rect promptRect = new Rect(0f, curY, rect.width, promptHeight);
if (Widgets.ButtonText(copyButtonRect, "AIImages.Prompt.CopyButton".Translate()))
// Рисуем промпт в скроллируемой области если он длинный
Widgets.DrawBoxSolid(promptRect, new Color(0.1f, 0.1f, 0.1f, 0.5f));
Widgets.Label(promptRect.ContractedBy(5f), promptText);
curY += promptHeight + 10f;
// Кнопка копирования промпта
if (
Widgets.ButtonText(
new Rect(0f, curY, rect.width / 2f - 5f, 30f),
"AIImages.Prompt.CopyButton".Translate()
)
)
{
GUIUtility.systemCopyBuffer = promptText;
copiedMessageTime = 2f; // Показываем сообщение на 2 секунды
copiedMessageTime = 2f;
}
// Кнопка обновления данных
if (
Widgets.ButtonText(
new Rect(rect.width / 2f + 5f, curY, rect.width / 2f - 5f, 30f),
"AIImages.Window.Refresh".Translate()
)
)
{
RefreshPawnData();
}
// Сообщение о копировании
if (copiedMessageTime > 0f)
{
Rect copiedRect = new Rect(copyButtonRect.xMax + 10f, contentY, 100f, 30f);
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f); // Затухающий зеленый
Widgets.Label(copiedRect, "AIImages.Prompt.Copied".Translate());
curY += 35f;
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
Widgets.Label(
new Rect(0f, curY, rect.width, 25f),
"AIImages.Prompt.Copied".Translate()
);
GUI.color = Color.white;
}
Widgets.EndScrollView();
}
/// <summary>
/// Вычисляет высоту всего контента для прокрутки
/// </summary>
private float CalculateContentHeight()
{
float height = 0f;
@@ -481,8 +385,10 @@ namespace AIImages
height += 35f;
// Текст внешности
string appearanceText = GetAppearanceDescription();
height += Text.CalcHeight(appearanceText, 640f) + 20f;
string appearanceText = AIImagesMod.PawnDescriptionService.GetAppearanceDescription(
pawn
);
height += Text.CalcHeight(appearanceText, 400f) + 20f;
// Разделитель
height += 15f;
@@ -491,21 +397,11 @@ namespace AIImages
height += 35f;
// Текст одежды
string apparelText = GetApparelDescription();
height += Text.CalcHeight(apparelText, 640f) + 20f;
string apparelText = AIImagesMod.PawnDescriptionService.GetApparelDescription(pawn);
height += Text.CalcHeight(apparelText, 400f) + 20f;
// Разделитель
height += 15f;
// Заголовок "Промпт"
height += 35f;
// Текст промпта
string promptText = GenerateStableDiffusionPrompt();
height += Text.CalcHeight(promptText, 640f) + 10f;
// Кнопка и отступ
height += 30f + 20f;
// Дополнительный отступ
height += 50f;
return height;
}