Refactor AIImages UI layout for improved positioning and consistency. Remove ShotType settings and related code to simplify prompt generation. Update models to include new properties for hair and apparel definitions, enhancing data handling for character appearance.

This commit is contained in:
Leonid Pershin
2025-10-26 18:23:40 +03:00
parent 0f60721162
commit 6715544952
8 changed files with 100 additions and 99 deletions

Binary file not shown.

View File

@@ -16,6 +16,7 @@ namespace AIImages.Models
public string BodyType { get; set; }
public Color SkinColor { get; set; }
public string HairStyle { get; set; }
public string HairDefName { get; set; }
public Color HairColor { get; set; }
public List<Trait> Traits { get; set; }
public List<ApparelData> Apparel { get; set; }
@@ -33,7 +34,9 @@ namespace AIImages.Models
public class ApparelData
{
public string Label { get; set; }
public string DefName { get; set; }
public string Material { get; set; }
public string MaterialDefName { get; set; }
public QualityCategory? Quality { get; set; }
public Color Color { get; set; }
public string LayerType { get; set; }

View File

@@ -15,7 +15,6 @@ namespace AIImages.Models
public int Seed { get; set; }
public string Model { get; set; }
public ArtStyle ArtStyle { get; set; }
public ShotType ShotType { get; set; }
public StableDiffusionSettings()
{
@@ -27,7 +26,6 @@ namespace AIImages.Models
Sampler = "Euler a";
Seed = -1; // Случайный seed
ArtStyle = ArtStyle.Realistic;
ShotType = ShotType.Portrait;
PositivePrompt = "";
NegativePrompt = "ugly, deformed, low quality, blurry, bad anatomy, worst quality";
}
@@ -38,6 +36,7 @@ namespace AIImages.Models
/// </summary>
public enum ArtStyle
{
None, // Без стиля
Realistic,
SemiRealistic,
Anime,
@@ -47,16 +46,4 @@ namespace AIImages.Models
Sketch,
CellShaded,
}
/// <summary>
/// Тип кадра/композиции
/// </summary>
public enum ShotType
{
Portrait, // Портрет (голова и плечи)
HalfBody, // Половина тела
FullBody, // Полное тело
CloseUp, // Крупный план
ThreeQuarter, // Три четверти
}
}

View File

