Skip to content

Commit 2758c95

Browse files
committed
refactor: Refactor rendering engine, introduce new layout types and simplify expression engine
- Add layout-related types including GridNode, StackDirection, and AlignItems - Introduce AsyncNCalcEngine to replace the legacy NCalcExpressionEngine and simplify expression evaluation - Refactor XmlSceneLoader into TemplateReader to support more flexible template parsing - Merge IAssetProvider and IMeasureService into AssetProvider - Add modular implementation of NodeRenderer to separate rendering, measurement, and layout logic - Optimize scope handling and template data binding for the Drawer class
1 parent 74ffe45 commit 2758c95

27 files changed

+1390
-806
lines changed
Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66

77
namespace Limekuma.Render;
88

9-
public sealed class AssetProvider : IAssetProvider, IMeasureService
9+
public sealed class AssetProvider
1010
{
11-
private readonly ConcurrentDictionary<string, Image> _assets;
12-
private readonly FontCollection _fontCollection;
13-
private readonly ConcurrentDictionary<string, string> _fontFamilyNames;
11+
private readonly ConcurrentDictionary<string, LoadedFont> _fontCache;
1412
private readonly Dictionary<string, (string, List<string>?)> _fontRules;
1513
private readonly Dictionary<string, (string, string?)> _pathRules;
1614

@@ -22,9 +20,7 @@ public AssetProvider(string resourcePath)
2220
{
2321
_fontRules = [];
2422
_pathRules = [];
25-
_assets = [];
26-
_fontCollection = new();
27-
_fontFamilyNames = [];
23+
_fontCache = [];
2824
LoadResources(resourcePath);
2925
}
3026

@@ -33,7 +29,7 @@ public AssetProvider(string resourcePath)
3329
public Image LoadImage(string ns, string key)
3430
{
3531
string path = ResolveResourcePath(ns, key);
36-
return LoadImage(path);
32+
return Image.Load(path);
3733
}
3834

3935
public (FontFamily, List<FontFamily>) ResolveFont(string key)
@@ -57,23 +53,25 @@ public Image LoadImage(string ns, string key)
5753
public Size Measure(string text, string family, float size)
5854
{
5955
(FontFamily fontFamily, List<FontFamily> fallbacks) = ResolveFont(family);
60-
Font font = fontFamily.CreateFont(size);
61-
FontRectangle rect = TextMeasurer.MeasureAdvance(text, new(font)
56+
float scaledSize = (float)(size * 72 / 300d);
57+
Font font = fontFamily.CreateFont(scaledSize);
58+
FontRectangle rect = TextMeasurer.MeasureAdvance(text, new TextOptions(font)
6259
{
63-
FallbackFontFamilies = fallbacks
60+
FallbackFontFamilies = fallbacks,
61+
Dpi = 300
6462
});
6563
return new((int)Math.Ceiling(rect.Width), (int)Math.Ceiling(rect.Height));
6664
}
6765

68-
public string? GetPath(string key)
66+
public string? GetPath(string ns)
6967
{
70-
if (!_pathRules.TryGetValue(key, out (string, string?) pathRule))
68+
if (!_pathRules.TryGetValue(ns, out (string, string?) pathRule))
7169
{
7270
return null;
7371
}
7472

7573
(string path, _) = pathRule;
76-
return path;
74+
return Path.GetFullPath(path);
7775
}
7876

7977
private void LoadResources(string resourcePath)
@@ -93,9 +91,9 @@ private void LoadPathRules(XElement resourcesNode)
9391
{
9492
foreach (XElement node in resourcesNode.Elements("Path"))
9593
{
96-
string? key = node.Attribute("key")?.Value;
94+
string? ns = node.Attribute("namespace")?.Value;
9795
string path = node.Value;
98-
if (string.IsNullOrEmpty(key))
96+
if (string.IsNullOrEmpty(ns))
9997
{
10098
continue;
10199
}
@@ -105,17 +103,17 @@ private void LoadPathRules(XElement resourcesNode)
105103
continue;
106104
}
107105

108-
_pathRules[key] = new(path, node.Attribute("rule")?.Value);
106+
_pathRules[ns] = new(Path.GetFullPath(path), node.Attribute("rule")?.Value);
109107
}
110108
}
111109

112110
private void LoadFontRules(XElement resourcesNode)
113111
{
114112
foreach (XElement node in resourcesNode.Elements("FontFamily"))
115113
{
116-
string? key = node.Attribute("key")?.Value;
114+
string? ns = node.Attribute("namespace")?.Value;
117115
string? fontPath = node.Element("Font")?.Value;
118-
if (string.IsNullOrEmpty(key))
116+
if (string.IsNullOrEmpty(ns))
119117
{
120118
continue;
121119
}
@@ -128,21 +126,21 @@ private void LoadFontRules(XElement resourcesNode)
128126
XElement? fallbacks = node.Element("Fallbacks");
129127
if (fallbacks is null)
130128
{
131-
_fontRules[key] = new(fontPath, null);
129+
_fontRules[ns] = new(fontPath, null);
132130
continue;
133131
}
134132

135133
List<string> fallbackPaths =
136134
[.. fallbacks.Elements("Font").Select(e => e.Value).Where(p => !string.IsNullOrEmpty(p))];
137-
_fontRules[key] = new(fontPath, fallbackPaths);
135+
_fontRules[ns] = new(fontPath, fallbackPaths);
138136
}
139137
}
140138

141139
private string ResolveResourcePath(string ns, string key)
142140
{
143141
if (!_pathRules.TryGetValue(ns, out (string, string?) pathRule))
144142
{
145-
return key;
143+
return Path.GetFullPath(key);
146144
}
147145

148146
(string path, string? rule) = pathRule;
@@ -154,27 +152,17 @@ private string ResolveResourcePath(string ns, string key)
154152
return Path.Combine(path, Smart.Format(rule, new { key }));
155153
}
156154

157-
private Image LoadImage(string path)
158-
{
159-
if (!_assets.TryGetValue(path, out Image? image))
160-
{
161-
image = Image.Load(path);
162-
_assets.TryAdd(path, image);
163-
}
164-
165-
return image;
166-
}
167-
168155
private FontFamily LoadFont(string path)
169156
{
170-
if (_fontFamilyNames.TryGetValue(path, out string? fontName) &&
171-
_fontCollection.TryGet(fontName, out FontFamily fontFamily))
157+
string fullPath = Path.GetFullPath(path);
158+
LoadedFont loaded = _fontCache.GetOrAdd(fullPath, p =>
172159
{
173-
return fontFamily;
174-
}
175-
176-
fontFamily = _fontCollection.Add(path);
177-
_fontFamilyNames.TryAdd(path, fontFamily.Name);
178-
return fontFamily;
160+
FontCollection collection = new();
161+
FontFamily family = collection.Add(p);
162+
return new(collection, family);
163+
});
164+
return loaded.Family;
179165
}
166+
167+
private sealed record LoadedFont(FontCollection Collection, FontFamily Family);
180168
}

