diff --git a/Moder.Core/App.axaml.cs b/Moder.Core/App.axaml.cs index b79dff2..931fc8a 100644 --- a/Moder.Core/App.axaml.cs +++ b/Moder.Core/App.axaml.cs @@ -119,7 +119,7 @@ private static HostApplicationBuilder CreateHostBuilder() var settings = new HostApplicationBuilderSettings { Args = Environment.GetCommandLineArgs(), - ApplicationName = "Moder" + ApplicationName = "Moder", }; #if DEBUG @@ -141,6 +141,7 @@ private static HostApplicationBuilder CreateHostBuilder() builder.Services.AddViewSingleton(); builder.Services.AddViewSingleton(); builder.Services.AddViewTransient(); + builder.Services.AddViewTransient(); builder.Services.AddViewTransient(); builder.Services.AddViewSingleton(); builder.Services.AddTransient(); diff --git a/Moder.Core/Graph/Controls/FocusTreeImage.cs b/Moder.Core/Graph/Controls/FocusTreeImage.cs new file mode 100644 index 0000000..2831f4a --- /dev/null +++ b/Moder.Core/Graph/Controls/FocusTreeImage.cs @@ -0,0 +1,138 @@ +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Skia; +using Avalonia.Threading; +using Moder.Core.Graph.Data; +using Moder.Core.Graph.Drawer; +using Moder.Core.Graph.DrawHelper; +using SkiaSharp; + +namespace Moder.Core.Graph.Controls; + +public class FocusTreeImage : Control +{ + public FocusTree? FocusTree { get; set; } + private bool DoDragGraph { get; set; } + private Point DragStartPoint { get; set; } + private SKColor SkForeColor { get; set; } = SKColors.White; + private SKColor SkBackColor { get; set; } = SKColors.SkyBlue; + + public bool ShowEmptyGrid + { + get => GetValue(ShowEmptyGridProperty); + set => SetValue(ShowEmptyGridProperty, value); + } + public static readonly StyledProperty ShowEmptyGridProperty = AvaloniaProperty.Register< + FocusTreeImage, + bool + >(nameof(ShowEmptyGrid), false, false, BindingMode.TwoWay); + + public Color ForeColor + { + get => GetValue(ForeColorProperty); + set => SetValue(ForeColorProperty, value); + } + public static readonly StyledProperty ForeColorProperty = AvaloniaProperty.Register< + FocusTreeImage, + Color + >(nameof(ForeColor), Colors.White); + + public Color BackColor + { + get => GetValue(BackColorProperty); + set => SetValue(BackColorProperty, value); + } + public static readonly StyledProperty BackColorProperty = AvaloniaProperty.Register< + FocusTreeImage, + Color + >(nameof(BackColor), Colors.SkyBlue); + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + DoDragGraph = true; + var position = e.GetPosition(this); + DragStartPoint = position; + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + DoDragGraph = false; + } + + protected override void OnPointerExited(PointerEventArgs e) + { + base.OnPointerExited(e); + if (FocusTree != null) + { + GridTransform.SetSelectPoint(FocusTree, null); + } + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + if (FocusTree == null) + { + return; + } + var position = e.GetPosition(this); + GridTransform.SetSelectPoint(FocusTree, position); + if (DoDragGraph) + { + var offset = position - DragStartPoint; + DragStartPoint = position; + GridTransform.OffsetOrigin(FocusTree, offset.ToSKPoint()); + } + else + { + // GridData.SelectPoint = position.ToSKPoint(); + // TODO: for test + var cell = new Cell(); + cell.SetRealPoint(FocusTree, position.ToSKPoint()); + Debug.WriteLine(cell); + // InvalidateVisual(); + // GridDrawer.DrawSelect(Bounds.Size, BackColor, e.Location); + } + } + + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + base.OnPointerWheelChanged(e); + if (FocusTree == null) + { + return; + } + var position = e.GetPosition(this); + var rect = new Rect(Bounds.Size); + var diffInWidth = position.X - rect.Width / 2; + var diffInHeight = position.Y - rect.Height / 2; + var dX = diffInWidth / CellData.EdgeLength * rect.Width / 200; + var dY = diffInHeight / CellData.EdgeLength * rect.Height / 200; + CellData.EdgeLength += (int)Math.Round(e.Delta.Y / 10 * Math.Max(rect.Width, rect.Height) / 200); + GridTransform.OffsetOrigin(FocusTree, new SKPoint((float)dX, (float)dY)); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + if (FocusTree is null) + { + return; + } + using var drawer = new FocusTreeDrawer( + new Rect(Bounds.Size), + ForeColor.ToSKColor(), + BackColor.ToSKColor(), + FocusTree + ); + context.Custom(drawer); + Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + } +} diff --git a/Moder.Core/Graph/Data/FocusNode.cs b/Moder.Core/Graph/Data/FocusNode.cs new file mode 100644 index 0000000..1022a5f --- /dev/null +++ b/Moder.Core/Graph/Data/FocusNode.cs @@ -0,0 +1,27 @@ +using Avalonia; +using Avalonia.Media; +using Moder.Core.Graph.Tools; + +namespace Moder.Core.Graph.Data; + +public class FocusNode : IRosterItem +{ + public PixelPoint Site { get; set; } + + public PixelPoint Signature => Site; + public NodeType Type { get; set; } + + public static NodeColorSelector Colors { get; } = new(); + + public Color Color => Colors[Type]; + public static FocusNode Default { get; } = new(); + + public FocusNode(PixelPoint site, NodeType type) + { + Site = site; + Type = type; + } + + public FocusNode() + : this(new PixelPoint(), NodeType.None) { } +} diff --git a/Moder.Core/Graph/Data/FocusTree.cs b/Moder.Core/Graph/Data/FocusTree.cs new file mode 100644 index 0000000..92469b6 --- /dev/null +++ b/Moder.Core/Graph/Data/FocusTree.cs @@ -0,0 +1,47 @@ +using Avalonia; +using Moder.Core.Graph.Tools; + +namespace Moder.Core.Graph.Data; + +public sealed class FocusTree +{ + public Roster FocusNodes { get; set; } = []; + + public PixelSize Size { get; set; } + + public int Width => Size.Width; + + public int Height => Size.Height; + + public FocusNode this[PixelPoint site] + { + get + { + if (FocusNodes.TryGetValue(site, out var sourceLand)) + { + return sourceLand; + } + return new FocusNode(site, NodeType.None); + } + } + + public PixelPoint SetPointWithin(PixelPoint point) + { + return SetPointWithin(point, Size); + } + + private static PixelPoint SetPointWithin(PixelPoint point, PixelSize range) + { + var x = point.X % range.Width; + if (x < 0) + { + x += range.Width; + } + var y = point.Y % range.Height; + if (y < 0) + { + y += range.Height; + } + return new(x, y); + } +} diff --git a/Moder.Core/Graph/Data/NodeColorSelector.cs b/Moder.Core/Graph/Data/NodeColorSelector.cs new file mode 100644 index 0000000..ac81957 --- /dev/null +++ b/Moder.Core/Graph/Data/NodeColorSelector.cs @@ -0,0 +1,19 @@ +using Avalonia.Media; + +namespace Moder.Core.Graph.Data; + +public class NodeColorSelector +{ + public Color this[NodeType type] => ColorCollection[type]; + + public void SetColor(NodeType type, string colorName) + { + if (Color.TryParse(colorName, out var color)) + { + ColorCollection[type] = color; + } + } + + private Dictionary ColorCollection { get; set; } = + new() { [NodeType.None] = Colors.White, [NodeType.Normal] = Colors.DarkSlateGray }; +} diff --git a/Moder.Core/Graph/Data/NodeType.cs b/Moder.Core/Graph/Data/NodeType.cs new file mode 100644 index 0000000..9ed5115 --- /dev/null +++ b/Moder.Core/Graph/Data/NodeType.cs @@ -0,0 +1,7 @@ +namespace Moder.Core.Graph.Data; + +public enum NodeType : byte +{ + None, + Normal, +} \ No newline at end of file diff --git a/Moder.Core/Graph/DrawHelper/Cell.cs b/Moder.Core/Graph/DrawHelper/Cell.cs new file mode 100644 index 0000000..4180dbb --- /dev/null +++ b/Moder.Core/Graph/DrawHelper/Cell.cs @@ -0,0 +1,232 @@ +using Avalonia; +using Moder.Core.Graph.Data; +using SkiaSharp; + +namespace Moder.Core.Graph.DrawHelper; + +public sealed class Cell +{ + public PixelPoint Site { get; private set; } + public FocusNode FocusNode { get; private set; } = FocusNode.Default; + int EdgeLength { get; set; } + float CenterPadding { get; set; } + float CenterLength { get; set; } + float CenterAddOnePadding { get; set; } + SKPoint GridOrigin { get; set; } + SKRect DrawRect { get; set; } + SKRect PanoramaRect { get; set; } + + public Cell() + : this( + CellData.EdgeLength, + CellData.PaddingFactor, + GridData.Origin, + GridData.DrawRect, + GridData.PanoramaRect + ) { } + + public Cell(GridDrawData drawData) + : this( + drawData.CellEdgeLength, + drawData.CellPaddingFactor, + drawData.Origin, + drawData.DrawRect, + drawData.PanoramaRect + ) { } + + public Cell(int egdeLength, float paddingFactor, SKPoint gridOrigin, SKRect drawRect, SKRect panoramaRect) + { + SetEgdeLength(egdeLength, paddingFactor); + GridOrigin = gridOrigin; + DrawRect = drawRect; + PanoramaRect = panoramaRect; + } + + public void SetEgdeLength(int egdeLength, float paddingFactor) + { + EdgeLength = egdeLength; + CenterPadding = EdgeLength * paddingFactor; + CenterLength = EdgeLength - CenterPadding * 2; + CenterAddOnePadding = CenterLength + CenterPadding; + } + + public void SetGridSite(FocusTree focusTree, PixelPoint site) + { + Site = focusTree.SetPointWithin(site); + FocusNode = focusTree[Site]; + } + + public void SetRealPoint(FocusTree focusTree, SKPoint realPoint) + { + Site = RealPointToSite(focusTree, realPoint, EdgeLength, GridOrigin); + FocusNode = focusTree[Site]; + } + + private static PixelPoint RealPointToSite( + FocusTree focusTree, + SKPoint realPoint, + int edgeLength, + SKPoint origin + ) + { + var dX = realPoint.X - origin.X; + var x = dX / edgeLength; + if (dX < 0) + { + x += focusTree.Width; + } + var dY = realPoint.Y - origin.Y; + var y = dY / edgeLength; + if (dY < 0) + { + y += focusTree.Height; + } + return new((int)x, (int)y); + } + + private (float, float) GridPointToRealLeftTop() + { + var x = EdgeLength * Site.X + GridOrigin.X; + if (x < PanoramaRect.Left) + { + x += PanoramaRect.Width; + } + else if (x > PanoramaRect.Right) + { + x -= PanoramaRect.Width; + } + var y = EdgeLength * Site.Y + GridOrigin.Y; + if (y < PanoramaRect.Top) + { + y += PanoramaRect.Height; + } + else if (y > PanoramaRect.Bottom) + { + y -= PanoramaRect.Height; + } + return new(x, y); + } + + public SKRect GetBounds() + { + var (left, top) = GridPointToRealLeftTop(); + var bounds = SKRect.Create(left, top, EdgeLength, EdgeLength); + return SKRect.Intersect(bounds, DrawRect); + } + + public SKRect GetPartBounds(Direction part) + { + var (left, top) = GridPointToRealLeftTop(); + var center = SKRect.Create(left + CenterPadding, top + CenterPadding, CenterLength, CenterLength); + var bounds = part switch + { + Direction.Center => center, + Direction.Left => SKRect.Create(left, center.Top, CenterPadding, CenterLength), + Direction.Top => SKRect.Create(center.Left, top, CenterLength, CenterPadding), + Direction.Right => SKRect.Create(center.Right, center.Top, CenterPadding, center.Height), + Direction.Bottom => SKRect.Create(center.Left, center.Bottom, center.Width, CenterPadding), + Direction.LeftTop => SKRect.Create(left, top, CenterPadding, CenterPadding), + Direction.TopRight => SKRect.Create(center.Right, top, CenterPadding, CenterPadding), + Direction.LeftBottom => SKRect.Create(left, center.Bottom, CenterPadding, CenterPadding), + Direction.BottomRight => SKRect.Create(center.Right, center.Bottom, CenterPadding, CenterPadding), + _ => default, + }; + return SKRect.Intersect(bounds, DrawRect); + } + + public SKRect GetBoundsInDirection(Direction direction) + { + var (left, top) = GridPointToRealLeftTop(); + var bounds = SKRect.Create(left, top, EdgeLength, EdgeLength); + var center = SKRect.Create(left + CenterPadding, top + CenterPadding, CenterLength, CenterLength); + bounds = direction switch + { + Direction.Center => bounds, + Direction.Left => SKRect.Create(center.Left, bounds.Top, CenterAddOnePadding, EdgeLength), + Direction.Top => SKRect.Create(bounds.Left, center.Top, EdgeLength, CenterAddOnePadding), + Direction.Right => SKRect.Create(bounds.Left, bounds.Top, CenterAddOnePadding, EdgeLength), + Direction.Bottom => SKRect.Create(bounds.Left, bounds.Top, EdgeLength, CenterAddOnePadding), + Direction.LeftTop => SKRect.Create( + center.Left, + center.Top, + CenterAddOnePadding, + CenterAddOnePadding + ), + Direction.TopRight => SKRect.Create( + bounds.Left, + center.Top, + CenterAddOnePadding, + CenterAddOnePadding + ), + Direction.LeftBottom => SKRect.Create( + center.Left, + bounds.Top, + CenterAddOnePadding, + CenterAddOnePadding + ), + Direction.BottomRight => SKRect.Create( + bounds.Left, + bounds.Top, + CenterAddOnePadding, + CenterAddOnePadding + ), + _ => default, + }; + return SKRect.Intersect(bounds, DrawRect); + } + + public Direction GetRealPointOnPart(SKPoint realPoint) + { + if (GetPartBounds(Direction.Center).Contains(realPoint)) + { + return Direction.Center; + } + if (GetPartBounds(Direction.Left).Contains(realPoint)) + { + return Direction.Left; + } + if (GetPartBounds(Direction.Top).Contains(realPoint)) + { + return Direction.Top; + } + if (GetPartBounds(Direction.Right).Contains(realPoint)) + { + return Direction.Right; + } + if (GetPartBounds(Direction.Bottom).Contains(realPoint)) + { + return Direction.Bottom; + } + if (GetPartBounds(Direction.LeftTop).Contains(realPoint)) + { + return Direction.LeftTop; + } + if (GetPartBounds(Direction.TopRight).Contains(realPoint)) + { + return Direction.TopRight; + } + if (GetPartBounds(Direction.BottomRight).Contains(realPoint)) + { + return Direction.BottomRight; + } + if (GetPartBounds(Direction.LeftBottom).Contains(realPoint)) + { + return Direction.LeftBottom; + } + return Direction.None; + } + + public static SKColor GetPartShadeColor(Direction part) + { + return part switch + { + Direction.Center => CellData.CellCenterShadeColor, + _ => CellData.CellAroundShadeColor, + }; + } + + public override string ToString() + { + return $"site:{Site} land-type:{FocusNode.Type}"; + } +} diff --git a/Moder.Core/Graph/DrawHelper/CellData.cs b/Moder.Core/Graph/DrawHelper/CellData.cs new file mode 100644 index 0000000..ad117a6 --- /dev/null +++ b/Moder.Core/Graph/DrawHelper/CellData.cs @@ -0,0 +1,28 @@ +using SkiaSharp; + +namespace Moder.Core.Graph.DrawHelper; + +public sealed class CellData +{ + private static int _edgeLength = 30; + public static int EdgeLengthMin { get; set; } = 25; + public static int EdgeLengthMax { get; set; } = 125; + public static float CenterPaddingFactorMin { get; set; } = 0.01f; + public static float CenterPaddingFactorMax { get; set; } = 0.4f; + public static float PaddingFactor + { + get => _paddingFactor; + set => + _paddingFactor = + value < CenterPaddingFactorMin || value > CenterPaddingFactorMax ? _paddingFactor : value; + } + static float _paddingFactor = 0.2f; + public static int EdgeLength + { + get => _edgeLength; + set { _edgeLength = value < EdgeLengthMin || value > EdgeLengthMax ? _edgeLength : value; } + } + public static SKColor CellCenterShadeColor { get; set; } = + new(SKColors.Orange.Red, SKColors.Orange.Green, SKColors.Orange.Blue, 125); + public static SKColor CellAroundShadeColor { get; set; } = SKColors.DimGray; +} diff --git a/Moder.Core/Graph/DrawHelper/Direction.cs b/Moder.Core/Graph/DrawHelper/Direction.cs new file mode 100644 index 0000000..a30a6da --- /dev/null +++ b/Moder.Core/Graph/DrawHelper/Direction.cs @@ -0,0 +1,16 @@ +namespace Moder.Core.Graph.DrawHelper; + +[Flags] +public enum Direction : byte +{ + None = 0x0, + Left = 0x1, + Top = 0x2, + Right = 0x4, + Bottom = 0x8, + LeftTop = Left | Top, + TopRight = Top | Right, + LeftBottom = Left | Bottom, + BottomRight = Bottom | Right, + Center = 0x16, +} diff --git a/Moder.Core/Graph/DrawHelper/GridData.cs b/Moder.Core/Graph/DrawHelper/GridData.cs new file mode 100644 index 0000000..ab823d8 --- /dev/null +++ b/Moder.Core/Graph/DrawHelper/GridData.cs @@ -0,0 +1,24 @@ +using Avalonia; +using HarfBuzzSharp; +using SkiaSharp; + +namespace Moder.Core.Graph.DrawHelper; + +public sealed class GridData +{ + public static SKPoint Origin { get; set; } + public static SKRect PanoramaRect { get; set; } + public static SKRect DrawRect { get; set; } + public static float GuideLineThickness { get; set; } = 2f; + public static SKColor GuideLineColor { get; set; } = SKColors.SlateGray; + public static SKColor FocusColor { get; set; } = + new(SKColors.Red.Red, SKColors.Red.Green, SKColors.Red.Blue, 200); + public static bool ShowEmptyGrid { get; set; } +#if DEBUG + = true; +#endif + public static PixelPoint? SelectSite { get; set; } + public static Direction SelectCellPart { get; set; } = Direction.None; + public static PixelPoint? FocusSite { get; set; } + public static bool RedrawAll { get; set; } +} \ No newline at end of file diff --git a/Moder.Core/Graph/DrawHelper/GridDraw.cs b/Moder.Core/Graph/DrawHelper/GridDraw.cs new file mode 100644 index 0000000..d308df6 --- /dev/null +++ b/Moder.Core/Graph/DrawHelper/GridDraw.cs @@ -0,0 +1,123 @@ +using System.Diagnostics; +using Avalonia; +using Avalonia.Skia; +using FSharp.Data.Runtime; +using HarfBuzzSharp; +using Moder.Core.Graph.Data; +using SkiaSharp; + +namespace Moder.Core.Graph.DrawHelper; + +public sealed class GridDraw +{ + private static GridDrawData? DrawData { get; set; } + + public static void Draw( + FocusTree focusTree, + SKCanvas canvas, + Rect bounds, + SKColor foreColor, + SKColor backColor + ) + { + var width = (int)bounds.Width; + var height = (int)bounds.Height; + if (width is 0 || height is 0) + { + return; + } + GridData.DrawRect = SKRect.Create(0, 0, width, height); + using var paint = new SKPaint(); + DrawData = new GridDrawData(foreColor, backColor); + Draw(focusTree, canvas, paint, DrawData); + } + + private static void Draw(FocusTree focusTree, SKCanvas canvas, SKPaint paint, GridDrawData drawData) + { + DrawGrid(focusTree, canvas, paint, drawData); + DrawCrossLine(canvas, paint, drawData); + } + + private static void DrawGrid(FocusTree focusTree, SKCanvas canvas, SKPaint paint, GridDrawData drawData) + { + canvas.Clear(drawData.BackColor); + var width = (int)(drawData.DrawRect.Width / drawData.CellEdgeLength + 2); + var height = (int)(drawData.DrawRect.Height / drawData.CellEdgeLength + 2); + var offsetX = (int)(drawData.Origin.X / drawData.CellEdgeLength + 1); + var offsetY = (int)(drawData.Origin.Y / drawData.CellEdgeLength + 1); + var cell = new Cell(drawData); + for (var i = 0; i < width; i++) + { + for (var j = 0; j < height; j++) + { + var site = new PixelPoint(i - offsetX, j - offsetY); + cell.SetGridSite(focusTree, site); + if (GridData.FocusSite == cell.Site) + { + DrawFocusCell(canvas, paint, cell); + } + else if (GridData.SelectSite == cell.Site) + { + DrawSelecteCell(canvas, paint, cell); + } + else + { + DrawCell(canvas, paint, cell, drawData.ForeColor); + } + } + } + } + + public static void DrawSelecteCell(SKCanvas canvas, SKPaint paint, Cell cell) + { + var bounds = cell.GetPartBounds(GridData.SelectCellPart); + paint.Color = Cell.GetPartShadeColor(GridData.SelectCellPart); + canvas.DrawRect(bounds, paint); + } + + public static void DrawFocusCell(SKCanvas canvas, SKPaint paint, Cell cell) + { + var bounds = cell.GetPartBounds(Direction.Center); + paint.Color = GridData.FocusColor; + canvas.DrawRect(bounds, paint); + } + + private static void DrawCell(SKCanvas canvas, SKPaint paint, Cell cell, SKColor foreColor) + { + //TODO: for test + if (cell.FocusNode.Type != NodeType.None) + { + var rect = cell.GetPartBounds(Direction.Center); + var x = rect.Left; + var y = rect.Top; + var width = rect.Width; + var height = rect.Height; + rect = SKRect.Create(x, y, width, height); + paint.Color = cell.FocusNode.Color.ToSKColor(); + canvas.DrawRect(rect, paint); + } + else if (GridData.ShowEmptyGrid) + { + paint.Color = foreColor; + paint.IsStroke = true; + canvas.DrawRect(cell.GetPartBounds(Direction.Center), paint); + paint.IsStroke = false; + } + } + + private static void DrawCrossLine(SKCanvas canvas, SKPaint paint, GridDrawData drawData) + { + paint.Color = GridData.GuideLineColor; + paint.StrokeWidth = GridData.GuideLineThickness; + canvas.DrawLine( + new(drawData.Origin.X, drawData.DrawRect.Top), + new(drawData.Origin.X, drawData.DrawRect.Bottom), + paint + ); + canvas.DrawLine( + new(drawData.DrawRect.Left, drawData.Origin.Y), + new(drawData.DrawRect.Right, drawData.Origin.Y), + paint + ); + } +} diff --git a/Moder.Core/Graph/DrawHelper/GridDrawData.cs b/Moder.Core/Graph/DrawHelper/GridDrawData.cs new file mode 100644 index 0000000..c5a7f3b --- /dev/null +++ b/Moder.Core/Graph/DrawHelper/GridDrawData.cs @@ -0,0 +1,25 @@ +using SkiaSharp; + +namespace Moder.Core.Graph.DrawHelper; + +public sealed class GridDrawData +{ + public SKColor ForeColor { get; } + public SKColor BackColor { get; } + public SKPoint Origin { get; } + public SKRect DrawRect { get; } + public SKRect PanoramaRect { get; } + public int CellEdgeLength { get; } + public float CellPaddingFactor { get; } + + public GridDrawData(SKColor foreColor, SKColor backColor) + { + ForeColor = foreColor; + BackColor = backColor; + Origin = GridData.Origin; + DrawRect = GridData.DrawRect; + PanoramaRect = GridData.PanoramaRect; + CellEdgeLength = CellData.EdgeLength; + CellPaddingFactor = CellData.PaddingFactor; + } +} diff --git a/Moder.Core/Graph/DrawHelper/GridTransform.cs b/Moder.Core/Graph/DrawHelper/GridTransform.cs new file mode 100644 index 0000000..ecc0f6b --- /dev/null +++ b/Moder.Core/Graph/DrawHelper/GridTransform.cs @@ -0,0 +1,62 @@ +using Avalonia; +using Avalonia.Skia; +using Moder.Core.Graph.Data; +using SkiaSharp; + +namespace Moder.Core.Graph.DrawHelper; + +public class GridTransform +{ + public static void ResetTransform() + { + GridData.Origin = new(); + GridData.PanoramaRect = new(); + GridData.DrawRect = new(); + } + + public static void OffsetOrigin(FocusTree focusTree, SKPoint offset) + { + GridData.PanoramaRect = SKRect.Create( + -CellData.EdgeLength, + -CellData.EdgeLength, + focusTree.Width * CellData.EdgeLength, + focusTree.Height * CellData.EdgeLength + ); + var x = (GridData.Origin.X + offset.X) % GridData.PanoramaRect.Width; + if (x < 0) + { + x += GridData.PanoramaRect.Width; + } + var y = (GridData.Origin.Y + offset.Y) % GridData.PanoramaRect.Height; + if (y < 0) + { + y += GridData.PanoramaRect.Height; + } + GridData.Origin = new(x, y); + GridData.RedrawAll = true; + } + + public static void SetSelectPoint(FocusTree focusTree, Point? point) + { + if (point is null) + { + GridData.SelectSite = new(-1, -1); + GridData.SelectCellPart = Direction.None; + } + else + { + var cell = new Cell(); + var pos = point.Value.ToSKPoint(); + cell.SetRealPoint(focusTree, pos); + GridData.SelectSite = cell.Site; + GridData.SelectCellPart = cell.GetRealPointOnPart(pos); + } + } + + public static void CheckFocusPoint(FocusTree focusTree, Point point) + { + var cell = new Cell(); + cell.SetRealPoint(focusTree, point.ToSKPoint()); + GridData.FocusSite = GridData.FocusSite == cell.Site ? null : cell.Site; + } +} diff --git a/Moder.Core/Graph/Drawer/FocusTreeDrawer.cs b/Moder.Core/Graph/Drawer/FocusTreeDrawer.cs new file mode 100644 index 0000000..878e480 --- /dev/null +++ b/Moder.Core/Graph/Drawer/FocusTreeDrawer.cs @@ -0,0 +1,54 @@ +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Microsoft.Extensions.DependencyInjection; +using Moder.Core.Graph.Data; +using Moder.Core.Graph.DrawHelper; +using NLog; +using SkiaSharp; + +namespace Moder.Core.Graph.Drawer; + +public class FocusTreeDrawer : ICustomDrawOperation +{ + public Rect Bounds { get; } + private SKColor ForeColor { get; } + private SKColor BackColor { get; } + private FocusTree FocusTree { get; } + + public bool Equals(ICustomDrawOperation? other) + { + return false; + } + + public void Dispose() { } + + public bool HitTest(Point p) + { + return true; + } + + public FocusTreeDrawer(Rect bounds, SKColor foreColor, SKColor backColor, FocusTree focusTree) + { + Bounds = bounds; + ForeColor = foreColor; + BackColor = backColor; + FocusTree = focusTree; + } + + public void Render(ImmediateDrawingContext context) + { + var leaseFeature = context.TryGetFeature(); + if (leaseFeature == null) + { + var logger = LogManager.GetCurrentClassLogger(); + logger.Error("Current rendering API is not Skia"); + return; + } + using var lease = leaseFeature.Lease(); + var canvas = lease.SkCanvas; + GridDraw.Draw(FocusTree, canvas, Bounds, ForeColor, BackColor); + } +} diff --git a/Moder.Core/Graph/Tools/IRosterItem.cs b/Moder.Core/Graph/Tools/IRosterItem.cs new file mode 100644 index 0000000..05bbcf3 --- /dev/null +++ b/Moder.Core/Graph/Tools/IRosterItem.cs @@ -0,0 +1,7 @@ +namespace Moder.Core.Graph.Tools; + +public interface IRosterItem + where TSignature : notnull +{ + public TSignature Signature { get; } +} diff --git a/Moder.Core/Graph/Tools/Roster.cs b/Moder.Core/Graph/Tools/Roster.cs new file mode 100644 index 0000000..1bef7e4 --- /dev/null +++ b/Moder.Core/Graph/Tools/Roster.cs @@ -0,0 +1,80 @@ +using System.Collections; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; + +namespace Moder.Core.Graph.Tools; + +public sealed class Roster : ICollection + where TSignature : notnull + where TItem : IRosterItem +{ + ConcurrentDictionary RosterMap { get; set; } = []; + + public TItem this[TSignature signature] + { + get => RosterMap[signature]; + set => RosterMap[signature] = value; + } + + public int Count => RosterMap.Count; + + public bool IsReadOnly => false; + + public bool TryAdd(TItem item) + { + return RosterMap.TryAdd(item.Signature, item); + } + + public bool TryGetValue(TSignature signature, [NotNullWhen(true)] out TItem? value) + { + return RosterMap.TryGetValue(signature, out value); + } + + public void AddRange(IList arrange) + { + foreach (var item in arrange) + RosterMap.TryAdd(item.Signature, item); + } + + public void Clear() + { + RosterMap.Clear(); + } + + public bool Contains(TItem item) + { + return RosterMap.ContainsKey(item.Signature); + } + + public void CopyTo(TItem[] array, int arrayIndex) + { + RosterMap.Values.ToArray().CopyTo(array, arrayIndex); + } + + public bool TryRemove(TItem item) + { + return RosterMap.TryRemove(item.Signature, out _); + } + + public IEnumerator GetEnumerator() + { + return RosterMap.Values.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return RosterMap.Values.GetEnumerator(); + } + + [Obsolete] + public void Add(TItem item) + { + throw new NotImplementedException(); + } + + [Obsolete] + public bool Remove(TItem item) + { + throw new NotImplementedException(); + } +} diff --git a/Moder.Core/Moder.Core.csproj b/Moder.Core/Moder.Core.csproj index b3cfee8..113d68c 100644 --- a/Moder.Core/Moder.Core.csproj +++ b/Moder.Core/Moder.Core.csproj @@ -21,6 +21,7 @@ + diff --git a/Moder.Core/Views/Game/FocusTreeEditorControlView.axaml b/Moder.Core/Views/Game/FocusTreeEditorControlView.axaml new file mode 100644 index 0000000..dea190f --- /dev/null +++ b/Moder.Core/Views/Game/FocusTreeEditorControlView.axaml @@ -0,0 +1,17 @@ + + + + + diff --git a/Moder.Core/Views/Game/FocusTreeEditorControlView.axaml.cs b/Moder.Core/Views/Game/FocusTreeEditorControlView.axaml.cs new file mode 100644 index 0000000..f51efe2 --- /dev/null +++ b/Moder.Core/Views/Game/FocusTreeEditorControlView.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Moder.Core.Graph.Data; +using Moder.Core.Graph.DrawHelper; +using Moder.Core.Graph.Tools; +using Moder.Core.Infrastructure; +using Moder.Language.Strings; + +namespace Moder.Core.Views.Game; + +public partial class FocusTreeEditorControlView : UserControl, ITabViewItem, IClosed +{ + public string Header { get; } = Resource.Menu_FocusTree; + public string Id { get; } = nameof(FocusTreeEditorControlView); + public string ToolTip => Header; + + public FocusTreeEditorControlView() + { + InitializeComponent(); + + // TODO: for test + var focusNode = new Roster(); + focusNode.TryAdd(new FocusNode(new PixelPoint(2, 2), NodeType.Normal)); + focusNode.TryAdd(new FocusNode(new PixelPoint(6, 5), NodeType.Normal)); + var focusTree = new FocusTree { Size = new PixelSize(50, 50), FocusNodes = focusNode }; + FocusTreeImage.FocusTree = focusTree; + + // TODO: Bug + MinWidth = 500; + MinHeight = 500; + } + + public void Close() + { + // TODO + } +} diff --git a/Moder.Core/Views/MainWindow.axaml b/Moder.Core/Views/MainWindow.axaml index 571bda4..53292f6 100644 --- a/Moder.Core/Views/MainWindow.axaml +++ b/Moder.Core/Views/MainWindow.axaml @@ -1,36 +1,37 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + Margin="8,0,0,0" + Path="./Assets/logo.svg" + Width="24" /> + - + diff --git a/Moder.Core/ViewsModel/Game/FoucsTreeEditorControlViewModel.cs b/Moder.Core/ViewsModel/Game/FoucsTreeEditorControlViewModel.cs new file mode 100644 index 0000000..30f00a8 --- /dev/null +++ b/Moder.Core/ViewsModel/Game/FoucsTreeEditorControlViewModel.cs @@ -0,0 +1,12 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Moder.Core.Infrastructure; + +namespace Moder.Core.ViewsModel.Game; + +public partial class FoucsTreeEditorControlViewModel : ObservableValidator, IClosed +{ + public void Close() + { + // TODO + } +} \ No newline at end of file diff --git a/Moder.Core/ViewsModel/MainWindowViewModel.cs b/Moder.Core/ViewsModel/MainWindowViewModel.cs index 7b36688..08cb9a6 100644 --- a/Moder.Core/ViewsModel/MainWindowViewModel.cs +++ b/Moder.Core/ViewsModel/MainWindowViewModel.cs @@ -22,4 +22,11 @@ private void OpenSettings() var tabview = App.Services.GetRequiredService(); tabview.AddSingleTabFromIoc(); } + + [RelayCommand] + private void OpenFocusTreeEditor() + { + var tabview = App.Services.GetRequiredService(); + tabview.AddSingleTabFromIoc(); + } } \ No newline at end of file diff --git a/Moder.Language/Strings/Resource.Designer.cs b/Moder.Language/Strings/Resource.Designer.cs index 89166ff..d3c3133 100644 --- a/Moder.Language/Strings/Resource.Designer.cs +++ b/Moder.Language/Strings/Resource.Designer.cs @@ -482,6 +482,15 @@ public static string Menu_CopyRelativePath { } } + /// + /// Looks up a localized string similar to 国策树. + /// + public static string Menu_FocusTree { + get { + return ResourceManager.GetString("Menu.FocusTree", resourceCulture); + } + } + /// /// Looks up a localized string similar to 初始化完成, 耗时: {0:F3} s. /// diff --git a/Moder.Language/Strings/Resource.en-US.resx b/Moder.Language/Strings/Resource.en-US.resx index 657d749..af92d8d 100644 --- a/Moder.Language/Strings/Resource.en-US.resx +++ b/Moder.Language/Strings/Resource.en-US.resx @@ -323,4 +323,7 @@ Tip + + Focus Tree + \ No newline at end of file diff --git a/Moder.Language/Strings/Resource.resx b/Moder.Language/Strings/Resource.resx index e36db2e..4c73bd5 100644 --- a/Moder.Language/Strings/Resource.resx +++ b/Moder.Language/Strings/Resource.resx @@ -323,4 +323,7 @@ 提示 + + 国策树 + \ No newline at end of file