Asset compiler first iteration:
This commit is contained in:
187
SlaveEngine.AssetBuilder/Compiler.cs
Normal file
187
SlaveEngine.AssetBuilder/Compiler.cs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using SlaveEngine.AssetBuilder.Models;
|
||||||
|
using SlaveEngine.Assets;
|
||||||
|
using SlaveEngine.Assets.Models;
|
||||||
|
using SlaveEngine.Assets.Primitives;
|
||||||
|
using Spectre.Console;
|
||||||
|
using YamlDotNet.Serialization;
|
||||||
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
|
namespace SlaveEngine.AssetBuilder;
|
||||||
|
|
||||||
|
public class Compiler {
|
||||||
|
private readonly string _inputDir;
|
||||||
|
private readonly string _outputDir;
|
||||||
|
private readonly IDeserializer _yaml;
|
||||||
|
|
||||||
|
public Compiler(string inputDir, string outputDir) {
|
||||||
|
_inputDir = inputDir;
|
||||||
|
_outputDir = outputDir;
|
||||||
|
|
||||||
|
_yaml = new DeserializerBuilder()
|
||||||
|
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
||||||
|
.IgnoreUnmatchedProperties()
|
||||||
|
.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
private T LoadYaml<T>(string path) {
|
||||||
|
var text = File.ReadAllText(path);
|
||||||
|
text = NormalizeFlowArrays(text);
|
||||||
|
return _yaml.Deserialize<T>(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeFlowArrays(string yaml) {
|
||||||
|
return Regex.Replace(yaml,
|
||||||
|
@"(position:\s*)\[([\s\S]*?)\]",
|
||||||
|
m => {
|
||||||
|
var prefix = m.Groups[1].Value.TrimEnd();
|
||||||
|
var values = m.Groups[2].Value
|
||||||
|
.Replace('\n', ' ')
|
||||||
|
.Split(',', StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(v => v.Trim())
|
||||||
|
.Where(v => v.Length > 0)
|
||||||
|
.ToList();
|
||||||
|
if (values.Count == 0) return prefix + " []";
|
||||||
|
return prefix + "\n" +
|
||||||
|
string.Join("\n", values.Select(v => $" - {v}"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Run() {
|
||||||
|
AnsiConsole.Write(new Rule("[yellow]SlaveEngine Asset Builder[/]") { Justification = Justify.Left });
|
||||||
|
AnsiConsole.MarkupLine(" [bold]Input:[/] {0}", _inputDir);
|
||||||
|
AnsiConsole.MarkupLine(" [bold]Output:[/] {0}", _outputDir);
|
||||||
|
AnsiConsole.WriteLine();
|
||||||
|
|
||||||
|
Directory.CreateDirectory(_outputDir);
|
||||||
|
|
||||||
|
var parts = DiscoverParts();
|
||||||
|
if (parts.Count == 0) {
|
||||||
|
AnsiConsole.MarkupLine("[red]No parts found.[/]");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = 0;
|
||||||
|
|
||||||
|
AnsiConsole.Progress()
|
||||||
|
.Columns(new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn(), new ElapsedTimeColumn())
|
||||||
|
.Start(ctx => {
|
||||||
|
var task = ctx.AddTask("Compiling parts");
|
||||||
|
task.MaxValue = parts.Count;
|
||||||
|
|
||||||
|
foreach (var (id, partDir) in parts) {
|
||||||
|
var (ok, msg) = CompilePart(partDir);
|
||||||
|
if (ok)
|
||||||
|
AnsiConsole.MarkupLine("[green]{0}[/]", msg);
|
||||||
|
else
|
||||||
|
AnsiConsole.MarkupLine("[red]{0}[/]", msg);
|
||||||
|
if (ok) success++;
|
||||||
|
task.Increment(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (success == parts.Count)
|
||||||
|
AnsiConsole.MarkupLine("[green]Done. {0}/{1} compiled.[/]", success, parts.Count);
|
||||||
|
else if (success > 0)
|
||||||
|
AnsiConsole.MarkupLine("[yellow]Done. {0}/{1} compiled.[/]", success, parts.Count);
|
||||||
|
else
|
||||||
|
AnsiConsole.MarkupLine("[red]Done. {0}/{1} compiled.[/]", success, parts.Count);
|
||||||
|
|
||||||
|
return success == parts.Count ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<(string id, string dir)> DiscoverParts() {
|
||||||
|
var parts = new List<(string id, string dir)>();
|
||||||
|
|
||||||
|
var catalogPath = Path.Combine(_inputDir, "Catalog.yaml");
|
||||||
|
if (File.Exists(catalogPath)) {
|
||||||
|
AnsiConsole.MarkupLine("[dim]Using catalog:[/] {0}", catalogPath);
|
||||||
|
var catalog = LoadYaml<CatalogYaml>(catalogPath);
|
||||||
|
if (catalog?.Parts != null) {
|
||||||
|
foreach (var entry in catalog.Parts) {
|
||||||
|
var partDir = Path.GetFullPath(Path.Combine(_inputDir, entry.Path));
|
||||||
|
if (Directory.Exists(partDir))
|
||||||
|
parts.Add((entry.Id, partDir));
|
||||||
|
else
|
||||||
|
AnsiConsole.MarkupLine("[yellow] Warning:[/] directory not found: {0}", partDir);
|
||||||
|
}
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var scanDir = Path.Combine(_inputDir, "Parts");
|
||||||
|
if (!Directory.Exists(scanDir)) return parts;
|
||||||
|
|
||||||
|
AnsiConsole.MarkupLine("[dim]Scanning:[/] {0}", scanDir);
|
||||||
|
foreach (var dir in Directory.GetDirectories(scanDir)) {
|
||||||
|
var yamlPath = Path.Combine(dir, "part.yaml");
|
||||||
|
if (!File.Exists(yamlPath)) continue;
|
||||||
|
try {
|
||||||
|
var partYaml = LoadYaml<PartYaml>(yamlPath);
|
||||||
|
if (partYaml != null && !string.IsNullOrEmpty(partYaml.Id))
|
||||||
|
parts.Add((partYaml.Id, dir));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
AnsiConsole.MarkupLine("[yellow] Warning:[/] failed to parse {0}: {1}", yamlPath, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (bool success, string message) CompilePart(string partDir) {
|
||||||
|
var yamlPath = Path.Combine(partDir, "part.yaml");
|
||||||
|
if (!File.Exists(yamlPath))
|
||||||
|
return (false, $"Skipping (no part.yaml): {partDir}");
|
||||||
|
|
||||||
|
PartYaml partYaml;
|
||||||
|
try {
|
||||||
|
partYaml = LoadYaml<PartYaml>(yamlPath);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return (false, $"Failed to parse {yamlPath}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (partYaml == null)
|
||||||
|
return (false, $"Empty YAML: {yamlPath}");
|
||||||
|
|
||||||
|
try {
|
||||||
|
var variantList = partYaml.Variants ?? new List<VariantYaml>();
|
||||||
|
var variants = new VariantAsset[variantList.Count];
|
||||||
|
for (var i = 0; i < variantList.Count; i++) {
|
||||||
|
var v = variantList[i];
|
||||||
|
var svgPath = Path.Combine(partDir, v.File);
|
||||||
|
if (!File.Exists(svgPath)) {
|
||||||
|
variants[i] = new VariantAsset { X = v.X, Y = v.Y, Groups = Array.Empty<PathGroup>() };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var groups = SvgParser.Parse(svgPath);
|
||||||
|
variants[i] = new VariantAsset { X = v.X, Y = v.Y, Groups = groups };
|
||||||
|
}
|
||||||
|
|
||||||
|
var fieldList = partYaml.Fields ?? new List<FieldYaml>();
|
||||||
|
var jointList = partYaml.Joints ?? new List<JointYaml>();
|
||||||
|
|
||||||
|
var asset = new PartAsset {
|
||||||
|
Guid = Guid.NewGuid(),
|
||||||
|
FileInfo = new AssetFileInfo { Filename = partYaml.Id, FilePath = "" },
|
||||||
|
Id = partYaml.Id,
|
||||||
|
OriginalKey = partYaml.OriginalKey ?? "",
|
||||||
|
Resource = partYaml.Resource ?? "",
|
||||||
|
MorphX = partYaml.MorphX,
|
||||||
|
MorphY = partYaml.MorphY,
|
||||||
|
Fields = fieldList.Select(f => f.Name ?? "").ToArray(),
|
||||||
|
Joints = jointList.Select(j => new Joint {
|
||||||
|
X = j.Position != null && j.Position.Count > 0 ? j.Position[0] : 0,
|
||||||
|
Y = j.Position != null && j.Position.Count > 1 ? j.Position[1] : 0
|
||||||
|
}).ToArray(),
|
||||||
|
Variants = variants
|
||||||
|
};
|
||||||
|
|
||||||
|
var outputPath = Path.Combine(_outputDir, $"{partYaml.Id}.spart");
|
||||||
|
SparWriter.Write(asset, outputPath);
|
||||||
|
|
||||||
|
return (true, $" {partYaml.Id}... OK ({variants.Length} variants)");
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return (false, $" {partYaml.Id}... FAILED: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
SlaveEngine.AssetBuilder/Models/YamlModels.cs
Normal file
35
SlaveEngine.AssetBuilder/Models/YamlModels.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
namespace SlaveEngine.AssetBuilder.Models;
|
||||||
|
|
||||||
|
public class PartYaml {
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string OriginalKey { get; set; } = "";
|
||||||
|
public string Resource { get; set; } = "";
|
||||||
|
public int MorphX { get; set; }
|
||||||
|
public int MorphY { get; set; }
|
||||||
|
public List<VariantYaml>? Variants { get; set; }
|
||||||
|
public List<FieldYaml>? Fields { get; set; }
|
||||||
|
public List<JointYaml>? Joints { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VariantYaml {
|
||||||
|
public int X { get; set; }
|
||||||
|
public int Y { get; set; }
|
||||||
|
public string File { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class FieldYaml {
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public class JointYaml {
|
||||||
|
public List<float>? Position { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogYaml {
|
||||||
|
public List<CatalogEntry>? Parts { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CatalogEntry {
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
public string Path { get; set; } = "";
|
||||||
|
}
|
||||||
32
SlaveEngine.AssetBuilder/Program.cs
Normal file
32
SlaveEngine.AssetBuilder/Program.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using Spectre.Console;
|
||||||
|
|
||||||
|
namespace SlaveEngine.AssetBuilder;
|
||||||
|
|
||||||
|
class Program {
|
||||||
|
static int Main(string[] args) {
|
||||||
|
var inputOption = new Option<DirectoryInfo>("--input", "Path to the input SlaveMatrix/Assets folder");
|
||||||
|
inputOption.SetDefaultValueFactory(() => new DirectoryInfo("./SlaveMatrix/Assets"));
|
||||||
|
|
||||||
|
var outputOption = new Option<DirectoryInfo>("--output", "Path to the output directory for compiled .spart files");
|
||||||
|
outputOption.SetDefaultValueFactory(() => new DirectoryInfo("./Assets"));
|
||||||
|
|
||||||
|
var rootCommand = new RootCommand(
|
||||||
|
"SlaveEngine Asset Builder — compiles YAML + SVG assets into .spart binaries");
|
||||||
|
rootCommand.AddOption(inputOption);
|
||||||
|
rootCommand.AddOption(outputOption);
|
||||||
|
|
||||||
|
rootCommand.SetHandler((DirectoryInfo input, DirectoryInfo output) => {
|
||||||
|
if (!input.Exists) {
|
||||||
|
AnsiConsole.MarkupLine("[red]Error:[/] input directory not found: {0}", input.FullName);
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var compiler = new Compiler(input.FullName, output.FullName);
|
||||||
|
var result = compiler.Run();
|
||||||
|
Environment.Exit(result);
|
||||||
|
}, inputOption, outputOption);
|
||||||
|
|
||||||
|
return rootCommand.Invoke(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
20
SlaveEngine.AssetBuilder/SlaveEngine.AssetBuilder.csproj
Normal file
20
SlaveEngine.AssetBuilder/SlaveEngine.AssetBuilder.csproj
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SlaveEngine.Assets\SlaveEngine.Assets.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Spectre.Console" Version="0.57.0" />
|
||||||
|
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||||
|
<PackageReference Include="YamlDotNet" Version="18.0.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
73
SlaveEngine.AssetBuilder/SparWriter.cs
Normal file
73
SlaveEngine.AssetBuilder/SparWriter.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using System.Text;
|
||||||
|
using SlaveEngine.Assets;
|
||||||
|
using SlaveEngine.Assets.Models;
|
||||||
|
using SlaveEngine.Assets.Primitives;
|
||||||
|
|
||||||
|
namespace SlaveEngine.AssetBuilder;
|
||||||
|
|
||||||
|
public static class SparWriter {
|
||||||
|
public static void Write(PartAsset asset, string outputPath) {
|
||||||
|
using var stream = File.Create(outputPath);
|
||||||
|
using var writer = new BinaryWriter(stream);
|
||||||
|
|
||||||
|
writer.Write(Encoding.UTF8.GetBytes("SPRT"));
|
||||||
|
writer.Write(1); // version
|
||||||
|
|
||||||
|
WriteString(writer, asset.Id);
|
||||||
|
WriteString(writer, asset.OriginalKey);
|
||||||
|
WriteString(writer, asset.Resource);
|
||||||
|
writer.Write(asset.MorphX);
|
||||||
|
writer.Write(asset.MorphY);
|
||||||
|
|
||||||
|
writer.Write(asset.Fields.Length);
|
||||||
|
foreach (var f in asset.Fields) WriteString(writer, f);
|
||||||
|
|
||||||
|
writer.Write(asset.Joints.Length);
|
||||||
|
foreach (var j in asset.Joints) {
|
||||||
|
writer.Write(j.X);
|
||||||
|
writer.Write(j.Y);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(asset.Variants.Length);
|
||||||
|
foreach (var variant in asset.Variants) {
|
||||||
|
writer.Write(variant.X);
|
||||||
|
writer.Write(variant.Y);
|
||||||
|
|
||||||
|
writer.Write(variant.Groups.Length);
|
||||||
|
foreach (var group in variant.Groups) {
|
||||||
|
WriteString(writer, group.Name);
|
||||||
|
writer.Write(group.HasTransform);
|
||||||
|
if (group.HasTransform) {
|
||||||
|
writer.Write(group.Tx);
|
||||||
|
writer.Write(group.Ty);
|
||||||
|
writer.Write(group.Angle);
|
||||||
|
writer.Write(group.Sx);
|
||||||
|
writer.Write(group.Sy);
|
||||||
|
writer.Write(group.Bx);
|
||||||
|
writer.Write(group.By);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(group.Paths.Length);
|
||||||
|
foreach (var path in group.Paths) {
|
||||||
|
WriteString(writer, path.Fill);
|
||||||
|
WriteString(writer, path.Stroke);
|
||||||
|
writer.Write(path.StrokeWidth);
|
||||||
|
writer.Write(path.IsClosed);
|
||||||
|
|
||||||
|
writer.Write(path.Commands.Length);
|
||||||
|
foreach (var cmd in path.Commands) {
|
||||||
|
writer.Write((byte)cmd.Type);
|
||||||
|
writer.Write(cmd.Args.Length);
|
||||||
|
foreach (var arg in cmd.Args) writer.Write(arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteString(BinaryWriter writer, string s) {
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(s);
|
||||||
|
writer.Write(bytes.Length);
|
||||||
|
writer.Write(bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
192
SlaveEngine.AssetBuilder/SvgParser.cs
Normal file
192
SlaveEngine.AssetBuilder/SvgParser.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using SlaveEngine.Assets.Models;
|
||||||
|
using SlaveEngine.Assets.Primitives;
|
||||||
|
|
||||||
|
namespace SlaveEngine.AssetBuilder;
|
||||||
|
|
||||||
|
public static class SvgParser {
|
||||||
|
private static readonly CultureInfo Inv = CultureInfo.InvariantCulture;
|
||||||
|
|
||||||
|
public static PathGroup[] Parse(string svgPath) {
|
||||||
|
var doc = XDocument.Load(svgPath);
|
||||||
|
var svg = doc.Root;
|
||||||
|
if (svg == null) return Array.Empty<PathGroup>();
|
||||||
|
|
||||||
|
var groups = new List<PathGroup>();
|
||||||
|
CollectGroups(svg, groups, null);
|
||||||
|
return groups.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CollectGroups(XElement parent, List<PathGroup> groups, string? inheritedName) {
|
||||||
|
foreach (var child in parent.Elements()) {
|
||||||
|
var name = child.Name.LocalName;
|
||||||
|
if (name != "g" && name != "path") continue;
|
||||||
|
var id = child.Attribute("id")?.Value ?? inheritedName ?? "";
|
||||||
|
|
||||||
|
if (name == "g") {
|
||||||
|
var transformAttr = child.Attribute("transform")?.Value;
|
||||||
|
|
||||||
|
if (transformAttr != null) {
|
||||||
|
ParseTransform(transformAttr,
|
||||||
|
out var tx, out var ty, out var angle,
|
||||||
|
out var sx, out var sy, out var bx, out var by);
|
||||||
|
|
||||||
|
var paths = CollectPaths(child);
|
||||||
|
groups.Add(new PathGroup {
|
||||||
|
Name = id,
|
||||||
|
HasTransform = true,
|
||||||
|
Tx = tx, Ty = ty, Angle = angle,
|
||||||
|
Sx = sx, Sy = sy, Bx = bx, By = by,
|
||||||
|
Paths = paths
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
CollectGroups(child, groups, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PathData[] CollectPaths(XElement parent) {
|
||||||
|
var paths = new List<PathData>();
|
||||||
|
foreach (var elem in parent.Elements()) {
|
||||||
|
var name = elem.Name.LocalName;
|
||||||
|
if (name == "path") {
|
||||||
|
var d = elem.Attribute("d")?.Value ?? "";
|
||||||
|
var (commands, isClosed) = ParsePathData(d);
|
||||||
|
paths.Add(new PathData {
|
||||||
|
Fill = elem.Attribute("fill")?.Value ?? "none",
|
||||||
|
Stroke = elem.Attribute("stroke")?.Value ?? "none",
|
||||||
|
StrokeWidth = float.Parse(
|
||||||
|
elem.Attribute("stroke-width")?.Value ?? "0", Inv),
|
||||||
|
IsClosed = isClosed,
|
||||||
|
Commands = commands
|
||||||
|
});
|
||||||
|
} else if (name == "g") {
|
||||||
|
paths.AddRange(CollectPaths(elem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseTransform(string t,
|
||||||
|
out float tx, out float ty, out float angle,
|
||||||
|
out float sx, out float sy, out float bx, out float by) {
|
||||||
|
tx = ty = angle = 0;
|
||||||
|
sx = sy = 1;
|
||||||
|
bx = by = 0;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(t)) return;
|
||||||
|
|
||||||
|
var parts = t.Split(')', StringSplitOptions.RemoveEmptyEntries);
|
||||||
|
foreach (var part in parts) {
|
||||||
|
var trimmed = part.TrimStart();
|
||||||
|
if (trimmed.StartsWith("translate(")) {
|
||||||
|
var args = trimmed["translate(".Length..].Split(',');
|
||||||
|
if (args.Length >= 2) {
|
||||||
|
tx = float.Parse(args[0].Trim(), Inv);
|
||||||
|
ty = float.Parse(args[1].Trim(), Inv);
|
||||||
|
}
|
||||||
|
} else if (trimmed.StartsWith("rotate(")) {
|
||||||
|
var arg = trimmed["rotate(".Length..].Trim();
|
||||||
|
angle = float.Parse(arg, Inv);
|
||||||
|
} else if (trimmed.StartsWith("scale(")) {
|
||||||
|
var args = trimmed["scale(".Length..].Split(',');
|
||||||
|
if (args.Length >= 2) {
|
||||||
|
sx = float.Parse(args[0].Trim(), Inv);
|
||||||
|
sy = float.Parse(args[1].Trim(), Inv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static (BezierCommand[] commands, bool isClosed) ParsePathData(string d) {
|
||||||
|
var commands = new List<BezierCommand>();
|
||||||
|
var tokens = Tokenize(d);
|
||||||
|
var i = 0;
|
||||||
|
float cx = 0, cy = 0;
|
||||||
|
|
||||||
|
while (i < tokens.Count) {
|
||||||
|
var cmd = tokens[i][0];
|
||||||
|
i++;
|
||||||
|
|
||||||
|
switch (cmd) {
|
||||||
|
case 'M': case 'm': {
|
||||||
|
var x = float.Parse(tokens[i++], Inv);
|
||||||
|
var y = float.Parse(tokens[i++], Inv);
|
||||||
|
if (cmd == 'm') { x += cx; y += cy; }
|
||||||
|
commands.Add(new BezierCommand { Type = CommandType.MoveTo, Args = new[] { x, y } });
|
||||||
|
cx = x; cy = y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'L': case 'l': {
|
||||||
|
var x = float.Parse(tokens[i++], Inv);
|
||||||
|
var y = float.Parse(tokens[i++], Inv);
|
||||||
|
if (cmd == 'l') { x += cx; y += cy; }
|
||||||
|
commands.Add(new BezierCommand { Type = CommandType.LineTo, Args = new[] { x, y } });
|
||||||
|
cx = x; cy = y;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'C': case 'c': {
|
||||||
|
var c1x = float.Parse(tokens[i++], Inv);
|
||||||
|
var c1y = float.Parse(tokens[i++], Inv);
|
||||||
|
var c2x = float.Parse(tokens[i++], Inv);
|
||||||
|
var c2y = float.Parse(tokens[i++], Inv);
|
||||||
|
var ex = float.Parse(tokens[i++], Inv);
|
||||||
|
var ey = float.Parse(tokens[i++], Inv);
|
||||||
|
if (cmd == 'c') {
|
||||||
|
c1x += cx; c1y += cy;
|
||||||
|
c2x += cx; c2y += cy;
|
||||||
|
ex += cx; ey += cy;
|
||||||
|
}
|
||||||
|
commands.Add(new BezierCommand {
|
||||||
|
Type = CommandType.CubicBezierTo,
|
||||||
|
Args = new[] { c1x, c1y, c2x, c2y, ex, ey }
|
||||||
|
});
|
||||||
|
cx = ex; cy = ey;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'Z': case 'z': {
|
||||||
|
commands.Add(new BezierCommand { Type = CommandType.Close, Args = Array.Empty<float>() });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var closed = commands.Count > 0 && commands[^1].Type == CommandType.Close;
|
||||||
|
return (commands.ToArray(), closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> Tokenize(string d) {
|
||||||
|
var tokens = new List<string>();
|
||||||
|
var i = 0;
|
||||||
|
while (i < d.Length) {
|
||||||
|
var c = d[i];
|
||||||
|
if (char.IsWhiteSpace(c) || c == ',') { i++; continue; }
|
||||||
|
if (char.IsLetter(c)) {
|
||||||
|
var upper = char.ToUpperInvariant(c);
|
||||||
|
if (upper is 'M' or 'L' or 'C' or 'Z') {
|
||||||
|
tokens.Add(c.ToString());
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c is '-' or '+' or '.' || char.IsDigit(c)) {
|
||||||
|
var start = i;
|
||||||
|
if (c is '-' or '+') i++;
|
||||||
|
while (i < d.Length && (char.IsDigit(d[i]) || d[i] == '.')) i++;
|
||||||
|
if (i < d.Length && (d[i] is 'e' or 'E')) {
|
||||||
|
i++;
|
||||||
|
if (i < d.Length && (d[i] is '+' or '-')) i++;
|
||||||
|
while (i < d.Length && char.IsDigit(d[i])) i++;
|
||||||
|
}
|
||||||
|
tokens.Add(d[start..i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
namespace SlaveEngine.Assets;
|
|
||||||
|
|
||||||
public class VectorAsset : Asset {
|
|
||||||
//TODO: insert the data from an SVG here.
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace SlaveEngine.Assets.Processors;
|
|
||||||
|
|
||||||
[AssetProcessor(typeof(VectorAsset), managedExtensions: [".svg"])]
|
|
||||||
public sealed class SVGProcessor : AssetProcessor{
|
|
||||||
|
|
||||||
public override Asset Process(AssetFileInfo fileInfo, byte[] data)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +1,78 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
namespace SlaveEngine.Assets;
|
namespace SlaveEngine.Assets;
|
||||||
|
|
||||||
public sealed class ResourceManager {
|
public sealed class ResourceManager {
|
||||||
|
|
||||||
public ResourceManager(string basePath)
|
public ResourceManager(string basePath) {
|
||||||
{
|
|
||||||
if (!Path.Exists(basePath))
|
if (!Path.Exists(basePath))
|
||||||
throw new DirectoryNotFoundException($"The base path '{basePath}' does not exist.");
|
throw new DirectoryNotFoundException($"The base path '{basePath}' does not exist.");
|
||||||
BasePath = basePath;
|
BasePath = basePath;
|
||||||
Instance = this;
|
Instance = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private AssetProcessor? GetProcessorForExtension(string extension) =>
|
|
||||||
_assetProcessors.FirstOrDefault(p => (bool)p.GetType()
|
|
||||||
.GetCustomAttribute<AssetProcessorAttribute>()?.ManagedExtensions
|
|
||||||
.Contains(extension)
|
|
||||||
);
|
|
||||||
|
|
||||||
public static ResourceManager? Instance { get; private set; }
|
public static ResourceManager? Instance { get; private set; }
|
||||||
|
|
||||||
private List<AssetProcessor> _assetProcessors = new();
|
|
||||||
|
|
||||||
public string BasePath { get; private set; }
|
public string BasePath { get; private set; }
|
||||||
|
|
||||||
public void Initialize(string basePath)
|
private readonly List<AssetProcessor> _assetProcessors = new();
|
||||||
{
|
private readonly Dictionary<string, string> _assetIndex = new();
|
||||||
ScanDirectory(basePath);
|
private readonly Dictionary<string, Asset> _assetCache = new();
|
||||||
|
|
||||||
|
public void Initialize() {
|
||||||
|
ScanDirectory(BasePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ScanDirectory(string directory)
|
private void ScanDirectory(string directory) {
|
||||||
{
|
foreach (var file in Directory.GetFiles(directory, "*.spart")) {
|
||||||
var files = Directory.GetFiles(directory);
|
try {
|
||||||
foreach (var file in files)
|
var id = ReadAssetIdFromSpart(file);
|
||||||
{
|
_assetIndex[id] = file;
|
||||||
var ext = Path.GetExtension(file);
|
} catch {
|
||||||
// check if we can process this extension
|
// skip corrupt files
|
||||||
var processor = GetProcessorForExtension(ext);
|
}
|
||||||
if (processor == null) continue;
|
|
||||||
|
|
||||||
var name = Path.GetFileNameWithoutExtension(file);
|
|
||||||
AssetFileInfo fileInfo = new()
|
|
||||||
{
|
|
||||||
Filename = name,
|
|
||||||
FilePath = file
|
|
||||||
};
|
|
||||||
|
|
||||||
processor.Process(fileInfo, File.ReadAllBytes(file));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RegisterProcessor(AssetProcessor processor)
|
public T? GetAsset<T>(string id) where T : Asset {
|
||||||
{
|
if (_assetCache.TryGetValue(id, out var cached))
|
||||||
//make sure the processor for this type isn't already in:
|
return (T)cached;
|
||||||
var ap = _assetProcessors.FirstOrDefault(p => p.GetType() == processor.GetType());
|
|
||||||
if (ap == null) _assetProcessors.Add(processor);
|
if (!_assetIndex.TryGetValue(id, out var path))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var ext = Path.GetExtension(path);
|
||||||
|
var processor = GetProcessorForExtension(ext);
|
||||||
|
if (processor == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var fileInfo = new AssetFileInfo {
|
||||||
|
Filename = Path.GetFileNameWithoutExtension(path),
|
||||||
|
FilePath = path
|
||||||
|
};
|
||||||
|
|
||||||
|
var asset = processor.Process(fileInfo, File.ReadAllBytes(path));
|
||||||
|
_assetCache[id] = asset;
|
||||||
|
return (T)asset;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public IReadOnlyCollection<string> GetAssetIds() => _assetIndex.Keys;
|
||||||
|
|
||||||
|
public void RegisterProcessor(AssetProcessor processor) {
|
||||||
|
if (_assetProcessors.All(p => p.GetType() != processor.GetType()))
|
||||||
|
_assetProcessors.Add(processor);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AssetProcessor? GetProcessorForExtension(string extension) =>
|
||||||
|
_assetProcessors.FirstOrDefault(p => p.GetType()
|
||||||
|
.GetCustomAttribute<AssetProcessorAttribute>()?.ManagedExtensions
|
||||||
|
.Contains(extension) == true);
|
||||||
|
|
||||||
|
private static string ReadAssetIdFromSpart(string path) {
|
||||||
|
using var stream = File.OpenRead(path);
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
reader.ReadBytes(8);
|
||||||
|
var len = reader.ReadInt32();
|
||||||
|
return Encoding.UTF8.GetString(reader.ReadBytes(len));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
14
Solution.sln
14
Solution.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaveMatrix.Extract", "Slav
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaveEngine.Assets", "SlaveEngine.Assets\SlaveEngine.Assets.csproj", "{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaveEngine.Assets", "SlaveEngine.Assets\SlaveEngine.Assets.csproj", "{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SlaveEngine.AssetBuilder", "SlaveEngine.AssetBuilder\SlaveEngine.AssetBuilder.csproj", "{DE1800EA-9AB1-4558-BB60-40BAA505F475}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|x64 = Debug|x64
|
Debug|x64 = Debug|x64
|
||||||
@@ -69,6 +71,18 @@ Global
|
|||||||
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|x86.Build.0 = Release|Any CPU
|
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
{11B3B1C3-B393-4A4D-AEC5-B26C86F6C0C6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{DE1800EA-9AB1-4558-BB60-40BAA505F475}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Reference in New Issue
Block a user