Initial Dansori character workspace

This commit is contained in:
eKeerar
2026-07-04 10:34:46 +09:00
commit 5a419816ff
2480 changed files with 38692 additions and 0 deletions
+173
View File
@@ -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);
}
}
+221
View File
@@ -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>
+16
View File
@@ -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;
};
}
}
+138
View File
@@ -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;
}
}
+220
View File
@@ -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);
}
}
}
}
+100
View File
@@ -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>
+194
View File
@@ -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>
+693
View File
@@ -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);
}
}
+53
View File
@@ -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")]
Binary file not shown.
@@ -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;
}
}
}
+74
View File
@@ -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": []
}