Initial Dansori character workspace
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(python -c ' *)",
|
||||
"PowerShell(dotnet build *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Character_Builder;
|
||||
|
||||
/// <summary>
|
||||
/// Alpha-based analysis for auto-alignment: opaque bounding box + neck anchors
|
||||
/// (top-edge center for a headless body, bottom-edge center for a head).
|
||||
/// </summary>
|
||||
public static class AlphaTools
|
||||
{
|
||||
public class Analysis
|
||||
{
|
||||
public int W, H;
|
||||
public int MinX, MinY, MaxX, MaxY;
|
||||
public double TopCenterX; // center X of opaque pixels along the top band
|
||||
public double BottomCenterX; // center X of opaque pixels along the bottom band
|
||||
public int AxisX; // torso central axis (robust neck X; ignores raised arms)
|
||||
public int NeckTop; // topmost row of the central column (robust neck Y)
|
||||
public int BH => MaxY - MinY + 1;
|
||||
public int BW => MaxX - MinX + 1;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, Analysis?> _cache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private const byte AlphaThreshold = 24;
|
||||
|
||||
public static Analysis? Analyze(string path)
|
||||
{
|
||||
if (_cache.TryGetValue(path, out var cached)) return cached;
|
||||
Analysis? a = null;
|
||||
try { a = Compute(path); } catch { a = null; }
|
||||
_cache[path] = a;
|
||||
return a;
|
||||
}
|
||||
|
||||
private static Analysis? Compute(string path)
|
||||
{
|
||||
var src = AssetScanner.LoadPart(path);
|
||||
if (src == null) return null;
|
||||
BitmapSource conv = src.Format == PixelFormats.Bgra32 ? src : new FormatConvertedBitmap(src, PixelFormats.Bgra32, null, 0);
|
||||
int w = conv.PixelWidth, h = conv.PixelHeight;
|
||||
if (w <= 0 || h <= 0) return null;
|
||||
int stride = w * 4;
|
||||
var px = new byte[h * stride];
|
||||
conv.CopyPixels(px, stride, 0);
|
||||
|
||||
int minX = int.MaxValue, minY = int.MaxValue, maxX = -1, maxY = -1;
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
int row = y * stride;
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
if (px[row + x * 4 + 3] >= AlphaThreshold)
|
||||
{
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxX < 0) return null; // fully transparent
|
||||
|
||||
int bh = maxY - minY + 1;
|
||||
int band = Math.Max(1, (int)(bh * 0.06));
|
||||
|
||||
double topSum = 0; long topCount = 0;
|
||||
for (int y = minY; y < Math.Min(h, minY + band); y++)
|
||||
{
|
||||
int row = y * stride;
|
||||
for (int x = minX; x <= maxX; x++)
|
||||
if (px[row + x * 4 + 3] >= AlphaThreshold) { topSum += x; topCount++; }
|
||||
}
|
||||
double bottomSum = 0; long bottomCount = 0;
|
||||
for (int y = Math.Max(0, maxY - band + 1); y <= maxY; y++)
|
||||
{
|
||||
int row = y * stride;
|
||||
for (int x = minX; x <= maxX; x++)
|
||||
if (px[row + x * 4 + 3] >= AlphaThreshold) { bottomSum += x; bottomCount++; }
|
||||
}
|
||||
|
||||
// Torso central axis: median opaque column in the mid-lower body (waist/legs are centered
|
||||
// and free of raised arms), giving a neck-X that is stable across gesture poses.
|
||||
int ta0 = minY + (int)(bh * 0.40), ta1 = Math.Min(maxY, minY + (int)(bh * 0.85));
|
||||
var colCount = new int[w];
|
||||
long axisTotal = 0;
|
||||
for (int y = ta0; y <= ta1; y++)
|
||||
{
|
||||
int row = y * stride;
|
||||
for (int x = minX; x <= maxX; x++)
|
||||
if (px[row + x * 4 + 3] >= AlphaThreshold) { colCount[x]++; axisTotal++; }
|
||||
}
|
||||
int axisX = (minX + maxX) / 2;
|
||||
if (axisTotal > 0)
|
||||
{
|
||||
long half = axisTotal / 2, acc = 0;
|
||||
for (int x = 0; x < w; x++) { acc += colCount[x]; if (acc >= half) { axisX = x; break; } }
|
||||
}
|
||||
|
||||
// Neck top: first row (from the top) whose central column — the opaque run around the
|
||||
// axis — is solid (>=15px). Skips raised hands/arms that sit above the real neck.
|
||||
int neckTop = minY;
|
||||
for (int y = minY; y < minY + (int)(bh * 0.5); y++)
|
||||
{
|
||||
if (CentralRunWidth(px, stride, y, w, axisX) >= 15) { neckTop = y; break; }
|
||||
}
|
||||
|
||||
// Precise neck via the bare-skin neck stump: the head's neck and the body's neck stump are
|
||||
// the same anatomical part, so matching this locks the assembly. The stump is the central
|
||||
// skin column at the top of a headless body; detecting it here gives a neck centre/top that
|
||||
// is robust to raised arms, open jackets and asymmetric poses. Falls back to the alpha
|
||||
// estimate above when no skin stump is present.
|
||||
int bboxW = maxX - minX + 1, seedX = (minX + maxX) / 2;
|
||||
for (int y = minY; y < minY + (int)(bh * 0.5); y++)
|
||||
{
|
||||
var (lo, hi, rw) = CentralSkinRun(px, stride, y, w, seedX);
|
||||
if (rw >= 30 && rw <= bboxW / 2) { axisX = (lo + hi) / 2; neckTop = y; break; }
|
||||
}
|
||||
|
||||
double cx = (minX + maxX) / 2.0;
|
||||
return new Analysis
|
||||
{
|
||||
W = w,
|
||||
H = h,
|
||||
MinX = minX,
|
||||
MinY = minY,
|
||||
MaxX = maxX,
|
||||
MaxY = maxY,
|
||||
AxisX = axisX,
|
||||
NeckTop = neckTop,
|
||||
TopCenterX = topCount > 0 ? topSum / topCount : cx,
|
||||
BottomCenterX = bottomCount > 0 ? bottomSum / bottomCount : cx,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Width of the opaque run that straddles <paramref name="axisX"/> on row y (0 if none).</summary>
|
||||
private static int CentralRunWidth(byte[] px, int stride, int y, int w, int axisX)
|
||||
{
|
||||
int row = y * stride, seed = -1;
|
||||
for (int d = 0; d <= 8 && seed < 0; d++)
|
||||
{
|
||||
if (axisX + d < w && px[row + (axisX + d) * 4 + 3] >= AlphaThreshold) seed = axisX + d;
|
||||
else if (axisX - d >= 0 && px[row + (axisX - d) * 4 + 3] >= AlphaThreshold) seed = axisX - d;
|
||||
}
|
||||
if (seed < 0) return 0;
|
||||
int lo = seed, hi = seed;
|
||||
while (lo - 1 >= 0 && px[row + (lo - 1) * 4 + 3] >= AlphaThreshold) lo--;
|
||||
while (hi + 1 < w && px[row + (hi + 1) * 4 + 3] >= AlphaThreshold) hi++;
|
||||
return hi - lo + 1;
|
||||
}
|
||||
|
||||
private static bool IsSkin(byte b, byte g, byte r)
|
||||
=> r > 150 && r >= g && g >= b - 8 && (r - b) >= 15 && (r - b) <= 130 && (g - b) <= 70;
|
||||
|
||||
/// <summary>The bare-skin run straddling <paramref name="cx"/> on row y (searches ±60px for the seed).</summary>
|
||||
private static (int lo, int hi, int w) CentralSkinRun(byte[] px, int stride, int y, int w, int cx)
|
||||
{
|
||||
int row = y * stride, seed = -1;
|
||||
for (int d = 0; d <= 60 && seed < 0; d++)
|
||||
{
|
||||
int xr = cx + d, xl = cx - d;
|
||||
if (xr < w && IsSkin(px[row + xr * 4], px[row + xr * 4 + 1], px[row + xr * 4 + 2])) seed = xr;
|
||||
else if (xl >= 0 && IsSkin(px[row + xl * 4], px[row + xl * 4 + 1], px[row + xl * 4 + 2])) seed = xl;
|
||||
}
|
||||
if (seed < 0) return (0, 0, 0);
|
||||
int lo = seed, hi = seed;
|
||||
while (lo - 1 >= 0 && IsSkin(px[row + (lo - 1) * 4], px[row + (lo - 1) * 4 + 1], px[row + (lo - 1) * 4 + 2])) lo--;
|
||||
while (hi + 1 < w && IsSkin(px[row + (hi + 1) * 4], px[row + (hi + 1) * 4 + 1], px[row + (hi + 1) * 4 + 2])) hi++;
|
||||
return (lo, hi, hi - lo + 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
<Application x:Class="Character_Builder.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<SolidColorBrush x:Key="Bg" Color="#14151A"/>
|
||||
<SolidColorBrush x:Key="Panel" Color="#1E2028"/>
|
||||
<SolidColorBrush x:Key="Panel2" Color="#262933"/>
|
||||
<SolidColorBrush x:Key="Stroke" Color="#343845"/>
|
||||
<SolidColorBrush x:Key="Accent" Color="#4CC2FF"/>
|
||||
<SolidColorBrush x:Key="AccentDk" Color="#04222F"/>
|
||||
<SolidColorBrush x:Key="Text" Color="#EDEFF2"/>
|
||||
<SolidColorBrush x:Key="Dim" Color="#A8ADB8"/>
|
||||
<SolidColorBrush x:Key="Mute" Color="#6E7480"/>
|
||||
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="18"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Label" TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource Dim}"/>
|
||||
<Setter Property="FontSize" Value="17"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Margin" Value="0,16,0,6"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="Card" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource Panel}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Stroke}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
</Style>
|
||||
|
||||
<!-- Button (subtle) -->
|
||||
<Style TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}"/>
|
||||
<Setter Property="Background" Value="{StaticResource Panel2}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Stroke}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Height" Value="46"/>
|
||||
<Setter Property="Padding" Value="18,0"/>
|
||||
<Setter Property="FontSize" Value="19"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="b" Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="8" Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="b" Property="BorderBrush" Value="{StaticResource Accent}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="b" Property="Opacity" Value="0.75"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="b" Property="Opacity" Value="0.4"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Window caption (title bar) buttons -->
|
||||
<Style x:Key="CaptionButton" TargetType="Button">
|
||||
<Setter Property="Width" Value="50"/>
|
||||
<Setter Property="Height" Value="36"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource Dim}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="FontFamily" Value="Segoe MDL2 Assets"/>
|
||||
<Setter Property="FontSize" Value="10"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="cb" Background="{TemplateBinding Background}" CornerRadius="6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="cb" Property="Background" Value="{StaticResource Panel2}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CaptionClose" TargetType="Button" BasedOn="{StaticResource CaptionButton}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="cb" Background="{TemplateBinding Background}" CornerRadius="6">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="cb" Property="Background" Value="#E23B3B"/>
|
||||
<Setter Property="Foreground" Value="White"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="AccentButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||
<Setter Property="Background" Value="{StaticResource Accent}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource AccentDk}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FontWeight" Value="Bold"/>
|
||||
</Style>
|
||||
|
||||
<!-- Dark ComboBox: readable light text on both the closed box and the dropdown -->
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="19"/>
|
||||
<Setter Property="Padding" Value="12,8"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBoxItem">
|
||||
<Border x:Name="ib" Background="Transparent" CornerRadius="6"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsHighlighted" Value="True">
|
||||
<Setter TargetName="ib" Property="Background" Value="#12344A"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="ib" Property="Background" Value="#173B52"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ComboToggle" TargetType="ToggleButton">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ToggleButton">
|
||||
<Border Background="Transparent"/>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Foreground" Value="{StaticResource Text}"/>
|
||||
<Setter Property="Background" Value="{StaticResource Panel2}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource Stroke}"/>
|
||||
<Setter Property="Height" Value="44"/>
|
||||
<Setter Property="Padding" Value="12,0"/>
|
||||
<Setter Property="FontSize" Value="19"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI"/>
|
||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<Border x:Name="box" Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1"
|
||||
CornerRadius="8"/>
|
||||
<ToggleButton Style="{StaticResource ComboToggle}"
|
||||
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"/>
|
||||
<ContentPresenter x:Name="sel" Margin="10,0,30,0"
|
||||
Content="{TemplateBinding SelectionBoxItem}"
|
||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||
IsHitTestVisible="False"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Center"
|
||||
TextElement.Foreground="{StaticResource Text}"/>
|
||||
<Path x:Name="arrow" Data="M0,0 L8,0 L4,5 Z" Fill="{StaticResource Dim}"
|
||||
HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,12,0"
|
||||
IsHitTestVisible="False"/>
|
||||
<Popup x:Name="PART_Popup" Placement="Bottom" AllowsTransparency="True"
|
||||
IsOpen="{TemplateBinding IsDropDownOpen}" Focusable="False"
|
||||
PopupAnimation="Slide">
|
||||
<Border Background="{StaticResource Panel}" BorderBrush="{StaticResource Stroke}"
|
||||
BorderThickness="1" CornerRadius="8" Margin="0,4,0,0"
|
||||
MinWidth="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Padding="4">
|
||||
<ScrollViewer MaxHeight="320" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="box" Property="BorderBrush" Value="{StaticResource Accent}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="box" Property="Opacity" Value="0.5"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="CheckBox">
|
||||
<Setter Property="Foreground" Value="{StaticResource Dim}"/>
|
||||
<Setter Property="FontSize" Value="19"/>
|
||||
<Setter Property="Margin" Value="0,5"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI Variable, Segoe UI"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -0,0 +1,16 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Character_Builder;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public App()
|
||||
{
|
||||
DispatcherUnhandledException += (_, e) =>
|
||||
{
|
||||
MessageBox.Show(e.Exception.Message, "오류", MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
e.Handled = true;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Character_Builder;
|
||||
|
||||
/// <summary>Finds the Characters_Build_Docs root, lists characters, and scans their part PNGs.</summary>
|
||||
public static class AssetScanner
|
||||
{
|
||||
/// <summary>Walk up from the exe to find the "Characters_Build_Docs" folder.</summary>
|
||||
public static string? FindRoot()
|
||||
{
|
||||
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
for (int i = 0; i < 10 && dir != null; i++)
|
||||
{
|
||||
if (string.Equals(dir.Name, "Characters_Build_Docs", StringComparison.OrdinalIgnoreCase))
|
||||
return dir.FullName;
|
||||
// also: a child named Characters_Build_Docs
|
||||
var child = Path.Combine(dir.FullName, "Characters_Build_Docs");
|
||||
if (Directory.Exists(child)) return child;
|
||||
dir = dir.Parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>Character = a subfolder that contains a Reference/ folder.</summary>
|
||||
public static List<CharacterInfo> GetCharacters(string root)
|
||||
{
|
||||
var list = new List<CharacterInfo>();
|
||||
if (!Directory.Exists(root)) return list;
|
||||
foreach (var d in Directory.GetDirectories(root))
|
||||
{
|
||||
var refDir = Path.Combine(d, "Reference");
|
||||
if (!Directory.Exists(refDir)) continue;
|
||||
var name = Path.GetFileName(d);
|
||||
var sheet = PickSheet(refDir, name);
|
||||
list.Add(new CharacterInfo
|
||||
{
|
||||
Name = name,
|
||||
FolderPath = d,
|
||||
SheetPath = sheet,
|
||||
Thumb = LoadBitmap(sheet)
|
||||
});
|
||||
}
|
||||
return list.OrderBy(c => c.Name).ToList();
|
||||
}
|
||||
|
||||
private static string? PickSheet(string refDir, string charName)
|
||||
{
|
||||
var sheets = Directory.GetFiles(refDir, "*_sheet.png");
|
||||
if (sheets.Length == 0) return Directory.GetFiles(refDir, "*.png").FirstOrDefault();
|
||||
bool duo = charName.Contains("and", StringComparison.OrdinalIgnoreCase)
|
||||
|| charName.Contains("Duo", StringComparison.OrdinalIgnoreCase)
|
||||
|| charName.Contains("&");
|
||||
// duo -> prefer combined; single -> prefer non-combined
|
||||
var ordered = sheets.OrderBy(s =>
|
||||
{
|
||||
bool combined = Path.GetFileName(s).Contains("combined", StringComparison.OrdinalIgnoreCase);
|
||||
return duo ? (combined ? 0 : 1) : (combined ? 1 : 0);
|
||||
}).ThenBy(s => s);
|
||||
return ordered.First();
|
||||
}
|
||||
|
||||
/// <summary>Scan every PNG under the character's Images/ folders.</summary>
|
||||
public static List<PartItem> GetParts(string charFolder)
|
||||
{
|
||||
var result = new List<PartItem>();
|
||||
if (!Directory.Exists(charFolder)) return result;
|
||||
var pngs = Directory.GetFiles(charFolder, "*.png", SearchOption.AllDirectories)
|
||||
.Where(p => p.Replace('\\', '/').Contains("/Images/"))
|
||||
.OrderBy(p => p);
|
||||
foreach (var p in pngs)
|
||||
{
|
||||
var fn = Path.GetFileName(p);
|
||||
var stem = Path.GetFileNameWithoutExtension(fn);
|
||||
var item = new PartItem { FileName = fn, FilePath = p, Display = stem };
|
||||
var low = stem.ToLowerInvariant();
|
||||
|
||||
if (low.Contains("_hairmask_"))
|
||||
{
|
||||
item.Shape = AfterToken(stem, "hairmask");
|
||||
}
|
||||
else if (low.Contains("_head_"))
|
||||
{
|
||||
var parts = stem.Split('_');
|
||||
int hi = Array.FindIndex(parts, t => t.Equals("head", StringComparison.OrdinalIgnoreCase));
|
||||
if (hi >= 0 && hi + 1 < parts.Length)
|
||||
{
|
||||
item.Shape = parts[hi + 1];
|
||||
item.Expr = (hi + 2 < parts.Length) ? string.Join("_", parts.Skip(hi + 2)) : "(base)";
|
||||
}
|
||||
}
|
||||
result.Add(item);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string AfterToken(string stem, string token)
|
||||
{
|
||||
var parts = stem.Split('_');
|
||||
int i = Array.FindIndex(parts, t => t.Equals(token, StringComparison.OrdinalIgnoreCase));
|
||||
return (i >= 0 && i + 1 < parts.Length) ? string.Join("_", parts.Skip(i + 1)) : "";
|
||||
}
|
||||
|
||||
public static BitmapImage? LoadBitmap(string? path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null;
|
||||
try
|
||||
{
|
||||
var b = new BitmapImage();
|
||||
b.BeginInit();
|
||||
b.CacheOption = BitmapCacheOption.OnLoad;
|
||||
b.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
|
||||
b.UriSource = new Uri(path);
|
||||
b.EndInit();
|
||||
b.Freeze();
|
||||
return b;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, BitmapSource?> _partCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Load a part PNG with its baked-in background keyed out to transparency (cached).</summary>
|
||||
public static BitmapSource? LoadPart(string? path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path)) return null;
|
||||
if (_partCache.TryGetValue(path, out var cached)) return cached;
|
||||
var raw = LoadBitmap(path);
|
||||
BitmapSource? keyed = raw;
|
||||
bool isHead = Path.GetFileName(path).ToLowerInvariant().Contains("_head_");
|
||||
try { if (raw != null) keyed = BgKey.KeyOut(raw, isHead); } catch { keyed = raw; }
|
||||
_partCache[path] = keyed;
|
||||
return keyed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
namespace Character_Builder;
|
||||
|
||||
/// <summary>
|
||||
/// Many part PNGs are exported as opaque RGB with a "fake transparency" checkerboard
|
||||
/// (two light grays ~#FEFEFE / ~#F1F1F1) baked into the pixels. That opaque background
|
||||
/// hides layers below it and breaks alpha-based neck detection.
|
||||
///
|
||||
/// KeyOut() removes the background by flood-filling from the image borders, so only the
|
||||
/// background that is *connected to the edge* is cleared — interior white clothing (crop
|
||||
/// top, hoodie) is kept. If the source already has real transparency, it is returned as-is.
|
||||
/// </summary>
|
||||
public static class BgKey
|
||||
{
|
||||
// A pixel counts as background when it is light and near-neutral (low saturation).
|
||||
private const int BgMinBrightness = 208; // min channel value
|
||||
private const int BgMaxSpread = 26; // max(channel) - min(channel)
|
||||
|
||||
// Boundary feather: soften light fringe pixels that touch cleared background.
|
||||
private const int FeatherLo = 200; // fully opaque below this brightness
|
||||
private const int FeatherHi = 250; // fully transparent at/above this brightness
|
||||
|
||||
public static BitmapSource KeyOut(BitmapSource src, bool isHead = false)
|
||||
{
|
||||
BitmapSource conv = src.Format == PixelFormats.Bgra32
|
||||
? src
|
||||
: new FormatConvertedBitmap(src, PixelFormats.Bgra32, null, 0);
|
||||
|
||||
int w = conv.PixelWidth, h = conv.PixelHeight;
|
||||
if (w <= 0 || h <= 0) return src;
|
||||
|
||||
int stride = w * 4;
|
||||
var px = new byte[h * stride];
|
||||
conv.CopyPixels(px, stride, 0);
|
||||
|
||||
// Already has real transparency? Leave it alone.
|
||||
long transparent = 0;
|
||||
for (int i = 3; i < px.Length; i += 4)
|
||||
if (px[i] < 16) { if (++transparent > (long)w * h / 50) break; }
|
||||
if (transparent > (long)w * h / 50) return src;
|
||||
|
||||
var seen = new bool[w * h];
|
||||
var stack = new int[w * h];
|
||||
int sp = 0;
|
||||
|
||||
// Seed the flood fill from every border pixel that looks like background.
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
TryPush(px, stride, seen, stack, ref sp, x, 0, w);
|
||||
TryPush(px, stride, seen, stack, ref sp, x, h - 1, w);
|
||||
}
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
TryPush(px, stride, seen, stack, ref sp, 0, y, w);
|
||||
TryPush(px, stride, seen, stack, ref sp, w - 1, y, w);
|
||||
}
|
||||
|
||||
// Flood fill (4-connected) over border-connected background; clear its alpha.
|
||||
while (sp > 0)
|
||||
{
|
||||
int idx = stack[--sp];
|
||||
int x = idx % w, y = idx / w;
|
||||
px[idx * 4 + 3] = 0; // transparent
|
||||
|
||||
TryPush(px, stride, seen, stack, ref sp, x - 1, y, w);
|
||||
TryPush(px, stride, seen, stack, ref sp, x + 1, y, w);
|
||||
TryPush(px, stride, seen, stack, ref sp, x, y - 1, w);
|
||||
TryPush(px, stride, seen, stack, ref sp, x, y + 1, w);
|
||||
}
|
||||
|
||||
// The baked "transparency" is a two-tone light-gray checkerboard. Loops/rings (necklace,
|
||||
// bracelet, headphone band) enclose background pockets the border fill can't reach; remove
|
||||
// them by detecting the checker pattern (both tones present, no dark object pixels nearby).
|
||||
RemoveCheckerPockets(px, w, h, stride);
|
||||
|
||||
// Head portraits include a white shirt/shoulders below the neck; that interior white
|
||||
// is not border-connected, so it survives the flood fill and looks like an un-keyed
|
||||
// white patch once the head is layered on a body. Drop light interior pixels in the
|
||||
// lower part of the head silhouette (below the face, so eyes/teeth are untouched).
|
||||
if (isHead) DropHeadShirt(px, w, h, stride);
|
||||
|
||||
Feather(px, w, h, stride);
|
||||
|
||||
var wb = new WriteableBitmap(w, h, 96, 96, PixelFormats.Bgra32, null);
|
||||
wb.WritePixels(new Int32Rect(0, 0, w, h), px, stride, 0);
|
||||
wb.Freeze();
|
||||
return wb;
|
||||
}
|
||||
|
||||
private static void TryPush(byte[] px, int stride, bool[] seen, int[] stack, ref int sp, int x, int y, int w)
|
||||
{
|
||||
int h = px.Length / stride;
|
||||
if (x < 0 || y < 0 || x >= w || y >= h) return;
|
||||
int idx = y * w + x;
|
||||
if (seen[idx]) return;
|
||||
int p = y * stride + x * 4;
|
||||
if (!IsBg(px[p], px[p + 1], px[p + 2])) return;
|
||||
seen[idx] = true;
|
||||
stack[sp++] = idx;
|
||||
}
|
||||
|
||||
private static bool IsBg(byte b, byte g, byte r)
|
||||
{
|
||||
int mn = Math.Min(r, Math.Min(g, b));
|
||||
int mx = Math.Max(r, Math.Max(g, b));
|
||||
return mn >= BgMinBrightness && (mx - mn) <= BgMaxSpread;
|
||||
}
|
||||
|
||||
/// <summary>Remove enclosed checkerboard-background pockets while keeping solid light objects.</summary>
|
||||
private static void RemoveCheckerPockets(byte[] px, int w, int h, int stride)
|
||||
{
|
||||
int W1 = w + 1;
|
||||
var li = new int[(h + 1) * W1]; // integral of "light" checker tone (~254)
|
||||
var mi = new int[(h + 1) * W1]; // integral of "mid" checker tone (~241)
|
||||
var di = new int[(h + 1) * W1]; // integral of dark (object) pixels
|
||||
var neutral = new bool[w * h];
|
||||
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
int row = y * stride, cur = (y + 1) * W1, prev = y * W1;
|
||||
int la = 0, ma = 0, da = 0;
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int p = row + x * 4; int b = px[p], g = px[p + 1], r = px[p + 2];
|
||||
int mn = Math.Min(r, Math.Min(g, b)), mx = Math.Max(r, Math.Max(g, b));
|
||||
double v = (r + g + b) / 3.0;
|
||||
bool nt = mn >= 224 && (mx - mn) <= 14;
|
||||
neutral[y * w + x] = nt;
|
||||
if (nt && v >= 249) la++;
|
||||
if (nt && v >= 234 && v <= 246) ma++;
|
||||
if (v < 205) da++;
|
||||
li[cur + x + 1] = li[prev + x + 1] + la;
|
||||
mi[cur + x + 1] = mi[prev + x + 1] + ma;
|
||||
di[cur + x + 1] = di[prev + x + 1] + da;
|
||||
}
|
||||
}
|
||||
|
||||
const int R = 18;
|
||||
int area = (2 * R + 1) * (2 * R + 1);
|
||||
int need = (int)(0.06 * area);
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
int row = y * stride;
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int p = row + x * 4;
|
||||
if (px[p + 3] == 0 || !neutral[y * w + x]) continue;
|
||||
int y0 = Math.Max(0, y - R), y1 = Math.Min(h, y + R + 1);
|
||||
int x0 = Math.Max(0, x - R), x1 = Math.Min(w, x + R + 1);
|
||||
if (Win(di, W1, y0, y1, x0, x1) > 2) continue; // an object edge is near → keep
|
||||
if (Win(li, W1, y0, y1, x0, x1) >= need && Win(mi, W1, y0, y1, x0, x1) >= need)
|
||||
px[p + 3] = 0; // both checker tones present → background
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int Win(int[] I, int W1, int y0, int y1, int x0, int x1)
|
||||
=> I[y1 * W1 + x1] - I[y0 * W1 + x1] - I[y1 * W1 + x0] + I[y0 * W1 + x0];
|
||||
|
||||
/// <summary>Clear light, low-saturation interior pixels (the shirt) in the lower head region.</summary>
|
||||
private static void DropHeadShirt(byte[] px, int w, int h, int stride)
|
||||
{
|
||||
int minY = int.MaxValue, maxY = -1;
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
int row = y * stride;
|
||||
for (int x = 0; x < w; x++)
|
||||
if (px[row + x * 4 + 3] >= 24) { if (y < minY) minY = y; if (y > maxY) maxY = y; break; }
|
||||
}
|
||||
if (maxY < 0) return;
|
||||
int yThr = minY + (int)((maxY - minY) * 0.58); // below the face
|
||||
|
||||
for (int y = yThr; y < h; y++)
|
||||
{
|
||||
int row = y * stride;
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int p = row + x * 4;
|
||||
if (px[p + 3] == 0) continue;
|
||||
int b = px[p], g = px[p + 1], r = px[p + 2];
|
||||
int mn = Math.Min(r, Math.Min(g, b));
|
||||
int mx = Math.Max(r, Math.Max(g, b));
|
||||
if (mn >= 222 && (mx - mn) <= 16) px[p + 3] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Soften light, low-saturation pixels that border a cleared area (anti-halo).</summary>
|
||||
private static void Feather(byte[] px, int w, int h, int stride)
|
||||
{
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
int row = y * stride;
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int p = row + x * 4;
|
||||
if (px[p + 3] == 0) continue;
|
||||
|
||||
bool touchesCleared =
|
||||
(x > 0 && px[p - 4 + 3] == 0) ||
|
||||
(x < w - 1 && px[p + 4 + 3] == 0) ||
|
||||
(y > 0 && px[p - stride + 3] == 0) ||
|
||||
(y < h - 1 && px[p + stride + 3] == 0);
|
||||
if (!touchesCleared) continue;
|
||||
|
||||
int b = px[p], g = px[p + 1], r = px[p + 2];
|
||||
int mn = Math.Min(r, Math.Min(g, b));
|
||||
int mx = Math.Max(r, Math.Max(g, b));
|
||||
if (mn < FeatherLo || (mx - mn) > BgMaxSpread) continue;
|
||||
|
||||
int t = Math.Min(255, Math.Max(0, (mn - FeatherLo) * 255 / (FeatherHi - FeatherLo)));
|
||||
px[p + 3] = (byte)(255 - t);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Character_Builder;
|
||||
|
||||
/// <summary>Reads/writes a composition as a human-readable .md file.</summary>
|
||||
public static class BuildMd
|
||||
{
|
||||
public static void Save(string path, BuildDefinition b)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"# Character Build — {b.Name}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("> Saved by Character_Builder. Layers are composited: body + head(expression) + hair tint + accessories.");
|
||||
sb.AppendLine("> Coordinate convention (for the Dansori app): stage 520x680. Each layer image is drawn Height=680 (Stretch=Uniform),");
|
||||
sb.AppendLine("> centered in the stage, scaled about its center by 'scale', then translated by (x, y) pixels. transform = x, y, scale.");
|
||||
sb.AppendLine("> The app can either reuse these transforms directly (same convention) or re-run alpha neck-alignment and treat these as deltas.");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"name: {b.Name}");
|
||||
sb.AppendLine($"character: {b.Character}");
|
||||
sb.AppendLine($"body: {b.BodyFile}");
|
||||
sb.AppendLine($"body.transform: {F(b.BodyOffsetX)}, {F(b.BodyOffsetY)}, {F(b.BodyScale)}");
|
||||
sb.AppendLine($"head: {b.HeadFile}");
|
||||
sb.AppendLine($"expression: {b.ExpressionFile}");
|
||||
sb.AppendLine($"hairmask: {b.HairmaskFile}");
|
||||
sb.AppendLine($"hairColor: {b.HairColor}");
|
||||
sb.AppendLine($"head.transform: {F(b.HeadOffsetX)}, {F(b.HeadOffsetY)}, {F(b.HeadScale)}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("## accessories");
|
||||
foreach (var a in b.Accessories)
|
||||
sb.AppendLine($"- {a.FileName} | {F(a.OffsetX)}, {F(a.OffsetY)}, {F(a.Scale)}");
|
||||
File.WriteAllText(path, sb.ToString(), new UTF8Encoding(false));
|
||||
}
|
||||
|
||||
public static BuildDefinition Load(string path)
|
||||
{
|
||||
var b = new BuildDefinition();
|
||||
bool inAcc = false;
|
||||
foreach (var raw in File.ReadAllLines(path))
|
||||
{
|
||||
var line = raw.Trim();
|
||||
if (line.Length == 0) continue;
|
||||
if (line.StartsWith("## accessories", StringComparison.OrdinalIgnoreCase)) { inAcc = true; continue; }
|
||||
if (line.StartsWith("#")) continue;
|
||||
if (line.StartsWith(">")) continue;
|
||||
|
||||
if (inAcc && line.StartsWith("-"))
|
||||
{
|
||||
var body = line.TrimStart('-', ' ');
|
||||
var parts = body.Split('|');
|
||||
var a = new AccessoryLayer { FileName = parts[0].Trim() };
|
||||
if (parts.Length > 1) ApplyTransform(parts[1], (x, y, s) => { a.OffsetX = x; a.OffsetY = y; a.Scale = s; });
|
||||
if (!string.IsNullOrEmpty(a.FileName)) b.Accessories.Add(a);
|
||||
continue;
|
||||
}
|
||||
|
||||
int c = line.IndexOf(':');
|
||||
if (c < 0) continue;
|
||||
var key = line.Substring(0, c).Trim().ToLowerInvariant();
|
||||
var val = line.Substring(c + 1).Trim();
|
||||
switch (key)
|
||||
{
|
||||
case "name": b.Name = val; break;
|
||||
case "character": b.Character = val; break;
|
||||
case "body": b.BodyFile = Empty(val); break;
|
||||
case "body.transform":
|
||||
ApplyTransform(val, (x, y, s) => { b.BodyOffsetX = x; b.BodyOffsetY = y; b.BodyScale = s; });
|
||||
break;
|
||||
case "head": b.HeadFile = Empty(val); break;
|
||||
case "expression": b.ExpressionFile = Empty(val); break;
|
||||
case "hairmask": b.HairmaskFile = Empty(val); break;
|
||||
case "haircolor": b.HairColor = Empty(val); break;
|
||||
case "head.transform":
|
||||
ApplyTransform(val, (x, y, s) => { b.HeadOffsetX = x; b.HeadOffsetY = y; b.HeadScale = s; });
|
||||
break;
|
||||
}
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private static string? Empty(string v) => string.IsNullOrWhiteSpace(v) ? null : v;
|
||||
|
||||
private static void ApplyTransform(string s, Action<double, double, double> set)
|
||||
{
|
||||
var t = s.Split(',');
|
||||
double x = t.Length > 0 ? D(t[0]) : 0;
|
||||
double y = t.Length > 1 ? D(t[1]) : 0;
|
||||
double sc = t.Length > 2 ? D(t[2]) : 1;
|
||||
set(x, y, sc);
|
||||
}
|
||||
|
||||
private static double D(string s) =>
|
||||
double.TryParse(s.Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var v) ? v : 0;
|
||||
|
||||
private static string F(double v) => v.ToString("0.###", CultureInfo.InvariantCulture);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<RootNamespace>Character_Builder</RootNamespace>
|
||||
<AssemblyName>Character_Builder</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,194 @@
|
||||
<Window x:Class="Character_Builder.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
|
||||
Title="Dansori · Character Builder" Height="1040" Width="1560"
|
||||
MinHeight="760" MinWidth="1320"
|
||||
WindowStyle="None" WindowState="Maximized" WindowStartupLocation="CenterScreen"
|
||||
Background="Transparent"
|
||||
FontFamily="Segoe UI Variable, Segoe UI">
|
||||
|
||||
<shell:WindowChrome.WindowChrome>
|
||||
<shell:WindowChrome CaptionHeight="52" ResizeBorderThickness="6"
|
||||
GlassFrameThickness="0" CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||
</shell:WindowChrome.WindowChrome>
|
||||
|
||||
<Border x:Name="RootBorder" Background="{StaticResource Bg}"
|
||||
BorderBrush="{StaticResource Stroke}" BorderThickness="1">
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ===== TOP BAR ===== -->
|
||||
<Grid Grid.Row="0" Margin="4,0,4,14">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border Width="12" Height="12" CornerRadius="6" Background="{StaticResource Accent}" Margin="0,0,12,0"/>
|
||||
<TextBlock Text="Character Builder" FontSize="28" FontWeight="Bold"/>
|
||||
<TextBlock Text="Dansori 캐릭터 조합기" Foreground="{StaticResource Mute}"
|
||||
FontSize="18" VerticalAlignment="Bottom" Margin="12,0,0,4"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right"
|
||||
shell:WindowChrome.IsHitTestVisibleInChrome="True">
|
||||
<Button x:Name="BtnRoot" Content="📁 루트" Click="Root_Click" Margin="0,0,8,0"/>
|
||||
<Button x:Name="BtnLoad" Content="↥ 불러오기" Click="Load_Click" Margin="0,0,8,0"/>
|
||||
<Button x:Name="BtnReset" Content="⟲ 초기화" Click="Reset_Click" Margin="0,0,8,0"/>
|
||||
<Button x:Name="BtnSave" Content="💾 저장" Click="Save_Click" Style="{StaticResource AccentButton}"/>
|
||||
<Border Width="1" Background="{StaticResource Stroke}" Margin="12,4,10,4"/>
|
||||
<Button x:Name="BtnMin" Style="{StaticResource CaptionButton}" Click="Min_Click">
|
||||
<Path Data="M0,5 H11" Stroke="{StaticResource Dim}" StrokeThickness="1.3"/>
|
||||
</Button>
|
||||
<Button x:Name="BtnMax" Style="{StaticResource CaptionButton}" Click="Max_Click" Margin="2,0">
|
||||
<Path Data="M0,0 H10 V10 H0 Z" Stroke="{StaticResource Dim}" StrokeThickness="1.3" Fill="Transparent"/>
|
||||
</Button>
|
||||
<Button x:Name="BtnClose" Style="{StaticResource CaptionClose}" Click="Close_Click">
|
||||
<Path Data="M0,0 L10,10 M0,10 L10,0" Stroke="{StaticResource Dim}" StrokeThickness="1.3"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- ===== MAIN ===== -->
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="440"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Character list -->
|
||||
<Border Grid.Column="0" Style="{StaticResource Card}" Margin="0,0,14,0">
|
||||
<DockPanel Margin="12">
|
||||
<TextBlock DockPanel.Dock="Top" Text="캐릭터" FontWeight="Bold" FontSize="21" Margin="2,2,0,12"/>
|
||||
<ListBox x:Name="CharList" Background="Transparent" BorderThickness="0"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
SelectionChanged="CharList_SelectionChanged">
|
||||
<ListBox.ItemContainerStyle>
|
||||
<Style TargetType="ListBoxItem">
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Border x:Name="bd" CornerRadius="10" Background="{StaticResource Panel2}"
|
||||
BorderBrush="{StaticResource Stroke}" BorderThickness="1" Padding="8">
|
||||
<ContentPresenter/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="bd" Property="BorderBrush" Value="{StaticResource Accent}"/>
|
||||
<Setter TargetName="bd" Property="Background" Value="#12344A"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="bd" Property="BorderBrush" Value="{StaticResource Accent}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ListBox.ItemContainerStyle>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<Border CornerRadius="8" Height="150" ClipToBounds="True" Background="#0E0F14">
|
||||
<Image Source="{Binding Thumb}" Stretch="Uniform" Margin="4"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" FontSize="19"
|
||||
Margin="2,9,0,2" TextTrimming="CharacterEllipsis"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Preview -->
|
||||
<Border Grid.Column="1" Style="{StaticResource Card}" Margin="0,0,14,0">
|
||||
<DockPanel Margin="14">
|
||||
<Grid DockPanel.Dock="Top" Margin="2,0,2,10">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="TxtCharName" Text="캐릭터를 선택하세요" FontSize="22" FontWeight="Bold"/>
|
||||
</StackPanel>
|
||||
<Button x:Name="BtnPlay" Content="▶ 제스처 미리보기" Click="Play_Click"
|
||||
HorizontalAlignment="Right" Width="210"/>
|
||||
</Grid>
|
||||
<Border Background="#0E0F14" CornerRadius="10" BorderBrush="{StaticResource Stroke}" BorderThickness="1">
|
||||
<Grid>
|
||||
<TextBlock x:Name="PreviewHint" Text="왼쪽에서 캐릭터를 고른 뒤, 오른쪽에서 파츠를 조합하세요."
|
||||
Foreground="{StaticResource Mute}" HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" TextWrapping="Wrap" TextAlignment="Center" MaxWidth="320"/>
|
||||
<Viewbox Stretch="Uniform" Margin="16">
|
||||
<Grid x:Name="StageHost" Width="540" Height="680"
|
||||
Background="Transparent" Cursor="SizeAll"
|
||||
MouseLeftButtonDown="Stage_MouseDown"
|
||||
MouseMove="Stage_MouseMove"
|
||||
MouseLeftButtonUp="Stage_MouseUp">
|
||||
<Grid x:Name="Stage" Width="540" Height="680"/>
|
||||
</Grid>
|
||||
</Viewbox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Controls -->
|
||||
<Border Grid.Column="2" Style="{StaticResource Card}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" Padding="14,10,14,14">
|
||||
<StackPanel>
|
||||
<TextBlock Text="조합" FontWeight="Bold" FontSize="21" Margin="2,2,0,4"/>
|
||||
|
||||
<TextBlock Text="바디 / 제스처" Style="{StaticResource Label}"/>
|
||||
<ComboBox x:Name="CmbBody" SelectionChanged="Body_Changed"/>
|
||||
|
||||
<TextBlock Text="헤어 모양" Style="{StaticResource Label}"/>
|
||||
<ComboBox x:Name="CmbShape" SelectionChanged="Shape_Changed"/>
|
||||
|
||||
<TextBlock Text="표정" Style="{StaticResource Label}"/>
|
||||
<ComboBox x:Name="CmbExpr" SelectionChanged="Expr_Changed"/>
|
||||
|
||||
<TextBlock Text="헤어 색상 (코드 틴트)" Style="{StaticResource Label}"/>
|
||||
<WrapPanel x:Name="SwatchPanel"/>
|
||||
|
||||
<TextBlock Text="악세서리" Style="{StaticResource Label}"/>
|
||||
<Border Background="{StaticResource Panel2}" BorderBrush="{StaticResource Stroke}"
|
||||
BorderThickness="1" CornerRadius="8" Padding="10,6" MinHeight="40" MaxHeight="230">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="AccPanel"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="레이어 위치/크기 조정" Style="{StaticResource Label}"/>
|
||||
<ComboBox x:Name="CmbLayer" SelectionChanged="Layer_Changed"/>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="34"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="X" Foreground="{StaticResource Dim}" VerticalAlignment="Center"/>
|
||||
<Slider x:Name="SldX" Grid.Row="0" Grid.Column="1" Minimum="-400" Maximum="400" ValueChanged="LayerXform_Changed"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Y" Foreground="{StaticResource Dim}" VerticalAlignment="Center"/>
|
||||
<Slider x:Name="SldY" Grid.Row="1" Grid.Column="1" Minimum="-400" Maximum="400" ValueChanged="LayerXform_Changed"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="⤢" Foreground="{StaticResource Dim}" VerticalAlignment="Center"/>
|
||||
<Slider x:Name="SldScale" Grid.Row="2" Grid.Column="1" Minimum="0.1" Maximum="1.6" ValueChanged="LayerXform_Changed"/>
|
||||
</Grid>
|
||||
<Button Content="🎯 머리 자동 정렬" Click="AutoAlign_Click" Margin="0,10,0,0"/>
|
||||
<TextBlock Text="미리보기에서 드래그하면 위 목록에서 선택한 레이어가 이동합니다."
|
||||
Foreground="{StaticResource Mute}" FontSize="16" TextWrapping="Wrap" Margin="2,10,0,0"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- ===== STATUS ===== -->
|
||||
<TextBlock x:Name="TxtStatus" Grid.Row="2" Margin="4,10,4,0"
|
||||
Foreground="{StaticResource Mute}" FontSize="17" Text="준비"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
@@ -0,0 +1,693 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace Character_Builder;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private const double StageH = 680;
|
||||
private const double StageW = 540;
|
||||
|
||||
// contain-fit: baked into each layer's transform so a wide pose isn't clipped by the stage bounds
|
||||
private double _fit = 1, _fitCX, _fitCY;
|
||||
|
||||
private string? _root;
|
||||
private List<CharacterInfo> _characters = new();
|
||||
private CharacterInfo? _char;
|
||||
private BuildDefinition _build = new();
|
||||
|
||||
private List<PartItem> _bodies = new();
|
||||
private List<PartItem> _headFrames = new(); // base + expressions
|
||||
private List<PartItem> _accessories = new();
|
||||
private List<string> _shapes = new();
|
||||
private Dictionary<string, PartItem> _hairmaskByShape = new();
|
||||
private Dictionary<string, string> _pathByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private bool _suppress;
|
||||
|
||||
// preview / animation
|
||||
private readonly TranslateTransform _floatT = new();
|
||||
private readonly DispatcherTimer _timer = new() { Interval = TimeSpan.FromMilliseconds(60) };
|
||||
private Image? _headImage;
|
||||
private ImageSource? _headNormal, _headBlink;
|
||||
private int _phase, _blinkRestoreAt = -1;
|
||||
private bool _playing;
|
||||
|
||||
private static readonly (string label, string? hex)[] Swatches =
|
||||
{
|
||||
("✕", null),
|
||||
// mint / teal / cyan
|
||||
("", "#B7F5E3"), ("", "#38E0C4"), ("", "#1F9E8C"), ("", "#4CC2FF"), ("", "#2C7BE5"),
|
||||
// purple / pink / red
|
||||
("", "#7C5CFF"), ("", "#B36BFF"), ("", "#FF6FD8"), ("", "#FF7B9C"), ("", "#FF5C5C"),
|
||||
// warm
|
||||
("", "#FF9F45"), ("", "#F5D06B"), ("", "#C9A227"),
|
||||
// green / natural / neutral
|
||||
("", "#6BD66B"), ("", "#3C6E47"), ("", "#8A5A2B"), ("", "#4A3B33"),
|
||||
("", "#1A1A1E"), ("", "#D8DCE3"), ("", "#F5F0FF"),
|
||||
};
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
StageHost.RenderTransform = _floatT;
|
||||
_timer.Tick += Timer_Tick;
|
||||
BuildSwatches();
|
||||
Loaded += (_, __) => { ApplyMaximizedInset(); InitRoot(); };
|
||||
StateChanged += (_, __) => ApplyMaximizedInset();
|
||||
}
|
||||
|
||||
// WindowStyle=None + maximize overflows the screen; inset so nothing is clipped.
|
||||
private void ApplyMaximizedInset()
|
||||
=> RootBorder.Padding = WindowState == WindowState.Maximized ? new Thickness(7) : new Thickness(0);
|
||||
|
||||
// ---------- window caption ----------
|
||||
private void Min_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||||
private void Max_Click(object sender, RoutedEventArgs e) =>
|
||||
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
|
||||
private void Close_Click(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
// ---------- root / characters ----------
|
||||
private void InitRoot()
|
||||
{
|
||||
_root = AssetScanner.FindRoot();
|
||||
if (_root == null)
|
||||
{
|
||||
TxtStatus.Text = "Characters_Build_Docs 루트를 찾지 못했습니다. [📁 루트] 로 폴더를 지정하세요.";
|
||||
return;
|
||||
}
|
||||
LoadCharacters();
|
||||
}
|
||||
|
||||
private void LoadCharacters()
|
||||
{
|
||||
if (_root == null) return;
|
||||
_characters = AssetScanner.GetCharacters(_root);
|
||||
CharList.ItemsSource = _characters;
|
||||
TxtStatus.Text = $"루트: {_root} · 캐릭터 {_characters.Count}개";
|
||||
}
|
||||
|
||||
private void Root_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFolderDialog { Title = "Characters_Build_Docs 폴더 선택" };
|
||||
if (dlg.ShowDialog() == true)
|
||||
{
|
||||
_root = dlg.FolderName;
|
||||
LoadCharacters();
|
||||
}
|
||||
}
|
||||
|
||||
private void CharList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppress) return;
|
||||
if (CharList.SelectedItem is CharacterInfo ci) SelectCharacter(ci, null);
|
||||
}
|
||||
|
||||
// ---------- select + populate ----------
|
||||
private void SelectCharacter(CharacterInfo ci, BuildDefinition? preset)
|
||||
{
|
||||
_char = ci;
|
||||
StopPlay();
|
||||
|
||||
var parts = AssetScanner.GetParts(ci.FolderPath);
|
||||
_pathByName = parts.GroupBy(p => p.FileName)
|
||||
.ToDictionary(g => g.Key, g => g.First().FilePath, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_bodies = parts.Where(p => p.FileName.ToLowerInvariant().Contains("_body_"))
|
||||
.Where(p => !IsPartialBody(p.FileName)).ToList();
|
||||
_headFrames = parts.Where(p => p.FileName.ToLowerInvariant().Contains("_head_")).ToList();
|
||||
_accessories = parts.Where(p => p.FileName.ToLowerInvariant().StartsWith("acc")).ToList();
|
||||
_shapes = _headFrames.Select(h => h.Shape).Where(s => !string.IsNullOrEmpty(s)).Distinct().OrderBy(s => s).ToList();
|
||||
_hairmaskByShape = parts.Where(p => p.FileName.ToLowerInvariant().Contains("_hairmask_"))
|
||||
.GroupBy(p => p.Shape).ToDictionary(g => g.Key, g => g.First());
|
||||
|
||||
_suppress = true;
|
||||
CmbBody.ItemsSource = _bodies;
|
||||
CmbShape.ItemsSource = _shapes;
|
||||
BuildAccessoryChecks();
|
||||
_suppress = false;
|
||||
|
||||
_build = preset ?? MakeDefaultBuild(ci);
|
||||
TxtCharName.Text = ci.Name + (preset != null ? $" · {preset.Name}" : "");
|
||||
PreviewHint.Visibility = Visibility.Collapsed;
|
||||
|
||||
ApplyBuildToUi();
|
||||
if (preset == null) AutoAlignHead(); else { UpdateFit(); RebuildStage(); }
|
||||
TxtStatus.Text = $"{ci.Name}: 바디 {_bodies.Count} · 머리프레임 {_headFrames.Count} · 악세 {_accessories.Count}";
|
||||
}
|
||||
|
||||
/// <summary>Component pieces (arms/legs/torso) aren't standalone bodies — hide them from the list.</summary>
|
||||
private static bool IsPartialBody(string fileName)
|
||||
{
|
||||
var stem = System.IO.Path.GetFileNameWithoutExtension(fileName).ToLowerInvariant();
|
||||
return stem.EndsWith("_legs") || stem.EndsWith("_torso")
|
||||
|| stem.EndsWith("_arm_l") || stem.EndsWith("_arm_r");
|
||||
}
|
||||
|
||||
private BuildDefinition MakeDefaultBuild(CharacterInfo ci)
|
||||
{
|
||||
var b = new BuildDefinition { Character = ci.Name, Name = ci.Name };
|
||||
var body = _bodies.FirstOrDefault(x => x.FileName.Contains("idle_upper")) ?? _bodies.FirstOrDefault();
|
||||
b.BodyFile = body?.FileName;
|
||||
var shape = _shapes.FirstOrDefault();
|
||||
if (shape != null)
|
||||
{
|
||||
b.HeadFile = _headFrames.FirstOrDefault(h => h.Shape == shape && h.Expr == "(base)")?.FileName
|
||||
?? _headFrames.FirstOrDefault(h => h.Shape == shape)?.FileName;
|
||||
b.ExpressionFile = _headFrames.FirstOrDefault(h => h.Shape == shape && h.Expr == "neutral")?.FileName
|
||||
?? b.HeadFile;
|
||||
if (_hairmaskByShape.TryGetValue(shape, out var hm)) b.HairmaskFile = hm.FileName;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
private void ApplyBuildToUi()
|
||||
{
|
||||
_suppress = true;
|
||||
CmbBody.SelectedItem = _bodies.FirstOrDefault(x => x.FileName == _build.BodyFile);
|
||||
|
||||
var shape = ShapeOf(_build.ExpressionFile) ?? ShapeOf(_build.HeadFile) ?? _shapes.FirstOrDefault();
|
||||
CmbShape.SelectedItem = shape;
|
||||
PopulateExpr(shape);
|
||||
CmbExpr.SelectedItem = _headFrames.FirstOrDefault(h => h.FileName == _build.ExpressionFile);
|
||||
|
||||
foreach (var cb in AccPanel.Children.OfType<CheckBox>())
|
||||
{
|
||||
var pi = cb.Tag as PartItem;
|
||||
cb.IsChecked = pi != null && _build.Accessories.Any(a => a.FileName == pi.FileName);
|
||||
}
|
||||
HighlightSwatch(_build.HairColor);
|
||||
PopulateLayerCombo();
|
||||
_suppress = false;
|
||||
}
|
||||
|
||||
private string? ShapeOf(string? fileName)
|
||||
{
|
||||
if (fileName == null) return null;
|
||||
return _headFrames.FirstOrDefault(h => h.FileName == fileName)?.Shape;
|
||||
}
|
||||
|
||||
private void PopulateExpr(string? shape)
|
||||
{
|
||||
var frames = _headFrames.Where(h => h.Shape == shape)
|
||||
.OrderBy(h => h.Expr == "(base)" ? 0 : 1).ThenBy(h => h.Expr)
|
||||
.Select(h => new PartItem { FileName = h.FileName, FilePath = h.FilePath, Shape = h.Shape, Expr = h.Expr, Display = h.Expr })
|
||||
.ToList();
|
||||
CmbExpr.ItemsSource = frames;
|
||||
}
|
||||
|
||||
// ---------- accessory checklist ----------
|
||||
private void BuildAccessoryChecks()
|
||||
{
|
||||
AccPanel.Children.Clear();
|
||||
if (_accessories.Count == 0)
|
||||
{
|
||||
AccPanel.Children.Add(new TextBlock { Text = "(악세서리 이미지 없음)", Foreground = (Brush)FindResource("Mute"), FontSize = 12 });
|
||||
return;
|
||||
}
|
||||
foreach (var a in _accessories)
|
||||
{
|
||||
var cb = new CheckBox { Content = a.Display, Tag = a };
|
||||
cb.Checked += Acc_Toggled;
|
||||
cb.Unchecked += Acc_Toggled;
|
||||
AccPanel.Children.Add(cb);
|
||||
}
|
||||
}
|
||||
|
||||
private void Acc_Toggled(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_suppress) return;
|
||||
if (sender is not CheckBox cb || cb.Tag is not PartItem pi) return;
|
||||
if (cb.IsChecked == true)
|
||||
{
|
||||
if (!_build.Accessories.Any(a => a.FileName == pi.FileName))
|
||||
{
|
||||
var layer = new AccessoryLayer { FileName = pi.FileName };
|
||||
var low = pi.FileName.ToLowerInvariant();
|
||||
// head-worn items start anchored to the head; wrist/foot items keep defaults.
|
||||
// Accessory art is framed standalone (roughly centred in its own canvas), so it must
|
||||
// start well below the head's own scale or it dwarfs the head — the user then fine-tunes.
|
||||
if (low.Contains("headphone") || low.Contains("catear") || low.Contains("clubband")
|
||||
|| low.Contains("cap") || low.Contains("glasses") || low.Contains("hat") || low.Contains("ear"))
|
||||
{
|
||||
layer.OffsetX = _build.HeadOffsetX;
|
||||
layer.OffsetY = _build.HeadOffsetY;
|
||||
layer.Scale = _build.HeadScale * 0.62;
|
||||
}
|
||||
else
|
||||
{
|
||||
// props / wrist / foot items: sit near the body centre at a modest size
|
||||
layer.OffsetX = _build.BodyOffsetX;
|
||||
layer.OffsetY = _build.BodyOffsetY;
|
||||
layer.Scale = Math.Max(0.18, _build.HeadScale * 0.5);
|
||||
}
|
||||
_build.Accessories.Add(layer);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_build.Accessories.RemoveAll(a => a.FileName == pi.FileName);
|
||||
}
|
||||
PopulateLayerCombo();
|
||||
RebuildStage();
|
||||
}
|
||||
|
||||
// ---------- swatches ----------
|
||||
private void BuildSwatches()
|
||||
{
|
||||
SwatchPanel.Children.Clear();
|
||||
foreach (var (label, hex) in Swatches)
|
||||
{
|
||||
var btn = new Button
|
||||
{
|
||||
Width = 38,
|
||||
Height = 38,
|
||||
Margin = new Thickness(0, 0, 10, 10),
|
||||
Padding = new Thickness(0),
|
||||
Content = label,
|
||||
Tag = hex,
|
||||
Background = hex == null ? (Brush)FindResource("Panel2") : new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)),
|
||||
Foreground = (Brush)FindResource("Text"),
|
||||
};
|
||||
btn.Click += Swatch_Click;
|
||||
SwatchPanel.Children.Add(btn);
|
||||
}
|
||||
}
|
||||
|
||||
private void Swatch_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button b) return;
|
||||
_build.HairColor = b.Tag as string;
|
||||
HighlightSwatch(_build.HairColor);
|
||||
RebuildStage();
|
||||
}
|
||||
|
||||
private void HighlightSwatch(string? hex)
|
||||
{
|
||||
foreach (var b in SwatchPanel.Children.OfType<Button>())
|
||||
{
|
||||
bool sel = string.Equals((b.Tag as string) ?? "", hex ?? "", StringComparison.OrdinalIgnoreCase);
|
||||
b.BorderBrush = sel ? (Brush)FindResource("Accent") : (Brush)FindResource("Stroke");
|
||||
b.BorderThickness = new Thickness(sel ? 2 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- compose events ----------
|
||||
private void Body_Changed(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppress) return;
|
||||
if (CmbBody.SelectedItem is PartItem p) { _build.BodyFile = p.FileName; AutoAlignHead(); }
|
||||
}
|
||||
|
||||
private void Shape_Changed(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppress) return;
|
||||
if (CmbShape.SelectedItem is not string shape) return;
|
||||
_build.HeadFile = _headFrames.FirstOrDefault(h => h.Shape == shape && h.Expr == "(base)")?.FileName
|
||||
?? _headFrames.FirstOrDefault(h => h.Shape == shape)?.FileName;
|
||||
_build.HairmaskFile = _hairmaskByShape.TryGetValue(shape, out var hm) ? hm.FileName : null;
|
||||
_suppress = true;
|
||||
PopulateExpr(shape);
|
||||
var neutral = (CmbExpr.ItemsSource as IEnumerable<PartItem>)?.FirstOrDefault(x => x.Expr == "neutral")
|
||||
?? (CmbExpr.ItemsSource as IEnumerable<PartItem>)?.FirstOrDefault();
|
||||
CmbExpr.SelectedItem = neutral;
|
||||
_build.ExpressionFile = neutral?.FileName ?? _build.HeadFile;
|
||||
_suppress = false;
|
||||
AutoAlignHead();
|
||||
}
|
||||
|
||||
private void Expr_Changed(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppress) return;
|
||||
if (CmbExpr.SelectedItem is PartItem p) { _build.ExpressionFile = p.FileName; RebuildStage(); }
|
||||
}
|
||||
|
||||
// ---------- layer transform ----------
|
||||
// CmbLayer index: 0 = head, 1 = body, 2.. = accessories. Every layer is adjustable.
|
||||
private void PopulateLayerCombo()
|
||||
{
|
||||
_suppress = true;
|
||||
CmbLayer.Items.Clear();
|
||||
CmbLayer.Items.Add("머리 (Head)");
|
||||
CmbLayer.Items.Add("바디 (Body)");
|
||||
foreach (var a in _build.Accessories) CmbLayer.Items.Add(a.FileName);
|
||||
CmbLayer.SelectedIndex = 0;
|
||||
_suppress = false;
|
||||
LoadLayerToSliders();
|
||||
}
|
||||
|
||||
private void Layer_Changed(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_suppress) return;
|
||||
LoadLayerToSliders();
|
||||
}
|
||||
|
||||
private (double x, double y, double s) GetLayer(int idx)
|
||||
{
|
||||
if (idx == 1) return (_build.BodyOffsetX, _build.BodyOffsetY, _build.BodyScale);
|
||||
if (idx >= 2 && idx - 2 < _build.Accessories.Count)
|
||||
{
|
||||
var a = _build.Accessories[idx - 2];
|
||||
return (a.OffsetX, a.OffsetY, a.Scale);
|
||||
}
|
||||
return (_build.HeadOffsetX, _build.HeadOffsetY, _build.HeadScale);
|
||||
}
|
||||
|
||||
private void SetLayer(int idx, double x, double y, double s)
|
||||
{
|
||||
if (idx == 1) { _build.BodyOffsetX = x; _build.BodyOffsetY = y; _build.BodyScale = s; }
|
||||
else if (idx >= 2 && idx - 2 < _build.Accessories.Count)
|
||||
{
|
||||
var a = _build.Accessories[idx - 2];
|
||||
a.OffsetX = x; a.OffsetY = y; a.Scale = s;
|
||||
}
|
||||
else { _build.HeadOffsetX = x; _build.HeadOffsetY = y; _build.HeadScale = s; }
|
||||
}
|
||||
|
||||
private void LoadLayerToSliders()
|
||||
{
|
||||
_suppress = true;
|
||||
var (x, y, s) = GetLayer(CmbLayer.SelectedIndex);
|
||||
SldX.Value = Math.Max(SldX.Minimum, Math.Min(SldX.Maximum, x));
|
||||
SldY.Value = Math.Max(SldY.Minimum, Math.Min(SldY.Maximum, y));
|
||||
SldScale.Value = Math.Max(SldScale.Minimum, Math.Min(SldScale.Maximum, s));
|
||||
_suppress = false;
|
||||
}
|
||||
|
||||
private void LayerXform_Changed(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (_suppress) return;
|
||||
SetLayer(CmbLayer.SelectedIndex, SldX.Value, SldY.Value, SldScale.Value);
|
||||
RebuildStage();
|
||||
}
|
||||
|
||||
// ---------- stage compositing ----------
|
||||
private string? ResolvePath(string? fn)
|
||||
=> (fn != null && _pathByName.TryGetValue(fn, out var p)) ? p : null;
|
||||
|
||||
private void RebuildStage()
|
||||
{
|
||||
if (Stage == null) return;
|
||||
Stage.Children.Clear();
|
||||
_headImage = null; _headNormal = null; _headBlink = null;
|
||||
if (_char == null) return;
|
||||
|
||||
// body
|
||||
var bodyPath = ResolvePath(_build.BodyFile);
|
||||
if (bodyPath != null) AddLayer(bodyPath, _build.BodyScale, _build.BodyOffsetX, _build.BodyOffsetY);
|
||||
|
||||
// head (expression frame preferred)
|
||||
var headPath = ResolvePath(_build.ExpressionFile) ?? ResolvePath(_build.HeadFile);
|
||||
if (headPath != null)
|
||||
{
|
||||
_headImage = AddLayer(headPath, _build.HeadScale, _build.HeadOffsetX, _build.HeadOffsetY);
|
||||
_headNormal = _headImage.Source;
|
||||
var shape = ShapeOf(_build.ExpressionFile) ?? ShapeOf(_build.HeadFile);
|
||||
var blink = _headFrames.FirstOrDefault(h => h.Shape == shape && h.Expr == "blink");
|
||||
if (blink != null) _headBlink = AssetScanner.LoadPart(blink.FilePath);
|
||||
|
||||
// hair tint
|
||||
var maskPath = ResolvePath(_build.HairmaskFile);
|
||||
if (maskPath != null && !string.IsNullOrEmpty(_build.HairColor))
|
||||
AddTint(maskPath, _build.HairColor!, _build.HeadScale, _build.HeadOffsetX, _build.HeadOffsetY);
|
||||
}
|
||||
|
||||
// accessories
|
||||
foreach (var a in _build.Accessories)
|
||||
{
|
||||
var ap = ResolvePath(a.FileName);
|
||||
if (ap != null) AddLayer(ap, a.Scale, a.OffsetX, a.OffsetY);
|
||||
}
|
||||
}
|
||||
|
||||
private Image AddLayer(string path, double scale, double offX, double offY)
|
||||
{
|
||||
var src = AssetScanner.LoadPart(path);
|
||||
double aspect = (src != null && src.PixelHeight > 0) ? (double)src.PixelWidth / src.PixelHeight : 1.0;
|
||||
// Bake scale AND the contain-fit into the element's SIZE (not a RenderTransform). A full-size
|
||||
// element (680*aspect) overflows the stage and the Viewbox clips it — cutting the character —
|
||||
// *before* any RenderTransform can shrink it. A pre-sized element only overflows by its
|
||||
// transparent margins, so the character is never clipped.
|
||||
double s = scale * _fit;
|
||||
var img = new Image
|
||||
{
|
||||
Source = src,
|
||||
Stretch = Stretch.Fill,
|
||||
Width = StageH * aspect * s,
|
||||
Height = StageH * s,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
RenderTransform = new TranslateTransform((offX - _fitCX) * _fit, (offY - _fitCY) * _fit),
|
||||
IsHitTestVisible = false,
|
||||
};
|
||||
Stage.Children.Add(img);
|
||||
return img;
|
||||
}
|
||||
|
||||
private void AddTint(string maskPath, string hex, double scale, double offX, double offY)
|
||||
{
|
||||
try
|
||||
{
|
||||
var mask = AssetScanner.LoadBitmap(maskPath);
|
||||
if (mask == null) return;
|
||||
double aspect = mask.PixelHeight > 0 ? (double)mask.PixelWidth / mask.PixelHeight : 1;
|
||||
double s = scale * _fit;
|
||||
var rect = new Rectangle
|
||||
{
|
||||
Height = StageH * s,
|
||||
Width = StageH * aspect * s,
|
||||
Fill = new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex)) { Opacity = 0.8 },
|
||||
OpacityMask = new ImageBrush(mask) { Stretch = Stretch.Uniform },
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
RenderTransform = new TranslateTransform((offX - _fitCX) * _fit, (offY - _fitCY) * _fit),
|
||||
IsHitTestVisible = false,
|
||||
};
|
||||
Stage.Children.Add(rect);
|
||||
}
|
||||
catch { /* tint is best-effort */ }
|
||||
}
|
||||
|
||||
// ---------- drag to move the selected layer ----------
|
||||
private bool _dragging;
|
||||
private Point _dragStart;
|
||||
private double _dragBaseX, _dragBaseY;
|
||||
|
||||
private void Stage_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (_char == null) return;
|
||||
(_dragBaseX, _dragBaseY) = GetSelectedOffset();
|
||||
_dragStart = e.GetPosition(Stage);
|
||||
_dragging = true;
|
||||
StageHost.CaptureMouse();
|
||||
}
|
||||
|
||||
private void Stage_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
|
||||
{
|
||||
if (!_dragging) return;
|
||||
var p = e.GetPosition(Stage);
|
||||
double f = _fit == 0 ? 1 : _fit; // layers are drawn at offset*_fit, so undo it for the delta
|
||||
SetSelectedOffset(_dragBaseX + (p.X - _dragStart.X) / f, _dragBaseY + (p.Y - _dragStart.Y) / f);
|
||||
}
|
||||
|
||||
private void Stage_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
{
|
||||
if (!_dragging) return;
|
||||
_dragging = false;
|
||||
StageHost.ReleaseMouseCapture();
|
||||
}
|
||||
|
||||
/// <summary>Offset of the layer currently chosen in CmbLayer (head/body/accessory).</summary>
|
||||
private (double x, double y) GetSelectedOffset()
|
||||
{
|
||||
var (x, y, _) = GetLayer(CmbLayer.SelectedIndex);
|
||||
return (x, y);
|
||||
}
|
||||
|
||||
private void SetSelectedOffset(double x, double y)
|
||||
{
|
||||
var (_, _, s) = GetLayer(CmbLayer.SelectedIndex);
|
||||
SetLayer(CmbLayer.SelectedIndex, Math.Round(x), Math.Round(y), s);
|
||||
_suppress = true;
|
||||
SldX.Value = Math.Max(SldX.Minimum, Math.Min(SldX.Maximum, Math.Round(x)));
|
||||
SldY.Value = Math.Max(SldY.Minimum, Math.Min(SldY.Maximum, Math.Round(y)));
|
||||
_suppress = false;
|
||||
RebuildStage();
|
||||
}
|
||||
|
||||
// ---------- auto-align (alpha neck detection) ----------
|
||||
private void AutoAlign_Click(object sender, RoutedEventArgs e) => AutoAlignHead();
|
||||
|
||||
/// <summary>
|
||||
/// Fit the whole figure: a headless body drawn full-height leaves no room for the head, so
|
||||
/// scale the body down and drop it, then sit the head on top with a visible neck.
|
||||
/// </summary>
|
||||
private void AutoAlignHead()
|
||||
{
|
||||
var bodyPath = ResolvePath(_build.BodyFile);
|
||||
var headPath = ResolvePath(_build.ExpressionFile) ?? ResolvePath(_build.HeadFile);
|
||||
if (bodyPath == null || headPath == null) { RebuildStage(); return; }
|
||||
|
||||
var ab = AlphaTools.Analyze(bodyPath);
|
||||
var ah = AlphaTools.Analyze(headPath);
|
||||
if (ab == null || ah == null) { RebuildStage(); return; }
|
||||
|
||||
const double chin = 0.58;
|
||||
|
||||
// Head:body height ratio — waist-up poses (landscape images) carry a larger head, full-body
|
||||
// poses (portrait images) a smaller one. These are nominal scales; UpdateFit() then scales
|
||||
// the whole composition to fill the preview, so absolute values here only set the ratio.
|
||||
bool fullBody = (double)ab.W / ab.H < 0.9;
|
||||
double r = fullBody ? 0.24 : 0.60; // full-body figures need a smaller (natural) head
|
||||
// Size the head against the body *below the neck* (torso→feet), not the full bbox: poses with
|
||||
// raised arms make the bbox taller, which would otherwise inflate the head ("big-head" look).
|
||||
double bodyBFrac = (double)(ab.MaxY - ab.NeckTop) / ab.H;
|
||||
double M = bodyBFrac * ah.H / (double)ah.BH;
|
||||
double bodyScale = 1.0;
|
||||
double hs = Math.Max(0.15, Math.Min(1.3, r * M));
|
||||
|
||||
// Attach the head at the body's neck: neck top (robust to raised arms) for Y, torso axis for X.
|
||||
double bodyNeckTopFrac = (double)ab.NeckTop / ab.H;
|
||||
double bodyOffY = -bodyScale * (bodyNeckTopFrac - 0.5) * StageH; // neck line at stage centre
|
||||
double lwB = StageH * ((double)ab.W / ab.H), lwH = StageH * ((double)ah.W / ah.H);
|
||||
double bodyNeckX = bodyScale * ((double)ab.AxisX / ab.W - 0.5) * lwB;
|
||||
double headCenterXf = (ah.MinX + ah.MaxX) / 2.0 / ah.W;
|
||||
double headOffX = bodyNeckX - hs * (headCenterXf - 0.5) * lwH;
|
||||
double headNeckFrac = (ah.MinY + chin * ah.BH) / (double)ah.H;
|
||||
double headOffY = -hs * (headNeckFrac - 0.5) * StageH;
|
||||
|
||||
_build.BodyScale = Math.Round(bodyScale, 3);
|
||||
_build.BodyOffsetX = 0;
|
||||
_build.BodyOffsetY = Math.Round(bodyOffY);
|
||||
_build.HeadScale = Math.Round(hs, 3);
|
||||
_build.HeadOffsetX = Math.Round(headOffX);
|
||||
_build.HeadOffsetY = Math.Round(headOffY);
|
||||
|
||||
UpdateFit();
|
||||
LoadLayerToSliders();
|
||||
RebuildStage();
|
||||
}
|
||||
|
||||
/// <summary>Scale/centre the whole composition (head + body) to fill the preview stage.</summary>
|
||||
private void UpdateFit()
|
||||
{
|
||||
var ab = ResolvePath(_build.BodyFile) is { } bp ? AlphaTools.Analyze(bp) : null;
|
||||
var ah = ResolvePath(_build.ExpressionFile) is { } ep ? AlphaTools.Analyze(ep)
|
||||
: ResolvePath(_build.HeadFile) is { } hp2 ? AlphaTools.Analyze(hp2) : null;
|
||||
|
||||
double x0 = double.MaxValue, x1 = double.MinValue, y0 = double.MaxValue, y1 = double.MinValue;
|
||||
void Acc(AlphaTools.Analysis? a, double s, double ox, double oy)
|
||||
{
|
||||
if (a == null) return;
|
||||
double lw = StageH * ((double)a.W / a.H);
|
||||
foreach (var fx in new[] { (double)a.MinX / a.W, (double)a.MaxX / a.W })
|
||||
{ double x = s * (fx - 0.5) * lw + ox; x0 = Math.Min(x0, x); x1 = Math.Max(x1, x); }
|
||||
foreach (var fy in new[] { (double)a.MinY / a.H, (double)a.MaxY / a.H })
|
||||
{ double y = s * (fy - 0.5) * StageH + oy; y0 = Math.Min(y0, y); y1 = Math.Max(y1, y); }
|
||||
}
|
||||
Acc(ab, _build.BodyScale, _build.BodyOffsetX, _build.BodyOffsetY);
|
||||
Acc(ah, _build.HeadScale, _build.HeadOffsetX, _build.HeadOffsetY);
|
||||
|
||||
if (x1 <= x0 || y1 <= y0) { _fit = 1; _fitCX = _fitCY = 0; return; }
|
||||
const double m = 0.03;
|
||||
_fit = Math.Min(StageW * (1 - 2 * m) / (x1 - x0), StageH * (1 - 2 * m) / (y1 - y0));
|
||||
_fitCX = (x0 + x1) / 2;
|
||||
_fitCY = (y0 + y1) / 2;
|
||||
}
|
||||
|
||||
// ---------- gesture preview ----------
|
||||
private void Play_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_char == null) return;
|
||||
if (_playing) StopPlay(); else StartPlay();
|
||||
}
|
||||
|
||||
private void StartPlay()
|
||||
{
|
||||
_playing = true; _phase = 0; _blinkRestoreAt = -1;
|
||||
BtnPlay.Content = "⏸ 정지";
|
||||
_timer.Start();
|
||||
}
|
||||
|
||||
private void StopPlay()
|
||||
{
|
||||
_playing = false;
|
||||
_timer.Stop();
|
||||
_floatT.Y = 0;
|
||||
BtnPlay.Content = "▶ 제스처 미리보기";
|
||||
if (_headImage != null && _headNormal != null) _headImage.Source = _headNormal;
|
||||
}
|
||||
|
||||
private void Timer_Tick(object? sender, EventArgs e)
|
||||
{
|
||||
_phase++;
|
||||
_floatT.Y = Math.Sin(_phase * 0.09) * 7;
|
||||
|
||||
// blink
|
||||
if (_headImage != null && _headBlink != null)
|
||||
{
|
||||
if (_phase % 55 == 0) { _headImage.Source = _headBlink; _blinkRestoreAt = _phase + 8; }
|
||||
else if (_phase == _blinkRestoreAt && _headNormal != null) { _headImage.Source = _headNormal; }
|
||||
}
|
||||
|
||||
// cycle gestures (bodies)
|
||||
if (CmbBody.Items.Count > 1 && _phase % 45 == 0)
|
||||
CmbBody.SelectedIndex = (CmbBody.SelectedIndex + 1) % CmbBody.Items.Count;
|
||||
}
|
||||
|
||||
// ---------- save / load / reset ----------
|
||||
private void Save_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_char == null) { TxtStatus.Text = "먼저 캐릭터를 선택하세요."; return; }
|
||||
var dlg = new Microsoft.Win32.SaveFileDialog
|
||||
{
|
||||
Title = "캐릭터 조합 저장",
|
||||
InitialDirectory = _char.FolderPath,
|
||||
Filter = "Markdown (*.md)|*.md",
|
||||
FileName = _char.Name + "_build.md",
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
_build.Name = System.IO.Path.GetFileNameWithoutExtension(dlg.FileName);
|
||||
_build.Character = _char.Name;
|
||||
BuildMd.Save(dlg.FileName, _build);
|
||||
TxtCharName.Text = _char.Name + " · " + _build.Name;
|
||||
TxtStatus.Text = "저장됨: " + dlg.FileName;
|
||||
}
|
||||
|
||||
private void Load_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = "캐릭터 조합 불러오기",
|
||||
InitialDirectory = _char?.FolderPath ?? _root ?? "",
|
||||
Filter = "Markdown (*.md)|*.md",
|
||||
};
|
||||
if (dlg.ShowDialog() != true) return;
|
||||
|
||||
BuildDefinition b;
|
||||
try { b = BuildMd.Load(dlg.FileName); }
|
||||
catch (Exception ex) { TxtStatus.Text = "불러오기 실패: " + ex.Message; return; }
|
||||
|
||||
var ci = _characters.FirstOrDefault(c => string.Equals(c.Name, b.Character, StringComparison.OrdinalIgnoreCase));
|
||||
if (ci == null) { TxtStatus.Text = "캐릭터 폴더를 찾을 수 없습니다: " + b.Character; return; }
|
||||
|
||||
_suppress = true;
|
||||
CharList.SelectedItem = ci;
|
||||
_suppress = false;
|
||||
SelectCharacter(ci, b);
|
||||
TxtStatus.Text = "불러옴: " + dlg.FileName;
|
||||
}
|
||||
|
||||
private void Reset_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_char != null) SelectCharacter(_char, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Character_Builder;
|
||||
|
||||
/// <summary>A character folder (has a Reference/ sheet).</summary>
|
||||
public class CharacterInfo
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string FolderPath { get; set; } = "";
|
||||
public string? SheetPath { get; set; }
|
||||
public ImageSource? Thumb { get; set; }
|
||||
public override string ToString() => Name;
|
||||
}
|
||||
|
||||
/// <summary>One scanned PNG asset (body/head/expression/hairmask/accessory).</summary>
|
||||
public class PartItem
|
||||
{
|
||||
public string Display { get; set; } = "";
|
||||
public string FileName { get; set; } = "";
|
||||
public string FilePath { get; set; } = "";
|
||||
public string Shape { get; set; } = ""; // for heads/expressions
|
||||
public string Expr { get; set; } = ""; // for expression frames
|
||||
public override string ToString() => Display;
|
||||
}
|
||||
|
||||
/// <summary>A composited accessory layer with its own transform.</summary>
|
||||
public class AccessoryLayer
|
||||
{
|
||||
public string FileName { get; set; } = "";
|
||||
public double OffsetX { get; set; }
|
||||
public double OffsetY { get; set; } = -190;
|
||||
public double Scale { get; set; } = 0.42;
|
||||
}
|
||||
|
||||
/// <summary>The saveable/loadable composition.</summary>
|
||||
public class BuildDefinition
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public string Character { get; set; } = "";
|
||||
public string? BodyFile { get; set; }
|
||||
public double BodyOffsetX { get; set; }
|
||||
public double BodyOffsetY { get; set; }
|
||||
public double BodyScale { get; set; } = 1.0;
|
||||
public string? HeadFile { get; set; }
|
||||
public string? ExpressionFile { get; set; }
|
||||
public string? HairmaskFile { get; set; }
|
||||
public string? HairColor { get; set; }
|
||||
public double HeadOffsetX { get; set; }
|
||||
public double HeadOffsetY { get; set; } = -190;
|
||||
public double HeadScale { get; set; } = 0.42;
|
||||
public List<AccessoryLayer> Accessories { get; set; } = new();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v8.0",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v8.0": {
|
||||
"Character_Builder/1.0.0": {
|
||||
"runtime": {
|
||||
"Character_Builder.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Character_Builder/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net8.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "8.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App",
|
||||
"version": "8.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\Character_Builder.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\Character_Builder.csproj": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\Character_Builder.csproj",
|
||||
"projectName": "Character_Builder",
|
||||
"projectPath": "D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\Character_Builder.csproj",
|
||||
"packagesPath": "C:\\Users\\eKeerar\\.nuget\\packages\\",
|
||||
"outputPath": "D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\eKeerar\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net8.0-windows"
|
||||
],
|
||||
"sources": {
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0-windows7.0": {
|
||||
"targetAlias": "net8.0-windows",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0-windows7.0": {
|
||||
"targetAlias": "net8.0-windows",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
},
|
||||
"Microsoft.WindowsDesktop.App.WPF": {
|
||||
"privateAssets": "none"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\eKeerar\.nuget\packages\</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">6.11.2</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\eKeerar\.nuget\packages\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" />
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")]
|
||||
@@ -0,0 +1,82 @@
|
||||
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "93E40EC7B36C3FCE2164127217EDE90B9E9D0788"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace Character_Builder {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// App
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application {
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "8.0.28.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
|
||||
#line 4 "..\..\..\App.xaml"
|
||||
this.StartupUri = new System.Uri("MainWindow.xaml", System.UriKind.Relative);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
System.Uri resourceLocater = new System.Uri("/Character_Builder;component/app.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\App.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Application Entry Point.
|
||||
/// </summary>
|
||||
[System.STAThreadAttribute()]
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "8.0.28.0")]
|
||||
public static void Main() {
|
||||
Character_Builder.App app = new Character_Builder.App();
|
||||
app.InitializeComponent();
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("Character_Builder")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("Character_Builder")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("Character_Builder")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")]
|
||||
[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")]
|
||||
|
||||
// MSBuild WriteCodeFragment 클래스에서 생성되었습니다.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1c258de41a7d7aa346051148913e5c061ad06ddde6059e732bbb9876cdf09ec2
|
||||
@@ -0,0 +1,13 @@
|
||||
is_global = true
|
||||
build_property.TargetFramework = net8.0-windows
|
||||
build_property.TargetPlatformMinVersion = 7.0
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = Character_Builder
|
||||
build_property.ProjectDir = D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
@@ -0,0 +1,6 @@
|
||||
// <auto-generated/>
|
||||
global using global::System;
|
||||
global using global::System.Collections.Generic;
|
||||
global using global::System.Linq;
|
||||
global using global::System.Threading;
|
||||
global using global::System.Threading.Tasks;
|
||||
@@ -0,0 +1 @@
|
||||
07a0985beef6e5eb47d3a46d05ca778b8054347852e2b1cae45c9a00e86007f5
|
||||
@@ -0,0 +1,40 @@
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\MainWindow.baml
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\App.baml
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\MainWindow.g.cs
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\App.g.cs
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder_MarkupCompile.cache
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.g.resources
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.GeneratedMSBuildEditorConfig.editorconfig
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.AssemblyInfoInputs.cache
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.AssemblyInfo.cs
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.csproj.CoreCompileInputs.cache
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.exe
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.deps.json
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.runtimeconfig.json
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.dll
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.pdb
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.dll
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\refint\Character_Builder.dll
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.pdb
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.genruntimeconfig.cache
|
||||
D:\개인자료\Work_AI\DansoriEQ\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\ref\Character_Builder.dll
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.exe
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.deps.json
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.runtimeconfig.json
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.dll
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\bin\Debug\net8.0-windows\Character_Builder.pdb
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\MainWindow.baml
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\App.baml
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\MainWindow.g.cs
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\App.g.cs
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder_MarkupCompile.cache
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.g.resources
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.GeneratedMSBuildEditorConfig.editorconfig
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.AssemblyInfoInputs.cache
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.AssemblyInfo.cs
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.csproj.CoreCompileInputs.cache
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.dll
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\refint\Character_Builder.dll
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.pdb
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\Character_Builder.genruntimeconfig.cache
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\ref\Character_Builder.dll
|
||||
@@ -0,0 +1 @@
|
||||
e0da6d5848fb3c8f7177291c1a3794bcd05e16ca5e19ea615d9ef22a5fe104df
|
||||
@@ -0,0 +1,20 @@
|
||||
Character_Builder
|
||||
|
||||
|
||||
winexe
|
||||
C#
|
||||
.cs
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\obj\Debug\net8.0-windows\
|
||||
Character_Builder
|
||||
none
|
||||
false
|
||||
TRACE;DEBUG;NET;NET8_0;NETCOREAPP
|
||||
D:\Work_AI\Dansori\Characters_Build_Docs\Character_Builder\App.xaml
|
||||
11407045341
|
||||
|
||||
827567737
|
||||
1981026658812
|
||||
MainWindow.xaml;
|
||||
|
||||
False
|
||||
|
||||
@@ -0,0 +1,463 @@
|
||||
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "8A60234C00FE3CBC6D4956AD0F7CF945C604F6A6"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace Character_Builder {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// MainWindow
|
||||
/// </summary>
|
||||
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
||||
|
||||
|
||||
#line 16 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border RootBorder;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 35 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BtnRoot;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 36 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BtnLoad;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 37 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BtnReset;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 38 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BtnSave;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 40 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BtnMin;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 43 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BtnMax;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 46 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BtnClose;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 64 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.ListBox CharList;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 112 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock TxtCharName;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 114 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BtnPlay;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 119 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock PreviewHint;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 123 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Grid StageHost;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 128 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Grid Stage;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 143 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.ComboBox CmbBody;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 146 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.ComboBox CmbShape;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 149 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.ComboBox CmbExpr;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 152 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.WrapPanel SwatchPanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 158 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel AccPanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 163 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.ComboBox CmbLayer;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 175 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Slider SldX;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 177 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Slider SldY;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 179 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Slider SldScale;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 190 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock TxtStatus;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "8.0.28.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/Character_Builder;component/mainwindow.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\MainWindow.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "8.0.28.0")]
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
||||
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
||||
switch (connectionId)
|
||||
{
|
||||
case 1:
|
||||
this.RootBorder = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 2:
|
||||
this.BtnRoot = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 35 "..\..\..\MainWindow.xaml"
|
||||
this.BtnRoot.Click += new System.Windows.RoutedEventHandler(this.Root_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 3:
|
||||
this.BtnLoad = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 36 "..\..\..\MainWindow.xaml"
|
||||
this.BtnLoad.Click += new System.Windows.RoutedEventHandler(this.Load_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 4:
|
||||
this.BtnReset = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 37 "..\..\..\MainWindow.xaml"
|
||||
this.BtnReset.Click += new System.Windows.RoutedEventHandler(this.Reset_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 5:
|
||||
this.BtnSave = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 38 "..\..\..\MainWindow.xaml"
|
||||
this.BtnSave.Click += new System.Windows.RoutedEventHandler(this.Save_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 6:
|
||||
this.BtnMin = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 40 "..\..\..\MainWindow.xaml"
|
||||
this.BtnMin.Click += new System.Windows.RoutedEventHandler(this.Min_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 7:
|
||||
this.BtnMax = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 43 "..\..\..\MainWindow.xaml"
|
||||
this.BtnMax.Click += new System.Windows.RoutedEventHandler(this.Max_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 8:
|
||||
this.BtnClose = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 46 "..\..\..\MainWindow.xaml"
|
||||
this.BtnClose.Click += new System.Windows.RoutedEventHandler(this.Close_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 9:
|
||||
this.CharList = ((System.Windows.Controls.ListBox)(target));
|
||||
|
||||
#line 66 "..\..\..\MainWindow.xaml"
|
||||
this.CharList.SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(this.CharList_SelectionChanged);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 10:
|
||||
this.TxtCharName = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 11:
|
||||
this.BtnPlay = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 114 "..\..\..\MainWindow.xaml"
|
||||
this.BtnPlay.Click += new System.Windows.RoutedEventHandler(this.Play_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 12:
|
||||
this.PreviewHint = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 13:
|
||||
this.StageHost = ((System.Windows.Controls.Grid)(target));
|
||||
|
||||
#line 125 "..\..\..\MainWindow.xaml"
|
||||
this.StageHost.MouseLeftButtonDown += new System.Windows.Input.MouseButtonEventHandler(this.Stage_MouseDown);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 126 "..\..\..\MainWindow.xaml"
|
||||
this.StageHost.MouseMove += new System.Windows.Input.MouseEventHandler(this.Stage_MouseMove);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
#line 127 "..\..\..\MainWindow.xaml"
|
||||
this.StageHost.MouseLeftButtonUp += new System.Windows.Input.MouseButtonEventHandler(this.Stage_MouseUp);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 14:
|
||||
this.Stage = ((System.Windows.Controls.Grid)(target));
|
||||
return;
|
||||
case 15:
|
||||
this.CmbBody = ((System.Windows.Controls.ComboBox)(target));
|
||||
|
||||
#line 143 "..\..\..\MainWindow.xaml"
|
||||
this.CmbBody.SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(this.Body_Changed);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 16:
|
||||
this.CmbShape = ((System.Windows.Controls.ComboBox)(target));
|
||||
|
||||
#line 146 "..\..\..\MainWindow.xaml"
|
||||
this.CmbShape.SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(this.Shape_Changed);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 17:
|
||||
this.CmbExpr = ((System.Windows.Controls.ComboBox)(target));
|
||||
|
||||
#line 149 "..\..\..\MainWindow.xaml"
|
||||
this.CmbExpr.SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(this.Expr_Changed);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 18:
|
||||
this.SwatchPanel = ((System.Windows.Controls.WrapPanel)(target));
|
||||
return;
|
||||
case 19:
|
||||
this.AccPanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 20:
|
||||
this.CmbLayer = ((System.Windows.Controls.ComboBox)(target));
|
||||
|
||||
#line 163 "..\..\..\MainWindow.xaml"
|
||||
this.CmbLayer.SelectionChanged += new System.Windows.Controls.SelectionChangedEventHandler(this.Layer_Changed);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 21:
|
||||
this.SldX = ((System.Windows.Controls.Slider)(target));
|
||||
|
||||
#line 175 "..\..\..\MainWindow.xaml"
|
||||
this.SldX.ValueChanged += new System.Windows.RoutedPropertyChangedEventHandler<double>(this.LayerXform_Changed);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 22:
|
||||
this.SldY = ((System.Windows.Controls.Slider)(target));
|
||||
|
||||
#line 177 "..\..\..\MainWindow.xaml"
|
||||
this.SldY.ValueChanged += new System.Windows.RoutedPropertyChangedEventHandler<double>(this.LayerXform_Changed);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 23:
|
||||
this.SldScale = ((System.Windows.Controls.Slider)(target));
|
||||
|
||||
#line 179 "..\..\..\MainWindow.xaml"
|
||||
this.SldScale.ValueChanged += new System.Windows.RoutedPropertyChangedEventHandler<double>(this.LayerXform_Changed);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 24:
|
||||
|
||||
#line 181 "..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.AutoAlign_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 25:
|
||||
this.TxtStatus = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
}
|
||||
this._contentLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"version": 3,
|
||||
"targets": {
|
||||
"net8.0-windows7.0": {}
|
||||
},
|
||||
"libraries": {},
|
||||
"projectFileDependencyGroups": {
|
||||
"net8.0-windows7.0": []
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\eKeerar\\.nuget\\packages\\": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.0",
|
||||
"restore": {
|
||||
"projectUniqueName": "D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\Character_Builder.csproj",
|
||||
"projectName": "Character_Builder",
|
||||
"projectPath": "D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\Character_Builder.csproj",
|
||||
"packagesPath": "C:\\Users\\eKeerar\\.nuget\\packages\\",
|
||||
"outputPath": "D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\eKeerar\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net8.0-windows"
|
||||
],
|
||||
"sources": {
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0-windows7.0": {
|
||||
"targetAlias": "net8.0-windows",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
}
|
||||
},
|
||||
"frameworks": {
|
||||
"net8.0-windows7.0": {
|
||||
"targetAlias": "net8.0-windows",
|
||||
"imports": [
|
||||
"net461",
|
||||
"net462",
|
||||
"net47",
|
||||
"net471",
|
||||
"net472",
|
||||
"net48",
|
||||
"net481"
|
||||
],
|
||||
"assetTargetFallback": true,
|
||||
"warn": true,
|
||||
"frameworkReferences": {
|
||||
"Microsoft.NETCore.App": {
|
||||
"privateAssets": "all"
|
||||
},
|
||||
"Microsoft.WindowsDesktop.App.WPF": {
|
||||
"privateAssets": "none"
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\8.0.422/PortableRuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "zLTNiLqmafM=",
|
||||
"success": true,
|
||||
"projectFilePath": "D:\\Work_AI\\Dansori\\Characters_Build_Docs\\Character_Builder\\Character_Builder.csproj",
|
||||
"expectedPackageFiles": [],
|
||||
"logs": []
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
> ⚠️ **아카이브** (소스 자산 생성 시기 기록, 이동된 소스 폴더 참조 포함). **현재 베이스 = `INTERACTIVE_RIG_HANDOFF.md` + `*_Profile/` + `README.md`.**
|
||||
|
||||
# HANDOFF — Dansori 캐릭터 자산 & Character Builder (세션 인수인계)
|
||||
|
||||
> 작성: 2026-07-02 (같은 날 저녁 갱신). 다른 세션에서 이어서 진행하기 위한 요약.
|
||||
> **최신**: NewImage 88장을 각 `Images/`로 **이동 완료** · `Characters`/`Characters.Old` **삭제 예정** · 이미지 생성 계속 후 **내일(2026-07-03) 이어서**.
|
||||
> 이 문서 = **캐릭터 자산 시스템(`Characters_Build_Docs`) + 조합 앱(`Character_Builder`)** 진행/계획.
|
||||
> DansoriEQ **본체 앱 UI** 작업(메인화면 캐릭터 배치·전 창 네온·부유 동기화 등)은 `../docs/HANDOFF.md §8` 참조.
|
||||
|
||||
---
|
||||
|
||||
## 0. 한 줄 요약
|
||||
Dansori 브랜드 마스코트(이소리·이단·듀오)를 **페이퍼돌 조립 방식**으로 관리하는 자산 체계와,
|
||||
이를 **직접 보고 조합/저장(.md)** 하는 WPF 앱을 구축했다. **이미지 대량 생성은 진행 중**이며,
|
||||
빌더 테스트는 **이미지가 충분히 생성된 뒤** 진행 예정.
|
||||
|
||||
## 1. 자산 시스템 구조 (`Characters_Build_Docs/`)
|
||||
```
|
||||
README.md # 브랜드 자산 규칙 + "새 캐릭터 추가 Playbook"(STEP 1~7) ← 단일 진실원
|
||||
HANDOFF.md # (이 문서)
|
||||
LeeSori/ # 이소리(민트/여)
|
||||
LeeSori.md # 토대(정체성·조립규칙·네이밍·§8 새 베리에이션 절차·23 표준 바디 목록)
|
||||
USAGE_FOR_DEV.md # 앱 통합 가이드(캐릭터 공통) — 조립/앵커/포터블 규칙
|
||||
Reference/sori_sheet.png
|
||||
Base/Base.md (+Images/) # 트랙슈트 헤드리스 바디: 파츠5 + 제스처18 = 23블록
|
||||
Hair/Hair.md (+Images/) # 4모양(short/long/waveS/waveL) 개별블록 88 (머리+표정20+hairmask ×4)
|
||||
Hair/Hair_{Long,Short,WaveS,WaveL}Neat.md # 단정·윤기 추가본 각 22 → 헤어 토큰 총 8종(원본4 + neat4)
|
||||
Accessories/Accessories.md (+Images/) # 오버레이 11(착용7+소품4)
|
||||
Variations/{DressShort,DressLong,Jeans,Tshirt,CeoPantsuit}/*.md (+Images/) # 각 23블록
|
||||
LeeDan/ # 이단(시안/남) — LeeSori와 동일 구조
|
||||
LeeDan.md · Reference/dan_sheet.png
|
||||
Base/Base.md(23) · Hair/Hair.md(88: short/buzz/messy/slick ×22) · Accessories(3: headphones/smartwatch/cap)
|
||||
Variations/{Suit,Casual,Club}/*.md(각 23)
|
||||
LeeSori_and_LeeDan/ # 듀오(베이크드 장면, 리그 아님)
|
||||
Duo.md · Reference/{combined_master_sheet,sori_sheet,dan_sheet}.png
|
||||
Scenes/Scenes.md(+Images/) # 7장: backtoback·sidebyside·fistbump·present·wave·success·earbud_share
|
||||
Character_Builder/ # WPF 조합 앱 (아래 §4)
|
||||
```
|
||||
- 구버전 레이어식 요청서는 `../docs/IMAGE_GEN_*`(및 `*.old`)에 **보존**(다른 세션 작업중, 건드리지 않음). 이 폴더는 그 내용을 캐릭터 중심으로 재정리한 독립본.
|
||||
|
||||
## 2. 페이퍼돌 조립 규칙 (모든 캐릭터 공통) — 상세 `README.md`
|
||||
- 완성 캐릭터 = **헤드리스 바디(복장)** + **머리(헤어모양)** + **표정 프레임** + **악세서리(오버레이)** + **색상(코드 틴트)**.
|
||||
- **정합 앵커**: 목(바디↔머리)·어깨·정수리/귀·목선·손목·발.
|
||||
- **동작은 코드**(끄덕/갸웃/호흡/blink/talk = 트랜스폼+프레임 교체) — 이미지 아님.
|
||||
- **색상은 코드**(hairmask로 hue-shift) — 색 변형 이미지 생성 안 함.
|
||||
- 생성용 md = **영어 프롬프트 위주**, `Reference/<char>_sheet.png` 첨부 명시, 결과는 **각 md 옆 `Images/`**에 저장.
|
||||
|
||||
## 3. 이미지 생성 현황 (갱신 2026-07-02 저녁)
|
||||
- **목표 총량 ≈ 516장**(md 이미지 블록 기준): 이소리 ~326 · 이단 183 · 듀오 7.
|
||||
- **완료·배치됨: 88장(이소리)** — `Characters/NewImage`에서 각 `Images/`로 **이동 완료**(미일치 0):
|
||||
- `LeeSori/Base/Images` **23**(트랙 바디 전부) · `LeeSori/Accessories/Images` **11**(전부) · `LeeSori/Hair/Images` **54**(short·long 완성 + waveS/waveL 일부)
|
||||
- `Characters/NewImage` **잔여 0**.
|
||||
- **미생성 ≈430**: 이소리 헤어 나머지(waveS/waveL 잔여 + **neat 4종 88**) + 복장변형 116 · **이단 183 전부** · **듀오 7**.
|
||||
- **앞으로**: 생성물은 임시폴더 말고 **각 md 옆 `Images/`에 바로 저장**(분류 불필요). 순서 = 각 캐릭터 `_RUN_ORDER.md`.
|
||||
|
||||
## 4. Character Builder (WPF 앱) — `Character_Builder/`
|
||||
- **상태**: **빌드 성공(오류0)**. `Character_Builder/bin/Debug/net8.0-windows/Character_Builder.exe`.
|
||||
- 빌드: `dotnet build Characters_Build_Docs/Character_Builder/Character_Builder.csproj` (외부 패키지 없음, 오프라인 OK).
|
||||
- **기능**: 캐릭터 리스트(시트 썸네일) → 선택 → 파츠 조합(바디/헤어모양/표정/헤어색/악세서리) →
|
||||
**미리보기 합성** → **.md 저장(파일명 지정)/불러오기** → **제스처 미리보기**(포즈순환+깜빡임+부유).
|
||||
- **자동 정렬**: `AlphaTools.cs` 가 알파(불투명) 분석으로 목 위치 검출 → 머리를 바디 목에 스냅 + 비율 스케일.
|
||||
캐릭터/바디/헤어 변경 시 자동, [🎯 머리 자동 정렬] 버튼으로 재실행, 슬라이더로 미세조정.
|
||||
- **.md(빌드파일) = Dansori 앱 개발용**: 좌표 규약(stage 520×680, 각 레이어 Height=680 Uniform·중심·center scale·(x,y)px)을
|
||||
.md 헤더에 명시. 앱은 트랜스폼 직접 사용 또는 동일 정렬 알고리즘 재사용(`AlphaTools` 재활용).
|
||||
- **테스트 보류**: 사용자가 **이미지가 충분히 생성된 뒤 테스트** 예정(내일).
|
||||
- 현재 `LeeSori/{Base,Accessories,Hair}/Images`에 88장 배치됨 → **이소리 + short/long 헤어 + 트랙 바디 + 악세서리** 조합은 지금도 미리보기 가능.
|
||||
|
||||
## 5. DansoriEQ 본체 앱 (요약 — 상세 `../docs/HANDOFF.md §8`)
|
||||
- 메인화면 EQ 그래프 좌측 배경에 **이소리(단독)** 크게 배치, 그래프 좌측이동/축소, 창 1600×980.
|
||||
- 배경 이소리 **부유/말하기**를 아바타와 동기(`MainWindow.SetBgMascotTalking`).
|
||||
- **전 팝업 창 모던 타이틀(FluentWindow+ui:TitleBar) + 파란 네온 프레임**, MainWindow 네온.
|
||||
- 다음 코드작업: 배경 이소리 **통짜→헤드리스 리그 교체**(파츠 합성+표정프레임+고개회전+hairmask 틴트+악세 오버레이).
|
||||
|
||||
## 6. 앞으로 할 일 (우선순위)
|
||||
1. **이미지 생성 계속** (사용자, 외부 이미지 AI): 각 `_RUN_ORDER.md` 순서로 **이소리 나머지 → 이단 전체 → 듀오**. 결과는 **각 md 옆 `Images/`에 바로 저장**(분류 불필요).
|
||||
2. ✅ **NewImage → 각 `Images/` 분류 완료** (88장 이동, 미일치 0).
|
||||
3. **구 자료 삭제**(사용자 직접, 예정): `../Characters`, `../Characters.Old`.
|
||||
- 안전성 확인됨: 앱은 자체 `src/DansoriEQ.App/Assets/Characters`(13장) 사용, 시트는 Build_Docs에 백업.
|
||||
- 유일본 손실 주의: Characters.Old의 구 파츠·흰배경 백업·추가포즈(새 파이프라인이 대체 예정).
|
||||
4. **Character Builder 테스트**(이미지 후) → 자동정렬 정확도 튜닝(머리 스케일 비율 현재 0.32, 목밴드 6%),
|
||||
필요 시 **드래그 이동**·색상 커스텀 추가.
|
||||
5. **앱 통합**: `LeeSori/USAGE_FOR_DEV.md` 규칙대로 DansoriEQ(및 향후 Dansori 앱)에 리그 통합.
|
||||
6. **폴더 이동**: `Characters_Build_Docs`를 Dansori 브랜드 루트로 이동(포터블 규칙 — 절대경로 없음, 통째 이동 가능).
|
||||
|
||||
## 7. 유지할 결정/제약
|
||||
- 색상=코드(이미지X) · 절대경로 하드코딩 금지 · 헤드리스 바디+목 앵커 · 듀오=베이크드 장면 ·
|
||||
생성 md=영어 프롬프트+시트 첨부 · 빌드 .md=앱 개발용(좌표규약 명시) · 모든 신규 이소리 베리에이션은 기본 리깅 상속.
|
||||
@@ -0,0 +1,47 @@
|
||||
> ⚠️ **아카이브** (소스 이미지 생성 기록). 하루카는 완성됨 — 현재 베이스 = `INTERACTIVE_RIG_HANDOFF.md` + `Haruka_Profile/`.
|
||||
|
||||
# Haruka Image Generation Progress Handoff
|
||||
|
||||
Updated: 2026-07-03 16:32 KST
|
||||
|
||||
## Scope
|
||||
- This session handled **Haruka only**.
|
||||
- `Isabel/` was intentionally excluded because it is being processed in another parallel session.
|
||||
|
||||
## Result
|
||||
- Haruka `_RUN_ORDER.md` target set is complete.
|
||||
- Total Haruka targets: **100**
|
||||
- Verified OK: **100**
|
||||
- Missing: **0**
|
||||
- Bad alpha / bad format: **0**
|
||||
|
||||
## Completed Groups
|
||||
- `Haruka/Reference`: 1 / 1
|
||||
- `Haruka/Hair/Images`: 22 / 22
|
||||
- `Haruka/Base/Images`: 23 / 23
|
||||
- `Haruka/Accessories/Images`: 7 / 7
|
||||
- `Haruka/Variations/Idol/Images`: 23 / 23
|
||||
- `Haruka/Variations/Witch/Images`: 24 / 24
|
||||
|
||||
## Alpha Verification
|
||||
All Haruka target PNGs were checked with `System.Drawing.Bitmap`.
|
||||
|
||||
Required condition:
|
||||
- `PixelFormat == Format32bppArgb`
|
||||
- Four corner pixels have `alpha == 0`
|
||||
|
||||
Verification summary:
|
||||
```text
|
||||
Haruka target total=100 ok=100 missing=0 bad=0
|
||||
Haruka\Accessories\Images: total=7 ok=7 missing=0 bad=0
|
||||
Haruka\Base\Images: total=23 ok=23 missing=0 bad=0
|
||||
Haruka\Hair\Images: total=22 ok=22 missing=0 bad=0
|
||||
Haruka\Reference: total=1 ok=1 missing=0 bad=0
|
||||
Haruka\Variations\Idol\Images: total=23 ok=23 missing=0 bad=0
|
||||
Haruka\Variations\Witch\Images: total=24 ok=24 missing=0 bad=0
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Generated sources were produced with a flat chroma-key background, then converted locally with the installed `remove_chroma_key.py` helper.
|
||||
- Existing images were not AI-edited for transparency; alpha was produced by local pixel-based chroma-key removal.
|
||||
- The final assets are saved beside their source md files under each `Images/` folder, following `_RUN_ORDER.md`.
|
||||
@@ -0,0 +1,50 @@
|
||||
# 확정 결정 로그 (Decisions)
|
||||
|
||||
> 논의를 통해 확정된 결정과 근거. 뒤집을 땐 여기에 사유와 함께 갱신.
|
||||
|
||||
## D1. 표현 방식 = 하이브리드 (확정)
|
||||
리그 + 베이크드 포즈 + 표정 프레임 스왑을 상황별로 조합.
|
||||
- **근거**: 리그는 앰비언트/열린 제스처에 강하고, 팔짱·하트 같은 자기-가림 포즈는 베이크드 이미지가 자연스럽다. 각자의 강점만 사용.
|
||||
|
||||
## D2. 구현 레벨 = 코드 네이티브 경량 리그 (확정, Live2D/Spine 배제)
|
||||
- **근거**: Live2D/Spine의 리깅은 **독점 GUI 에디터에서 사람이** 하는 작업 → **AI 자동화 목적과 상충**. 우리 코드로 리그/모션/반응을 소유하면 데이터(JSON)만으로 자동화·반복이 가능.
|
||||
|
||||
## D3. 분절 = 완전 해부학 16파츠 (확정)
|
||||
head·neck·chest·pelvis + (상완·전완·손)×2 + (허벅지·종아리·발)×2.
|
||||
- **근거**: 팔꿈치·무릎·손목·목·허리가 실제로 접혀야 제스처/춤이 자연스럽다.
|
||||
|
||||
## D4. 얼굴 = 표정 프레임 스왑 (확정)
|
||||
20종 표정 이미지 교체 + 말하기 = talk 프레임 순환(유사 립싱크).
|
||||
- **한계 인지**: 눈+입이 세트로 고정 → "감정+정밀 립싱크 동시"는 불가. 필요 시 D7로 승급.
|
||||
|
||||
## D5. 자기-가림 포즈 = 베이크드 이미지 (확정)
|
||||
팔짱(armscross)·하트(heart) 등은 리그 보간 대신 **통짜 포즈 이미지**로. (기존 표준 18제스처 자산 재사용.)
|
||||
|
||||
## D6. 투명 알파 필수 (확정)
|
||||
모든 파츠/프레임 = 32-bit RGBA(`Format32bppArgb`), 배경 alpha=0. 24-bit·매트 배경 금지.
|
||||
|
||||
## D7. mesh-warp(그리드 변형) = 옵션·후속 (보류)
|
||||
목/얼굴 국소 mesh-warp(WebGL)로 목 이음새·정밀 립싱크·중간 각도 고개돌림을 승급.
|
||||
- **승급 조건**: 강체 리그로 목/얼굴이 실제로 부족할 때, 그 부위에만 국소 도입. 전신 적용 안 함.
|
||||
|
||||
## D8. 이미지 = ChatGPT 자동생성 (확정)
|
||||
사람이 안 그림. 생성용 `.md` 스펙을 우리가 제공.
|
||||
|
||||
## D9. 색상·모션 = 코드/데이터 (확정)
|
||||
색 변형 = hairmask hue-shift. 모션 = 리그 클립. 반응 = 시퀀서 데이터.
|
||||
|
||||
## D10. 프로필 구조 (확정)
|
||||
`Haruka_Profile` 구조를 복제해 **`Haruka_Profile`** 로 운용(캐릭터별 자료 구조 표준). 시트 표준 위치 = `03_Assets/Reference/haruka_sheet.png`.
|
||||
|
||||
## D11. 리그 파츠 생성 = 마스터-슬라이스 우선, 개별생성 폴백/attachment (확정)
|
||||
- 핵심 16파츠는 **마스터 1장 → 로컬 슬라이스**(같은 좌표계 → 관절 자동 정합, 접합 오차↓)가 **1순위**. 파츠 개별 생성은 그 **폴백**(같은 16파츠를 만드는 대체 방법 — 둘 다 만들 필요 없음).
|
||||
- **슬라이스 출력 = 풀캔버스**: 각 파츠는 **크롭 없이 마스터와 동일한 520×900 캔버스에 제자리 배치**(그 외 투명). 16장 스택 시 마스터 복원 → 위치정보 보존, 앵커 튜닝 불필요. (타이트 크롭하면 위치정보가 사라져 정합이 깨짐.)
|
||||
- **단 마스터에 없는 변형 파츠**(핑거하트·주먹·가리킴 등 대체 손 attachment)는 **개별 생성으로만** 가능 → 그 용도엔 개별 생성이 별도로 필요.
|
||||
- **근거**: 슬라이스는 좌표 정합에 강함(반복 수정 원인 제거). 생성 AI는 픽셀 좌표를 못 맞추므로 접합 좌표는 **생성 후 이미지에서 측정**(정규화 앵커 `imgAnchor`)해 `rig.json`에 저장.
|
||||
|
||||
## 열린 결정 (미확정)
|
||||
- **O1. 최종 런타임 호스트**: 프로토타입=웹(Canvas). 본체=WPF. WPF에 동일 리그/시퀀서를 이식(C#) 할지, 아니면 WebView2로 웹 런타임을 임베드할지 → `../08_Roadmap/App_Integration.md` 에서 결정 예정.
|
||||
- **O2. 대사 표시**: 말풍선 캡션 vs TTS 음성 vs 둘 다.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# 목적과 방향 (Purpose & Direction)
|
||||
|
||||
## 최종 목적
|
||||
하루카를 **앱에 탑재된 인터랙티브 마스코트**로 만든다. 사용자의 행동·앱 상태(상황)에 따라 캐릭터가 **적절한 제스처·표정·대사로 반응**해 살아있는 느낌을 준다.
|
||||
|
||||
## 대표 사용 시나리오 (상황 → 반응)
|
||||
| 상황(트리거) | 반응 |
|
||||
|---|---|
|
||||
| 오류/금지된 동작 | 팔짱 끼고 인상 쓰며 고개 저으며 **"안돼요"** |
|
||||
| 성공/완료/칭찬 | 손 하트 그리며 밝게 **"잘됐어요"** |
|
||||
| 대기/유휴(배경) | 가볍게 **춤추는** 루프(앰비언트) |
|
||||
| (확장) 인사 | 손 흔들며 "안녕하세요" |
|
||||
| (확장) 안내/설명 | 한 손 제시(present) + 말하기 |
|
||||
| (확장) 생각중/로딩 | 갸웃 + thinking 표정 |
|
||||
> 확장 반응은 같은 프레임워크로 계속 추가한다(`../06_Reactions/Reactions.md`).
|
||||
|
||||
## 방향성 (핵심 원칙)
|
||||
1. **AI 자동화**: 모든 캐릭터 이미지는 **ChatGPT로 생성**(각 생성용 `.md` 스펙 제공). 사람이 그리지 않는다.
|
||||
2. **동작·색상은 코드/데이터**: 모션(리그 클립)·반응 시퀀스·색 변형(hairmask hue-shift)은 이미지가 아니라 코드/데이터. → 재사용·자동화·경량.
|
||||
3. **하이브리드 표현**: 상황에 맞춰 **리그(앰비언트/열린 제스처)** + **베이크드 포즈(자기-가림 포즈)** + **표정 프레임 스왑(감정/말하기)** 을 조합.
|
||||
4. **경량·포터블**: 에디터/외주 없이 **우리 코드**로 리그·모션·반응을 소유. 데이터(JSON)는 뷰어(웹)와 WPF 앱이 동일하게 사용.
|
||||
5. **투명 알파 필수**: 모든 파츠/프레임은 32-bit RGBA(`Format32bppArgb`), 배경 alpha=0.
|
||||
6. **점진적 품질 상향**: 강체 리그로 시작 → 필요한 곳(목/얼굴)만 **mesh-warp** 국소 승급(옵션).
|
||||
|
||||
## 범위 밖(당분간 안 함)
|
||||
- Live2D/Spine 도입(GUI 리깅=자동화와 상충). 전신 mesh-warp. 3D. 정밀 음소 립싱크(얼굴 mesh-warp 승급 시 재검토).
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
# 아키텍처 (Architecture)
|
||||
|
||||
## 레이어 모델
|
||||
```
|
||||
[상황 이벤트] 예: "error" / "success" / "idle"
|
||||
│
|
||||
▼
|
||||
[트리거 매퍼] reactions.json 상황키 → 반응 클립 이름
|
||||
│
|
||||
▼
|
||||
[반응 시퀀서] clips/<name>.json 타임라인으로 아래 레이어들을 조율
|
||||
├─ Body 레이어 ── 리그 클립(rig.json+track) │ 또는 │ 베이크드 포즈 이미지
|
||||
├─ Face 레이어 ── 표정 프레임 스왑(neutral/negative/love/…)
|
||||
├─ Mouth 레이어 ── 말하기(talk 프레임 순환, 유사 립싱크)
|
||||
├─ Transform 레이어 ── 리그 위에 덧입히는 잔모션(고개젓기·바운스)
|
||||
└─ FX/Caption 레이어 ── 말풍선·효과음(옵션)
|
||||
│
|
||||
▼
|
||||
[컴포지터] 파츠 합성 + 표정 오버레이 + 앵커 정렬(AlphaTools 재활용)
|
||||
│
|
||||
▼
|
||||
[렌더러] Canvas(웹 프로토타입) / WPF(본체) — 60fps
|
||||
```
|
||||
|
||||
## 레이어 책임
|
||||
- **Body**: 캐릭터 몸의 자세/모션. 두 모드.
|
||||
- `rig` 모드 = 16파츠 리그를 클립으로 구동(앰비언트·열린 제스처·전환).
|
||||
- `baked` 모드 = 통짜 포즈 이미지 1장(자기-가림 포즈: 팔짱·하트).
|
||||
- **Face**: 감정 = 표정 프레임 교체(머리 이미지 스왑).
|
||||
- **Mouth**: 말하기 = talk/neutral 프레임 순환(유사 립싱크). *정밀 립싱크는 mesh-warp 승급 시.*
|
||||
- **Transform**: 리그 본에 delta를 더하는 잔모션(고개 좌우·호흡·바운스). Body가 baked여도 전체 트랜스폼은 적용 가능.
|
||||
- **FX/Caption**: 말풍선 텍스트·효과음(옵션).
|
||||
|
||||
## 하이브리드 선택 규칙 (언제 무엇을)
|
||||
| 상황 | Body 모드 | 근거 |
|
||||
|---|---|---|
|
||||
| 배경춤·유휴·호흡 | **rig** | 부드러운 앰비언트, 자산 최소 |
|
||||
| 손 흔들기·가리키기·제시·박수 | **rig** | 열린 자세 → 리그로 자연스러움 |
|
||||
| 고개 끄덕/젓기 | **rig**(Transform) | 작은 각도 + 가림 |
|
||||
| 팔짱·핑거하트·볼하트 | **baked** | 손이 몸에 겹침 → 리그는 뚫림/어색 |
|
||||
| 큰 감정 포즈(만세·좌절) | **baked** 또는 rig(joy/cheer) | 필요 정밀도에 따라 |
|
||||
|
||||
## 전환 (transition)
|
||||
- rig→rig: 트랜스폼 보간(부드럽게).
|
||||
- rig↔baked: 짧은 **크로스페이드**(150~250ms) 또는 리그로 근사 진입 후 스냅.
|
||||
- 반응 종료 후 `return` 지정 클립(보통 `idle`/`dance_idle`)으로 복귀.
|
||||
|
||||
## 데이터 재사용
|
||||
- `rig.json`·클립·`reactions.json`·표정/포즈 이미지는 **웹 뷰어와 WPF가 동일하게** 사용(플랫폼 독립 데이터).
|
||||
- 앵커 정렬은 기존 `Character_Builder/AlphaTools.cs`(알파 기반 목/어깨 검출) 로직을 재활용.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# 한계와 완화 (Limits & Mitigations)
|
||||
|
||||
강체 컷아웃 + 프레임 스왑의 본질적 한계와, 우리가 쓰는 완화책. 그리고 mesh-warp 승급 기준.
|
||||
|
||||
## L1. 관절 이음새 (강체 회전)
|
||||
- **문제**: 파츠를 크게 회전하면 관절 경계가 벌어지거나 겹침(특히 목). 분절을 늘려도 **근육 연속성은 해결 안 됨**(강체의 본질).
|
||||
- **완화**: ① 회전 각도 작게 ② 피벗 낮게 ③ **근위단 오버랩 스텁**(부모가 틈을 덮음) ④ 옷깃/머리/초커로 **가림** ⑤ z-순서.
|
||||
- **잔여 위험**: 큰 각도 고개돌림·큰 팔동작은 여전히 티남 → baked 포즈로 우회 또는 L4 승급.
|
||||
|
||||
## L2. 립싱크 (프레임 스왑 얼굴)
|
||||
- **문제**: 표정이 눈+입 세트 고정 → "특정 감정 + 정밀 입모양" 동시 불가.
|
||||
- **완화**: 감정 표정 + 고개짓 + talk/neutral 프레임 순환으로 **뉘앙스 전달**. 필요하면 **감정+talk 조합 머리**를 소수 추가 생성(`../03_Assets/Expressions_and_Poses.md`).
|
||||
- **잔여 위험**: 음소 단위 정밀 립싱크는 불가 → L4 승급.
|
||||
|
||||
## L3. 자기-가림 포즈
|
||||
- **문제**: 팔짱·하트 등 손이 몸/반대팔에 겹치는 포즈는 리그로 뚫림.
|
||||
- **완화**: **baked 포즈 이미지**로 대체(기존 18제스처 자산). 진입은 크로스페이드.
|
||||
|
||||
## L4. mesh-warp 승급 (옵션·후속)
|
||||
- **무엇**: 목/얼굴에 그리드 메시를 씌워 **피부 늘어남·입/눈썹 독립 변형·중간 각도 고개돌림**을 구현(WebGL). 우리 런타임 내 구현 → 자동화 유지.
|
||||
- **승급 조건(하나라도 강하게 필요할 때)**: (a) 목 이음새가 baked/가림으로도 부족 (b) 감정+정밀 립싱크 동시 필요 (c) 중간 각도 고개돌림 필요.
|
||||
- **한계(승급해도)**: 큰 각도(대략 30°↑) 고개돌림은 **가려진 반대면이 없어** 여전히 각도별 머리 아트 추가가 필요. 자동 워프가 없는 면을 만들지는 못함.
|
||||
- **원칙**: 전신 아님, **국소(neck/head)만**. 품질 튜닝은 스크린샷 피드백 루프.
|
||||
|
||||
## 요약
|
||||
> 강체+프레임스왑으로 **대부분의 상황 반응은 충분히 자연스럽게** 만든다(가림·baked·잔모션 조합). 정밀 립싱크/큰 고개돌림만 별도 승급(L4/각도 아트) 대상.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# 자산 전체 맵 (Assets Overview) — Haruka
|
||||
|
||||
> ✅ **완성** — 리그·소스 이미지·Library·반응 런타임 모두 완비. 프로필 자립(소스 원본은 별도 아카이브).
|
||||
|
||||
## 자산 (전부 프로필 내)
|
||||
| 종류 | 개수 | 위치 |
|
||||
|---|---|---|
|
||||
| 시트 | 1 | `Reference/haruka_sheet.png` |
|
||||
| 리그 파츠(마스터+16 풀캔버스) | 17 | `Parts/Images/` — 피벗 산출·배경춤 검증 완료 |
|
||||
| 베이크드 포즈 | 54 | `Library/BakedPoses/` (Sailor·Idol·Witch 각 18) |
|
||||
| 레거시 파츠 | 15 | `Library/CoarseParts/` (의상별 5) |
|
||||
| 표정 프레임 | 20 (twin) | `Library/Heads/` (반응 head base `haruka_head_twin`) |
|
||||
| hairmask / 악세서리 | 1 / 8 | `Library/Hairmasks/` · `Accessories/`(마녀모자 포함) |
|
||||
| `_layout.json` | — | `06_Reactions/` (반응 목 정합) |
|
||||
|
||||
## 뷰어 (더블클릭 재생)
|
||||
- 배경춤: `07_Viewer/index.html`
|
||||
- 반응: `07_Viewer/reactions.html` — 트리거 idle / error / success
|
||||
|
||||
## dance 튜닝 (occlusion-aware)
|
||||
세일러 **블라우스↔스커트가 별개 의상** → `dance_idle`에서 **chest 리지드**(허리 봉인). 블라우스가 얇아 어깨 소켓 틈이 나기 쉬워 **팔 진폭 축소**(upperarm ±4), 생동감은 소매 안 forearm/hand로.
|
||||
|
||||
## 반응 매핑
|
||||
| 상황 | Body(baked) | Face |
|
||||
|---|---|---|
|
||||
| error | `haruka_body_sailor_armscross` | `haruka_head_twin_negative` |
|
||||
| success | `haruka_body_sailor_heart` | `haruka_head_twin_love` |
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 표정 & 베이크드 포즈 (Expressions & Poses)
|
||||
|
||||
반응 시퀀서의 **Face 레이어**(표정)와 **Body baked 모드**(포즈)가 쓰는 기존 자산 목록. 출처는 기존 Haruka 생성 md.
|
||||
|
||||
## 표정 프레임 20종 (Face 레이어)
|
||||
출처: `(소스 아카이브)`(및 neat 변형). 헤어모양별로 동일 세트.
|
||||
```
|
||||
neutral · blink · talk · talk_wide · smile · positive · negative · confused · wink ·
|
||||
surprised · laugh · thinking · cool · love · shy · sad · pout · sleepy · proud · playful
|
||||
```
|
||||
- **말하기**: `talk` ↔ `talk_wide` ↔ (해당 감정 프레임) 순환으로 입 움직임 근사.
|
||||
- **감정 표현**: 위 목록에서 상황에 맞는 프레임 선택.
|
||||
|
||||
## 베이크드 포즈 18종 (Body baked 모드)
|
||||
출처: `(소스 아카이브)` (표준 제스처 바디, 헤드리스). 파일 `haruka_body_sailor_<pose>`.
|
||||
```
|
||||
idle_full · idle_upper · wave · handwave · listen · present · dj · piano · control ·
|
||||
thumbsup · heart · clap · peace · armscross · shrug · point · cheer · joy
|
||||
```
|
||||
+ 파츠 5(apose·torso·arm_r·arm_l·legs) = 표준 23.
|
||||
|
||||
## 반응 3종이 쓰는 조합
|
||||
| 반응 | Body | Face(감정) | Mouth |
|
||||
|---|---|---|---|
|
||||
| gesture_no | baked `armscross` | `negative` (또는 `pout`) | "안돼요" (negative↔talk 순환) |
|
||||
| gesture_heart | baked `heart` | `love`/`positive`/`smile` | "잘됐어요" (love↔talk 순환) |
|
||||
| dance_idle | rig 16파츠 | `smile`/`neutral` | — |
|
||||
|
||||
## (옵션) 감정+talk 조합 머리 — 립싱크 강화용
|
||||
프레임 스왑 한계(L2)로 "감정 유지 + 입만 움직임"이 안 될 때만 소수 신규 생성.
|
||||
- 후보: `haruka_head_<shape>_negative_talk`, `haruka_head_<shape>_love_talk` (+ positive_talk)
|
||||
- **판단**: 먼저 표정↔talk 순환으로 충분한지 확인 후 결정(불충분하면 생성 또는 mesh-warp L4).
|
||||
|
||||
## 주의
|
||||
- Face 프레임은 **선택한 헤어모양 세트**와 일치해야 함(예: short 바디엔 short 표정).
|
||||
- 모든 프레임/포즈는 투명 알파 32-bit(`Format32bppArgb`).
|
||||
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 476 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
After Width: | Height: | Size: 463 KiB |
|
After Width: | Height: | Size: 402 KiB |
|
After Width: | Height: | Size: 674 KiB |
|
After Width: | Height: | Size: 596 KiB |
|
After Width: | Height: | Size: 748 KiB |
|
After Width: | Height: | Size: 817 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 742 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 666 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 966 KiB |
|
After Width: | Height: | Size: 909 KiB |
|
After Width: | Height: | Size: 918 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 974 KiB |
|
After Width: | Height: | Size: 914 KiB |
|
After Width: | Height: | Size: 917 KiB |
|
After Width: | Height: | Size: 562 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 498 KiB |
|
After Width: | Height: | Size: 864 KiB |
|
After Width: | Height: | Size: 988 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 926 KiB |
|
After Width: | Height: | Size: 867 KiB |
|
After Width: | Height: | Size: 926 KiB |
|
After Width: | Height: | Size: 852 KiB |
|
After Width: | Height: | Size: 876 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.2 MiB |