@@ -39,6 +39,7 @@ namespace AIImages.Services
string
>
{
{ ArtStyle.None, "" },
{ ArtStyle.Realistic, "photorealistic, hyperrealistic, realistic photo, photography" },
{ ArtStyle.SemiRealistic, "semi-realistic, detailed illustration, realistic art" },
{ ArtStyle.Anime, "anime style, manga style, anime character" },
@@ -52,18 +53,6 @@ namespace AIImages.Services
{ 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
@@ -75,18 +64,17 @@ namespace AIImages.Services
StringBuilder prompt = new StringBuilder();
// 1. Художественный стиль
if (ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt))
if (
ArtStylePrompts.TryGetValue(settings.ArtStyle, out string stylePrompt)
&& !string.IsNullOrEmpty(stylePrompt)
)
{
prompt.Append(stylePrompt);
prompt.Append(", ");
}
// 2. Тип кадра
if (ShotTypePrompts.TryGetValue(settings.ShotType, out string shotPrompt))
{
prompt.Append(shotPrompt);
prompt.Append(" of ");
}
// 2. Тип кадра - автоматически добавляем "portrait" для генерации персонажей
prompt.Append("portrait, head and shoulders of ");
// 3. Базовое описание (возраст и пол)
prompt.Append(GetAgeAndGenderDescription(appearanceData));
@@ -157,16 +145,18 @@ namespace AIImages.Services
);
// Специфичные для стиля негативы
if (
settings.ArtStyle == ArtStyle.Realistic
|| settings.ArtStyle == ArtStyle.SemiRealistic
)
switch (settings.ArtStyle)
{
case ArtStyle.Realistic:
case ArtStyle.SemiRealistic:
negativePrompt.Append("cartoon, anime, painting, drawing, illustration, ");
}
else if (settings.ArtStyle == ArtStyle.Anime)
{
break;
case ArtStyle.Anime:
negativePrompt.Append("realistic, photo, photography, 3d, ");
break;
case ArtStyle.None:
// Без дополнительных негативных промптов для стиля None
break;
}
// Пользовательский негативный промпт
@@ -244,7 +234,7 @@ namespace AIImages.Services
private string GetHairDescription(PawnAppearanceData data)
{
if (string.IsNullOrEmpty(data.HairStyle))
if (string.IsNullOrEmpty(data.HairDefName))
return "";
StringBuilder hair = new StringBuilder();
@@ -254,10 +244,8 @@ namespace AIImages.Services
hair.Append(hairColor);
hair.Append(" ");
// Стиль прически (упрощаем сложные названия)
string style = data
.HairStyle.ToLower()
.Replace("_", " ")
// Стиль прически - используем DefName для английского названия
string style = CleanDefName(data.HairDefName)
.Replace("shaved", "very short")
.Replace("mohawk", "mohawk hairstyle");
@@ -310,15 +298,15 @@ namespace AIImages.Services
itemDesc.Append(" ");
}
// Материал
if (!string.IsNullOrEmpty(item.Material))
// Материал - используем DefName для английского названия
if (!string.IsNullOrEmpty(item.MaterialDefName))
{
itemDesc.Append(item.Material.ToLower());
itemDesc.Append(CleanDefName(item.MaterialDefName));
itemDesc.Append(" ");
}
// Название предмета
itemDesc.Append(item.Label.ToLower());
// Название предмета - используем DefName для английского названия
itemDesc.Append(CleanDefName(item.DefName));
items.Add(itemDesc.ToString());
}
@@ -332,22 +320,56 @@ namespace AIImages.Services
{
var baseTags = "highly detailed, professional, masterpiece, best quality";
if (style == ArtStyle.Realistic || style == ArtStyle.SemiRealistic)
switch (style)
{
case ArtStyle.None:
return baseTags;
case ArtStyle.Realistic:
case ArtStyle.SemiRealistic:
return $"{baseTags}, professional photography, 8k uhd, dslr, high quality, sharp focus";
}
else if (style == ArtStyle.Anime)
{
case ArtStyle.Anime:
return $"{baseTags}, anime masterpiece, high resolution, vibrant colors";
}
else if (style == ArtStyle.ConceptArt)
{
case ArtStyle.ConceptArt:
return $"{baseTags}, trending on artstation, professional digital art";
}
else
{
default:
return baseTags;
}
}
/// <summary>
/// Преобразует defName в читаемый английский текст для промпта
/// Пример: "Apparel_Pants" -> "pants", "Synthread" -> "synthread"
/// </summary>
private string CleanDefName(string defName)
{
if (string.IsNullOrEmpty(defName))
return "";
string cleaned = defName;
// Убираем распространенные префиксы RimWorld
cleaned = System.Text.RegularExpressions.Regex.Replace(
cleaned,
"^(Apparel_|Armor_|Weapon_|Thing_)",
"",
System.Text.RegularExpressions.RegexOptions.IgnoreCase
);
// Разделяем CamelCase на слова
cleaned = System.Text.RegularExpressions.Regex.Replace(
cleaned,
"([a-z])([A-Z])",
"$1 $2"
);
// Заменяем подчеркивания на пробелы
cleaned = cleaned.Replace("_", " ");
// Убираем множественные пробелы
cleaned = System.Text.RegularExpressions.Regex.Replace(cleaned, @"\s+", " ");
// Приводим к нижнему регистру и убираем лишние пробелы
return cleaned.Trim().ToLower();
}
}
}

View File

@@ -25,6 +25,7 @@ namespace AIImages.Services
BodyType = pawn.story.bodyType?.defName,
SkinColor = pawn.story.SkinColor,
HairStyle = pawn.story.hairDef?.label,
HairDefName = pawn.story.hairDef?.defName,
HairColor = pawn.story.HairColor,
};
@@ -42,7 +43,9 @@ namespace AIImages.Services
var apparelData = new ApparelData
{
Label = apparel.def.label,
DefName = apparel.def.defName,
Material = apparel.Stuff?.label,
MaterialDefName = apparel.Stuff?.defName,
Color = apparel.DrawColor,
LayerType = apparel.def.apparel?.LastLayer.ToString(),
Durability = apparel.HitPoints,

View File

@@ -28,7 +28,6 @@ namespace AIImages.Settings
// Художественный стиль
public ArtStyle artStyle = ArtStyle.Realistic;
public ShotType shotType = ShotType.Portrait;
// Путь для сохранения
public string savePath = "AIImages/Generated";
@@ -58,7 +57,6 @@ namespace AIImages.Settings
);
Scribe_Values.Look(ref artStyle, "artStyle", ArtStyle.Realistic);
Scribe_Values.Look(ref shotType, "shotType", ShotType.Portrait);
Scribe_Values.Look(ref savePath, "savePath", "AIImages/Generated");
@@ -84,7 +82,6 @@ namespace AIImages.Settings
Seed = seed,
Model = selectedModel,
ArtStyle = artStyle,
ShotType = shotType,
PositivePrompt = basePositivePrompt,
NegativePrompt = baseNegativePrompt,
};

