feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,56 @@
using System.Windows.Media;
using PCL.Core.App.IoC;
namespace PCL.Core.UI.Theme;
public class CatColorResource
{
private const string ColorPrefix = "ColorObject";
private const string BrushPrefix = "ColorBrush";
/// <summary>
/// 透明度值。
/// </summary>
public float Alpha { get; init; } = 255;
/// <summary>
/// 红色值。
/// </summary>
public required float Red { get; init; }
/// <summary>
/// 绿色值。
/// </summary>
public required float Green { get; init; }
/// <summary>
/// 蓝色值。
/// </summary>
public required float Blue { get; init; }
/// <summary>
/// 资源后缀。
/// </summary>
public required string Suffix { get; init; }
/// <summary>
/// 是否应用到 <see cref="SolidColorBrush"/> 资源。
/// </summary>
public bool ApplyToBrush { get; init; } = false;
/// <summary>
/// 是否应用到 <see cref="Color"/> 资源。
/// </summary>
public bool ApplyToColor { get; init; } = false;
/// <summary>
/// 应用该颜色到 WPF 资源字典中,仅可在 <see cref="LifecycleState.Loaded"/> 及后面的阶段调用。
/// </summary>
public void Apply()
{
var res = Lifecycle.CurrentApplication.Resources;
var color = Color.FromScRgb(Alpha, Red, Green, Blue);
if (ApplyToColor) res[$"{ColorPrefix}{Suffix}"] = color;
if (ApplyToBrush) res[$"{BrushPrefix}{Suffix}"] = new SolidColorBrush(color);
}
}
@@ -0,0 +1,209 @@
using System;
using Wacton.Unicolour;
namespace PCL.Core.UI.Theme;
/// <summary>
/// Color definition, cast and other utilities based on OKLAB color space.
/// </summary>
public class LabColor
{
/// <summary>
/// <see cref="Unicolour"/> instance for extended use.<br/>
/// Please avoid using it. Use <see cref="LabColor"/> implementations instead.
/// </summary>
internal Unicolour UnicolourInstance { get; }
private Oklab _Oklab { get => field ??= UnicolourInstance.Oklab; } = null!;
/// <summary>
/// Light channel.
/// </summary>
public double L => _Oklab.L;
/// <summary>
/// A channel.
/// </summary>
public double A => _Oklab.A;
/// <summary>
/// B channel.
/// </summary>
public double B => _Oklab.B;
/// <summary>
/// Alpha channel.
/// </summary>
public double Alpha { get; }
public override int GetHashCode() => HashCode.Combine(L, A, B, Alpha);
// ReSharper disable CompareOfFloatsByEqualityOperator
public override bool Equals(object? obj) => (obj is LabColor c) && c.L == L && c.A == A && c.B == B && c.Alpha == Alpha;
private LabColor(double l, double a, double b, double alpha)
{
Alpha = alpha;
UnicolourInstance = new Unicolour(ColourSpace.Oklab, l, a, b, alpha);
}
private LabColor(Unicolour instance)
{
Alpha = instance.Alpha.A;
UnicolourInstance = instance;
}
/// <summary>
/// Create a <see cref="LabColor"/> instance from <see cref="Unicolour"/> instance.<br/>
/// Please avoid using it. Use <see cref="LabColor.Create(double, double, double, double)"/> instead.
/// </summary>
internal static LabColor CreateInternal(Unicolour instance) => new(instance);
/// <summary>
/// Create a <see cref="LabColor"/> instance.
/// </summary>
public static LabColor Create(double l, double a, double b, double alpha = 1.0) => new(l, a, b, alpha);
/// <summary>
/// Create a <see cref="LabColor"/> instance from OKLCH values.
/// </summary>
public static LabColor FromLch(double l, double c = 0, double h = 0, double alpha = 1.0)
{
var unicolour = new Unicolour(ColourSpace.Oklch, l, c, h, alpha);
return CreateInternal(unicolour);
}
/// <summary>
/// Create a <see cref="LabColor"/> instance from RGB values.<br/>
/// <b>NOTE</b>: The alpha value is a number typed double, not byte.
/// </summary>
public static LabColor FromRgb(byte r, byte g, byte b, double alpha = 1.0)
{
var unicolour = new Unicolour(ColourSpace.Rgb255, r, g, b, alpha);
return CreateInternal(unicolour);
}
/// <summary>
/// Create a <see cref="LabColor"/> instance from ScRGB values.
/// </summary>
public static LabColor FromRgb(double r, double g, double b, double alpha = 1.0)
{
var unicolour = new Unicolour(ColourSpace.RgbLinear, r, g, b, alpha);
return CreateInternal(unicolour);
}
/// <summary>
/// Create a <see cref="LabColor"/> instance from <see cref="System.Windows.Media.Color"/>.
/// </summary>
public static LabColor FromWpfColor(System.Windows.Media.Color color)
=> FromRgb(color.ScR, color.ScG, color.ScB, color.ScA);
public static implicit operator LabColor(System.Windows.Media.Color color) => FromWpfColor(color);
/// <summary>
/// ScRGB 映射模式.
/// </summary>
public enum ScRgbMappingMode
{
/// <summary>
/// 禁用,可能导致色彩不准确
/// </summary>
Disable = -1,
/// <summary>
/// 截断到 sRGB 范围内,可能导致某些较为鲜艳的颜色变灰
/// </summary>
Clip = GamutMap.RgbClipping,
/// <summary>
/// 在 OKLCH 色彩定义下降低色度 (Chroma) 到 sRGB 范围内
/// </summary>
ReduceChroma = GamutMap.OklchChromaReduction,
/// <summary>
/// 在 WXY 色彩定义下降低纯度 (Purity) 到 sRGB 范围内
/// </summary>
ReducePurity = GamutMap.WxyPurityReduction,
}
/// <summary>
/// Set conversion mapping of <see cref="ToScRgb"/> method and <see cref="ScRgb"/> property.
/// </summary>
public static ScRgbMappingMode ScRgbMapping { get; set; } = ScRgbMappingMode.ReduceChroma;
/// <summary>
/// Convert to ScRGB values.<br/>
/// <b>NOTE</b>: Use <see cref="ScRgb"/> property for better performance.
/// </summary>
/// <seealso cref="ScRgbMapping"/>
public (float Alpha, float Red, float Green, float Blue) ToScRgb()
{
var a = UnicolourInstance.Alpha.A;
var instance = (ScRgbMapping == ScRgbMappingMode.Disable)
? UnicolourInstance
: UnicolourInstance.MapToRgbGamut((GamutMap)ScRgbMapping);
var (r, g, b) = instance.RgbLinear;
return ((float)a, (float)r, (float)g, (float)b);
}
private bool _isScRgbInitialized = false;
/// <summary>
/// Cached ScRGB values.
/// </summary>
public (float Alpha, float Red, float Green, float Blue) ScRgb
{
get
{
if (_isScRgbInitialized) return field;
_isScRgbInitialized = true;
return field = ToScRgb();
}
} = default;
/// <summary>
/// Convert to <see cref="System.Windows.Media.Color"/>.
/// </summary>
public System.Windows.Media.Color ToWpfColor()
{
var (a, r, g, b) = ScRgb;
return System.Windows.Media.Color.FromScRgb(a, r, g, b);
}
public static implicit operator System.Windows.Media.Color(LabColor lab) => lab.ToWpfColor();
/// <summary>
/// Convert to <see cref="CatColorResource"/>.
/// </summary>
/// <param name="suffix">See <see cref="CatColorResource.Suffix"/></param>
/// <param name="applyToColor">See <see cref="CatColorResource.ApplyToColor"/></param>
/// <param name="applyToBrush">See <see cref="CatColorResource.ApplyToBrush"/></param>
public CatColorResource ToCatColor(string suffix, bool applyToColor = true, bool applyToBrush = true)
{
var (a, r, g, b) = ScRgb;
return new CatColorResource
{
Red = r,
Green = g,
Blue = b,
Alpha = a,
ApplyToBrush = applyToBrush,
ApplyToColor = applyToColor,
Suffix = suffix
};
}
internal static Unicolour MixInternal(LabColor c1, LabColor c2, double c1Amount = 0.5)
=> c1.UnicolourInstance.Mix(c2.UnicolourInstance, ColourSpace.Oklab, c1Amount);
/// <summary>
/// Mix two colors.
/// </summary>
/// <param name="c1">Color 1</param>
/// <param name="c2">Color 2</param>
/// <param name="c1Amount">The amount of color 1 in mixture (0.0~1.0), as color 2 takes the rest</param>
/// <returns></returns>
public static LabColor Mix(LabColor c1, LabColor c2, double c1Amount = 0.5) => CreateInternal(MixInternal(c1, c2, c1Amount));
/// <summary>
/// Mix two colors, see <see cref="Mix"/>.
/// </summary>
public static LabColor operator +(LabColor c1, LabColor c2) => Mix(c1, c2);
}
@@ -0,0 +1,35 @@
using System;
using System.IO;
using System.Security;
using Microsoft.Win32;
using PCL.Core.Logging;
namespace PCL.Core.UI.Theme;
public static class SystemThemeHelper {
private const string ThemeRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
private const string AppsUseLightThemeKey = "AppsUseLightTheme";
/// <summary>
/// 检查系统是否处于深色模式。
/// </summary>
/// <returns>如果系统使用深色模式,则返回 true;否则返回 false(包括注册表不可访问的情况)。</returns>
public static bool IsSystemInDarkMode() {
try {
using var registryKey = Registry.CurrentUser.OpenSubKey(ThemeRegistryPath);
if (registryKey is null) {
LogWrapper.Warn($"注册表键 {ThemeRegistryPath} 不存在");
return false;
}
var value = registryKey.GetValue(AppsUseLightThemeKey) as int?;
return value == 0; // 0 表示深色模式(AppsUseLightTheme = false
} catch (Exception ex) when (ex is SecurityException or IOException) {
LogWrapper.Warn(ex, $"无法访问注册表键 {ThemeRegistryPath}");
return false;
}
}
[Obsolete("Use ThemeService.IsDarkMode instead")]
public static bool IsDarkMode() => ThemeService.IsDarkMode;
}
@@ -0,0 +1,246 @@
using System;
using PCL.Core.App;
using PCL.Core.App.Configuration;
using PCL.Core.App.IoC;
namespace PCL.Core.UI.Theme;
/// <summary>
/// 配色模式更改事件。
/// </summary>
/// <param name="isDarkMode">当前是否为深色模式</param>
/// <param name="theme">当前配色主题</param>
public delegate void ColorModeChangedEvent(bool isDarkMode, ColorTheme theme);
/// <summary>
/// 配色主题更改事件。
/// </summary>
/// <param name="theme">当前配色主题</param>
public delegate void ColorThemeChangedEvent(ColorTheme theme);
[LifecycleScope("theme", "主题", false)]
[LifecycleService(LifecycleState.WindowCreating)]
public sealed partial class ThemeService
{
[AnyConfigItem<ToneProfileConfig>("UiToneProfiles", ConfigSource.Local)]
public static partial ToneProfileConfig ToneProfiles { get; set; }
[RegisterConfigEvent]
public static ConfigEventRegistry OnColorModeConfigChanged => new(
scope: Config.Preference.Theme.ColorModeConfig,
trigger: ConfigEvent.Update,
handler: _ => RefreshColorMode()
);
[RegisterConfigEvent]
public static ConfigEventRegistry OnColorThemeConfigChanged => new(
scope: [Config.Preference.Theme.DarkColorConfig, Config.Preference.Theme.LightColorConfig],
trigger: ConfigEvent.Update,
handler: e =>
{
// ignore no change or non-current color theme change
if (e.OldValue == e.Value) return;
if (IsDarkMode) { if (e.Item == Config.Preference.Theme.LightColorConfig) return; }
else { if (e.Item == Config.Preference.Theme.DarkColorConfig) return; }
// trigger color refresh
if (Lifecycle.CurrentState > LifecycleState.Loading)
{
Lifecycle.CurrentApplication.Dispatcher.BeginInvoke(() =>
{
ApplyColorResources();
ColorThemeChanged?.Invoke(CurrentTheme);
_AprilFoolLogic();
});
}
}
);
[LifecycleStart]
private static void _Start()
{
IsDarkMode = _IsDarkMode();
_LogStatus();
_RefreshAll();
}
private static void _LogStatus()
{
Context.Debug($"当前状态: {(IsDarkMode ? "Dark" : "Light")}, {CurrentTheme}");
}
private static bool _IsDarkMode() => Config.Preference.Theme.ColorMode switch
{
ColorMode.Light => false,
ColorMode.Dark => true,
ColorMode.System => SystemThemeHelper.IsSystemInDarkMode(),
_ => false
};
/// <summary>
/// 当前是否为深色主题。
/// </summary>
public static bool IsDarkMode { get; private set; }
/// <summary>
/// 配色模式更改事件。
/// </summary>
public static event ColorModeChangedEvent? ColorModeChanged;
/// <summary>
/// 配色主题更改事件。
/// </summary>
public static event ColorThemeChangedEvent? ColorThemeChanged;
private static void _RefreshAll()
{
ApplyGrayResources();
ApplyColorResources();
ColorModeChanged?.Invoke(IsDarkMode, CurrentTheme);
_AprilFoolLogic();
}
private static void _AprilFoolLogic()
{
// for HMCL theme on April Fools' Day
if (Basics.IsAprilFool) Config.Preference.WindowTitleTypeConfig.TriggerEvent(ConfigEvent.Changed, null);
else if (CurrentTheme == ColorTheme.HmclBlue) CurrentTheme = ColorTheme.CatBlue;
}
/// <summary>
/// 刷新配色模式,若检测到当前配色模式有实际更改,则会触发主题刷新。
/// </summary>
public static void RefreshColorMode()
{
var isDarkMode = _IsDarkMode();
if (IsDarkMode == isDarkMode) return;
Context.Info("正在更改配色模式");
IsDarkMode = isDarkMode;
_LogStatus();
if (Lifecycle.CurrentState > LifecycleState.Loading)
{
Lifecycle.CurrentApplication.Dispatcher.BeginInvoke(_RefreshAll);
}
}
/// <summary>
/// 当前使用的色彩属性。
/// </summary>
public static ToneProfile CurrentTone => IsDarkMode ? ToneProfiles.Dark : ToneProfiles.Light;
/// <summary>
/// 当前使用的主题色。
/// </summary>
public static ColorTheme CurrentTheme
{
get
{
var theme = Config.Preference.Theme;
return IsDarkMode ? theme.DarkColor : theme.LightColor;
}
set
{
var theme = Config.Preference.Theme;
var config = IsDarkMode ? theme.DarkColorConfig : theme.LightColorConfig;
config.SetValue(value);
}
}
/// <summary>
/// 获取当前色彩主题对应的各种参数。
/// </summary>
public static (int Hue, double LightAdjust, double ChromaAdjust) GetCurrentThemeArgs()
{
var theme = CurrentTheme;
return theme switch
{
ColorTheme.SkyBlue => (235, 0.36, 0.2),
ColorTheme.CatBlue => (255, 0, -0.2),
ColorTheme.DeathBlue => (268, -0.05, -0.1),
ColorTheme.HmclBlue => (275, -0.03, -0.35),
#if DEBUG
_ => ((int)theme, 0, 0)
#else
_ => throw new IndexOutOfRangeException($"Invalid theme index: {(int)theme}")
#endif
};
}
private static double _AdjustLinear(double value, double adjustment)
{
if (adjustment == 0) return value;
// 确保输入在合理范围内
value = Math.Clamp(value, 0.0, 1.0);
adjustment = Math.Clamp(adjustment, -1.0, 1.0);
// 非对称线性插值
return adjustment switch
{
> 0 => value + (1.0 - value) * adjustment,
_ => value + value * adjustment
};
}
private static CatColorResource[] _CalculateGrays(ToneProfile tone) => [
LabColor.FromLch(tone.L1).ToCatColor("Gray1"),
LabColor.FromLch(tone.L2).ToCatColor("Gray2"),
LabColor.FromLch(tone.L3).ToCatColor("Gray3"),
LabColor.FromLch(tone.L4).ToCatColor("Gray4"),
LabColor.FromLch(tone.L5).ToCatColor("Gray5"),
LabColor.FromLch(tone.L6).ToCatColor("Gray6"),
LabColor.FromLch(tone.L7).ToCatColor("Gray7"),
LabColor.FromLch(tone.L8).ToCatColor("Gray8"),
LabColor.FromLch(tone.LWhite, alpha:tone.AHalfWhite).ToCatColor("HalfWhite", false),
LabColor.FromLch(tone.LWhite, alpha:tone.ASemiWhite).ToCatColor("SemiWhite", false),
LabColor.FromLch(tone.LWhite).ToCatColor("White", false),
LabColor.FromLch(tone.LWhite, alpha:tone.ATransparent).ToCatColor("Transparent", false),
LabColor.FromLch(tone.LBackground, alpha:tone.ABackground).ToCatColor("TransparentBackground", false),
LabColor.FromLch(tone.LBackground).ToCatColor("Background", false),
LabColor.FromLch(tone.LBackground, alpha:tone.AToolTip).ToCatColor("ToolTip", false),
LabColor.FromLch(tone.L7, 0.25, 30, tone.AHalfTransparent).ToCatColor("RedBack", false),
LabColor.FromLch(tone.LForeground).ToCatColor("Memory", false),
];
private static CatColorResource[] _CalculateColors(ToneProfile tone, (int hue, double lightAdj, double chromaAdj) args) => [
LabColor.FromLch(_AdjustLinear(tone.L1, args.lightAdj * 0.1), _AdjustLinear(tone.C1, args.chromaAdj * 0.25), args.hue).ToCatColor("1"),
LabColor.FromLch(_AdjustLinear(tone.L2, args.lightAdj), _AdjustLinear(tone.C2, args.chromaAdj), args.hue).ToCatColor("2"),
LabColor.FromLch(_AdjustLinear(tone.L3, args.lightAdj), _AdjustLinear(tone.C3, args.chromaAdj), args.hue).ToCatColor("3"),
LabColor.FromLch(_AdjustLinear(tone.L4, args.lightAdj), _AdjustLinear(tone.C4, args.chromaAdj), args.hue).ToCatColor("4"),
LabColor.FromLch(_AdjustLinear(tone.L5, args.lightAdj), _AdjustLinear(tone.C5, args.chromaAdj), args.hue).ToCatColor("5"),
LabColor.FromLch(_AdjustLinear(tone.L6, args.lightAdj), _AdjustLinear(tone.C6, args.chromaAdj), args.hue).ToCatColor("6"),
LabColor.FromLch(_AdjustLinear(tone.L7, args.lightAdj), _AdjustLinear(tone.C7, args.chromaAdj), args.hue).ToCatColor("7"),
LabColor.FromLch(_AdjustLinear(tone.L8, args.lightAdj), _AdjustLinear(tone.C8, args.chromaAdj), args.hue).ToCatColor("8"),
LabColor.FromLch(_AdjustLinear(tone.L8, args.lightAdj), _AdjustLinear(tone.C8, args.chromaAdj), args.hue, tone.ASemiTransparent).ToCatColor("SemiTransparent", false),
LabColor.FromLch(_AdjustLinear(tone.L5, args.lightAdj), _AdjustLinear(tone.C5, args.chromaAdj), args.hue).ToCatColor("Bg0"),
LabColor.FromLch(_AdjustLinear(tone.L7, args.lightAdj), _AdjustLinear(tone.C7, args.chromaAdj), args.hue, tone.ASemiWhite).ToCatColor("Bg1"),
];
private static CatColorResource[] LightGrayCache { get => field ??= _CalculateGrays(ToneProfiles.Light); set; } = null!;
private static CatColorResource[] DarkGrayCache { get => field ??= _CalculateGrays(ToneProfiles.Dark); set; } = null!;
/// <summary>
/// 清除灰度配色的计算缓存。
/// </summary>
public static void InvalidateGrayCache()
{
LightGrayCache = null!;
DarkGrayCache = null!;
}
/// <summary>
/// 应用灰度配色到 WPF 资源字典。
/// </summary>
public static void ApplyGrayResources()
{
var cache = IsDarkMode ? DarkGrayCache : LightGrayCache;
foreach (var c in cache) c.Apply();
}
/// <summary>
/// 应用彩色配色到 WPF 资源字典。
/// </summary>
public static void ApplyColorResources()
{
var colors = _CalculateColors(CurrentTone, GetCurrentThemeArgs());
foreach (var c in colors) c.Apply();
}
}
@@ -0,0 +1,30 @@
namespace PCL.Core.UI.Theme;
public record ToneProfile(
double L1 = 0.35,
double L2 = 0.5,
double L3 = 0.575,
double L4 = 0.65,
double L5 = 0.8,
double L6 = 0.92,
double L7 = 0.94,
double L8 = 0.96,
double LWhite = 1,
double LForeground = 0,
double LBackground = 0.995,
double C1 = 0.025,
double C2 = 0.188,
double C3 = 0.213,
double C4 = 0.168,
double C5 = 0.093,
double C6 = 0.036,
double C7 = 0.028,
double C8 = 0.018,
double ASemiWhite = 0.733,
double AHalfWhite = 0.333,
double ASemiTransparent = 0.004,
double AHalfTransparent = 0.5,
double ATransparent = 0,
double ABackground = 0.824,
double AToolTip = 0.9
);
@@ -0,0 +1,16 @@
namespace PCL.Core.UI.Theme;
public record ToneProfileConfig
{
public static readonly ToneProfile DefaultLight = new();
public static readonly ToneProfile DefaultDark = new(
L1: 0.96, L2: 0.75, L3: 0.6, L4: 0.65,
L5: 0.45, L6: 0.25, L7: 0.225, L8: 0.2,
LBackground: 0.3, LForeground: 1, LWhite: 0.275
);
public ToneProfile Light { get => field ?? DefaultLight; init; } = null!;
public ToneProfile Dark { get => field ?? DefaultDark; init; } = null!;
}