src/Render/Drawer.cs

Lines changed: 42 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
using Limekuma.Render.ExpressionEngine;
33
using Limekuma.Render.Nodes;
44
using SixLabors.ImageSharp;
5-
using System.Collections;
65

76
namespace Limekuma.Render;
87

9-
public class Drawer
8+
public sealed class Drawer
109
{
1110
public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecord> ever,
1211
IEnumerable<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober) =>
@@ -26,17 +25,40 @@ public async Task<Image> DrawBestsAsync(CommonUser user, IEnumerable<CommonRecor
2625
IEnumerable<CommonRecord> current, int everTotal, int currentTotal, string typename, string prober,
2726
bool isAnime, bool drawLevelSeg, string xmlPath)
2827
{
29-
Dictionary<string, object> scope = new()
28+
List<CommonRecord> everList = [.. ever];
29+
List<CommonRecord> currentList = [.. current];
30+
List<object> everCards = [.. everList.Select((record, idx) => new { Record = record, Index = idx + 1 })];
31+
List<object> currentCards =
32+
[.. currentList.Select((record, idx) => new { Record = record, Index = idx + everList.Count + 1 })];
33+
int everDelta = everList.Count > 34 ? everList[0].DXRating - everList[^1].DXRating : everList.Count > 0 ? everList[0].DXRating : 0;
34+
int currentDelta = currentList.Count > 14 ? currentList[0].DXRating - currentList[^1].DXRating : currentList.Count > 0 ? currentList[0].DXRating : 0;
35+
int realRating = everTotal + currentTotal;
36+
string proberState = "on";
37+
if (user.Rating != realRating)
38+
{
39+
proberState = "off";
40+
}
41+
else if (everList.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank < Ranks.C)) ||
42+
currentList.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank < Ranks.C)))
43+
{
44+
proberState = "warning";
45+
}
46+
47+
Dictionary<string, object?> scope = new(StringComparer.OrdinalIgnoreCase)
3048
{
3149
["user"] = user,
32-
["ever"] = ever,
33-
["current"] = current,
50+
["everCards"] = everCards,
51+
["currentCards"] = currentCards,
52+
["everDelta"] = everDelta,
53+
["currentDelta"] = currentDelta,
3454
["everTotal"] = everTotal,
3555
["currentTotal"] = currentTotal,
56+
["realRating"] = realRating,
3657
["typename"] = typename,
3758
["prober"] = prober,
3859
["isAnime"] = isAnime,
3960
["drawLevelSeg"] = drawLevelSeg,
61+
["proberState"] = proberState,
4062
};
4163
return await DrawAsync(scope, xmlPath);
4264
}
@@ -48,35 +70,32 @@ public async Task<Image> DrawListAsync(CommonUser user, IEnumerable<CommonRecord
4870
public async Task<Image> DrawListAsync(CommonUser user, IEnumerable<CommonRecord> records, int page, int total,
4971
IEnumerable<int> counts, string level, string prober, string xmlPath)
5072
{
51-
Dictionary<string, object> scope = new()
73+
List<CommonRecord> list = [.. records];
74+
List<object> recordCards = [.. list.Select((record, idx) => new { Record = record, Index = idx + 1 })];
75+
List<int> countList = counts.ToList();
76+
int totalCount = countList.Count > 0 ? countList[^1] : 0;
77+
bool warning = list.Any(r => r.DXScore is 0 && (r.DXStar > 0 || r.Rank < Ranks.C));
78+
Dictionary<string, object?> scope = new(StringComparer.OrdinalIgnoreCase)
5279
{
5380
["user"] = user,
54-
["records"] = records,
81+
["recordCards"] = recordCards,
5582
["page"] = page,
5683
["total"] = total,
57-
["counts"] = counts,
84+
["counts"] = countList,
85+
["statsTotalCount"] = totalCount,
5886
["level"] = level,
59-
["prober"] = prober
87+
["prober"] = prober,
88+
["proberState"] = warning ? "warning" : "on",
6089
};
6190
return await DrawAsync(scope, xmlPath);
6291
}
6392

64-
private async Task<Image> DrawAsync(Dictionary<string, object> scope, string xmlPath)
93+
private async Task<Image> DrawAsync(Dictionary<string, object?> scope, string xmlPath)
6594
{
66-
IAsyncExpressionEngine expr = new NCalcExpressionEngine();
67-
expr.RegisterFunction("Reverse", (object l) =>
68-
{
69-
if (l is IEnumerable<object> e)
70-
{
71-
return e.Reverse();
72-
}
73-
74-
return (IEnumerable)(l.ToString() ?? "[NIL]").Reverse();
75-
});
76-
XmlSceneLoader loader = new(expr);
95+
AsyncNCalcEngine expr = new();
96+
TemplateReader loader = new(expr);
7797
Node tree = await loader.LoadAsync(xmlPath, scope);
7898
AssetProvider assets = AssetProvider.Shared;
79-
Image image = Renderer.Render((CanvasNode)tree, assets, assets);
80-
return image;
99+
return NodeRenderer.Render((CanvasNode)tree, assets, assets);
81100
}
82101
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using NCalc;
2+
using System.Collections;
3+
using System.Globalization;
4+
5+
namespace Limekuma.Render.ExpressionEngine;
6+
7+
public sealed class AsyncNCalcEngine
8+
{
9+
public async Task<T?> EvalAsync<T>(string expr, object? scope)
10+
{
11+
object? result = await EvalAsync(expr, scope);
12+
if (result is T t)
13+
{
14+
return t;
15+
}
16+
17+
if (result is null)
18+
{
19+
return default;
20+
}
21+
22+
if (typeof(T) == typeof(IEnumerable<object>) && result is IEnumerable enumerable)
23+
{
24+
List<object> list = [];
25+
foreach (object? item in enumerable)
26+
{
27+
if (item is not null)
28+
{
29+
list.Add(item);
30+
}
31+
}
32+
33+
return (T)(object)list;
34+
}
35+
36+
return (T?)ConvertValue(result, typeof(T));
37+
}
38+
39+
public async Task<object?> EvalAsync(string expr, object? scope)
40+
{
41+
AsyncExpression expression = new(expr);
42+
Dictionary<string, object?> flattened = ScopeFlattener.Flatten(scope);
43+
foreach ((string key, object? value) in flattened)
44+
{
45+
expression.Parameters[key] = value is Enum e ? Convert.ToInt32(e, CultureInfo.InvariantCulture) : value;
46+
}
47+
48+
return await expression.EvaluateAsync();
49+
}
50+
51+
private static object? ConvertValue(object? value, Type targetType)
52+
{
53+
if (value is null)
54+
{
55+
return targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
56+
}
57+
58+
Type finalTarget = Nullable.GetUnderlyingType(targetType) ?? targetType;
59+
if (finalTarget.IsInstanceOfType(value))
60+
{
61+
return value;
62+
}
63+
64+
if (finalTarget.IsEnum)
65+
{
66+
if (value is string enumText)
67+
{
68+
return Enum.Parse(finalTarget, enumText, true);
69+
}
70+
71+
object enumValue = Convert.ChangeType(value, Enum.GetUnderlyingType(finalTarget), CultureInfo.InvariantCulture);
72+
return Enum.ToObject(finalTarget, enumValue);
73+
}
74+
75+
if (finalTarget == typeof(bool))
76+
{
77+
if (value is string sv)
78+
{
79+
if (bool.TryParse(sv, out bool b))
80+
{
81+
return b;
82+
}
83+
84+
if (double.TryParse(sv, NumberStyles.Float, CultureInfo.InvariantCulture, out double d))
85+
{
86+
return Math.Abs(d) > 0.0000001;
87+
}
88+
}
89+
90+
if (value is IConvertible convertible)
91+
{
92+
return Math.Abs(convertible.ToDouble(CultureInfo.InvariantCulture)) > 0.0000001;
93+
}
94+
}
95+
96+
if (value is IConvertible)
97+
{
98+
return Convert.ChangeType(value, finalTarget, CultureInfo.InvariantCulture);
99+
}
100+
101+
return finalTarget == typeof(string) ? value.ToString() : value;
102+
}
103+
}

src/Render/ExpressionEngine/IAsyncExpressionEngine.cs

Lines changed: 0 additions & 8 deletions
This file was deleted.

0 commit comments

Comments
 (0)