View File

@@ -88,25 +88,6 @@ namespace AIImages
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

View File

@@ -269,8 +269,8 @@ namespace AIImages
{
float imageSize = Mathf.Min(rect.width, 400f);
Rect imageRect = new Rect(
(rect.width - imageSize) / 2f,
curY,
rect.x + (rect.width - imageSize) / 2f,
rect.y + curY,
imageSize,
imageSize
);
@@ -282,8 +282,8 @@ namespace AIImages
// Placeholder для изображения
float placeholderSize = Mathf.Min(rect.width, 300f);
Rect placeholderRect = new Rect(
(rect.width - placeholderSize) / 2f,
curY,
rect.x + (rect.width - placeholderSize) / 2f,
rect.y + curY,
placeholderSize,
placeholderSize
);
@@ -299,7 +299,10 @@ namespace AIImages
{
Text.Font = GameFont.Small;
float statusHeight = Text.CalcHeight(generationStatus, rect.width);
Widgets.Label(new Rect(0f, curY, rect.width, statusHeight), generationStatus);
Widgets.Label(
new Rect(rect.x, rect.y + curY, rect.width, statusHeight),
generationStatus
);
curY += statusHeight + 10f;
}
@@ -307,7 +310,7 @@ namespace AIImages
Text.Font = GameFont.Small;
if (
Widgets.ButtonText(
new Rect(0f, curY, rect.width, 35f),
new Rect(rect.x, rect.y + curY, rect.width, 35f),
isGenerating
? "AIImages.Generation.Generating".Translate()
: "AIImages.Generation.Generate".Translate()
@@ -321,7 +324,7 @@ namespace AIImages
// Промпт секция
Text.Font = GameFont.Medium;
Widgets.Label(
new Rect(0f, curY, rect.width, 30f),
new Rect(rect.x, rect.y + curY, rect.width, 30f),
"AIImages.Prompt.SectionTitle".Translate()
);
curY += 35f;
@@ -334,7 +337,7 @@ namespace AIImages
);
float promptHeight = Mathf.Min(Text.CalcHeight(promptText, rect.width), 150f);
Rect promptRect = new Rect(0f, curY, rect.width, promptHeight);
Rect promptRect = new Rect(rect.x, rect.y + curY, rect.width, promptHeight);
// Рисуем промпт в скроллируемой области если он длинный
Widgets.DrawBoxSolid(promptRect, new Color(0.1f, 0.1f, 0.1f, 0.5f));
@@ -344,7 +347,7 @@ namespace AIImages
// Кнопка копирования промпта
if (
Widgets.ButtonText(
new Rect(0f, curY, rect.width / 2f - 5f, 30f),
new Rect(rect.x, rect.y + curY, rect.width / 2f - 5f, 30f),
"AIImages.Prompt.CopyButton".Translate()
)
)
@@ -356,7 +359,12 @@ namespace AIImages
// Кнопка обновления данных
if (
Widgets.ButtonText(
new Rect(rect.width / 2f + 5f, curY, rect.width / 2f - 5f, 30f),
new Rect(
rect.x + rect.width / 2f + 5f,
rect.y + curY,
rect.width / 2f - 5f,
30f
),
"AIImages.Window.Refresh".Translate()
)
)
@@ -370,7 +378,7 @@ namespace AIImages
curY += 35f;
GUI.color = new Color(0f, 1f, 0f, copiedMessageTime / 2f);
Widgets.Label(
new Rect(0f, curY, rect.width, 25f),
new Rect(rect.x, rect.y + curY, rect.width, 25f),
"AIImages.Prompt.Copied".Translate()
);
GUI.color = Color.white;