Files
2026-07-04 10:34:46 +09:00

101 lines
4.4 KiB
C#

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);
}