XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet
公开
关注 0 Fork 0 Star 0
UTF-8
using Microsoft.UI;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Shapes;
using SpaceEngineersBlueprintEditor.SpaceEngineersCore.BlueprintEditing;
using System.Numerics;
using VRage.Game;
using Windows.Foundation;

namespace SpaceEngineersBlueprintEditor.Controls;

public sealed partial class CubeDeformationEditor : UserControl
{
    private static readonly int[][] CubeEdges =
    [
        [0, 1], [0, 2], [0, 4], [1, 3], [1, 5], [2, 3],
        [2, 6], [3, 7], [4, 5], [4, 6], [5, 7], [6, 7]
    ];

    private static readonly FaceDefinition[] Faces =
    [
        new(8, [0, 1, 3, 2]),
        new(9, [4, 6, 7, 5]),
        new(10, [0, 4, 5, 1]),
        new(11, [2, 3, 7, 6]),
        new(12, [0, 2, 6, 4]),
        new(13, [1, 5, 7, 3])
    ];

    public static readonly DependencyProperty CubeGridProperty = DependencyProperty.Register(
        nameof(CubeGrid),
        typeof(object),
        typeof(CubeDeformationEditor),
        new PropertyMetadata(null, SelectionPropertyChanged));

    public static readonly DependencyProperty CubeBlockProperty = DependencyProperty.Register(
        nameof(CubeBlock),
        typeof(object),
        typeof(CubeDeformationEditor),
        new PropertyMetadata(null, SelectionPropertyChanged));

    public static readonly DependencyProperty HasUnsavedChangesProperty = DependencyProperty.Register(
        nameof(HasUnsavedChanges),
        typeof(bool),
        typeof(CubeDeformationEditor),
        new PropertyMetadata(false));

    private readonly List<CubeDeformationControlPoint> controlPoints = [];
    private readonly SolidColorBrush edgeBrush = new(ColorHelper.FromArgb(220, 160, 165, 175));
    private readonly SolidColorBrush cornerBrush = new(ColorHelper.FromArgb(255, 255, 166, 61));
    private readonly SolidColorBrush faceCenterBrush = new(ColorHelper.FromArgb(255, 64, 156, 255));
    private readonly SolidColorBrush selectedBrush = new(Colors.White);
    private readonly SolidColorBrush faceBrush = new(ColorHelper.FromArgb(42, 85, 140, 220));
    private readonly SolidColorBrush xAxisBrush = new(ColorHelper.FromArgb(255, 229, 83, 83));
    private readonly SolidColorBrush yAxisBrush = new(ColorHelper.FromArgb(255, 76, 175, 80));
    private readonly SolidColorBrush zAxisBrush = new(ColorHelper.FromArgb(255, 66, 133, 244));

    private int selectedPointIndex = -1;
    private int draggedPointIndex = -1;
    private bool isRotating;
    private bool isUpdatingEditors;
    private Point dragStart;
    private CubeDeformationOffset dragStartOffset;
    private double dragStartYaw;
    private double dragStartPitch;
    private double yaw = -0.72;
    private double pitch = 0.5;
    private double zoom = 92;
    private Axis selectedAxis = Axis.X;

    public object? CubeGrid
    {
        get => GetValue(CubeGridProperty);
        set => SetValue(CubeGridProperty, value);
    }

    public object? CubeBlock
    {
        get => GetValue(CubeBlockProperty);
        set => SetValue(CubeBlockProperty, value);
    }

    public bool HasUnsavedChanges
    {
        get => (bool)GetValue(HasUnsavedChangesProperty);
        set => SetValue(HasUnsavedChangesProperty, value);
    }

    private MyObjectBuilder_CubeGrid? SelectedGrid => CubeGrid as MyObjectBuilder_CubeGrid;

    private MyObjectBuilder_CubeBlock? SelectedBlock => CubeBlock as MyObjectBuilder_CubeBlock;

    public CubeDeformationEditor()
    {
        InitializeComponent();
        Loaded += (_, _) => ReloadSelection();
    }

    private static void SelectionPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs args)
    {
        if (sender is CubeDeformationEditor editor && editor.IsLoaded)
        {
            editor.ReloadSelection();
        }
    }

    private void ReloadSelection()
    {
        controlPoints.Clear();
        selectedPointIndex = -1;
        draggedPointIndex = -1;

        if (SelectedGrid is MyObjectBuilder_CubeGrid grid && SelectedBlock is MyObjectBuilder_CubeBlock block)
        {
            controlPoints.AddRange(CubeDeformationService.GetControlPoints(grid, block));
            blockDescription.Text = $"{block.SubtypeName}  ·  Min ({block.Min.X}, {block.Min.Y}, {block.Min.Z})";
            emptyState.Visibility = Visibility.Collapsed;
            editorScrollView.IsEnabled = true;
            SelectPoint(0);
        }
        else
        {
            blockDescription.Text = string.Empty;
            selectedPointDescription.Text = string.Empty;
            emptyState.Visibility = Visibility.Visible;
            editorScrollView.IsEnabled = false;
            UpdateNumberEditors(CubeDeformationOffset.Zero);
        }

        RenderViewport();
    }

    private void SelectPoint(int index)
    {
        if (index < 0 || index >= controlPoints.Count)
        {
            return;
        }

        selectedPointIndex = index;
        var point = controlPoints[index];
        var pointKind = point.Kind == CubeDeformationControlKind.Corner
            ? "SheetMetalEditor_Corner".GetLocalized()
            : "SheetMetalEditor_FaceCenter".GetLocalized();
        selectedPointDescription.Text = $"{pointKind} ({FormatCoordinate(point.LocalX)}X, {FormatCoordinate(point.LocalY)}Y, {FormatCoordinate(point.LocalZ)}Z)";
        UpdateNumberEditors(point.Offset);
        RenderViewport();
    }

    private static string FormatCoordinate(int coordinate) => coordinate switch
    {
        0 => "−",
        2 => "+",
        _ => "0"
    };

    private void UpdateNumberEditors(CubeDeformationOffset offset)
    {
        isUpdatingEditors = true;
        offsetX.Value = Math.Round(offset.X, 3);
        offsetY.Value = Math.Round(offset.Y, 3);
        offsetZ.Value = Math.Round(offset.Z, 3);
        isUpdatingEditors = false;
    }

    private void ApplySelectedOffset(CubeDeformationOffset offset)
    {
        if (selectedPointIndex < 0 ||
            SelectedGrid is not MyObjectBuilder_CubeGrid grid ||
            SelectedBlock is not MyObjectBuilder_CubeBlock block)
        {
            return;
        }

        var oldPoint = controlPoints[selectedPointIndex];
        var clampedOffset = offset.Clamp();
        CubeDeformationService.SetOffset(
            grid,
            block,
            oldPoint.LocalX,
            oldPoint.LocalY,
            oldPoint.LocalZ,
            clampedOffset);
        controlPoints[selectedPointIndex] = new(
            oldPoint.Id,
            oldPoint.Kind,
            oldPoint.LocalX,
            oldPoint.LocalY,
            oldPoint.LocalZ,
            clampedOffset);
        HasUnsavedChanges = true;
        UpdateNumberEditors(clampedOffset);
        RenderViewport();
    }

    private void Offset_ValueChanged(NumberBox sender, NumberBoxValueChangedEventArgs args)
    {
        if (isUpdatingEditors || selectedPointIndex < 0 ||
            double.IsNaN(offsetX.Value) || double.IsNaN(offsetY.Value) || double.IsNaN(offsetZ.Value))
        {
            return;
        }

        ApplySelectedOffset(new(
            (float)offsetX.Value,
            (float)offsetY.Value,
            (float)offsetZ.Value));
    }

    private void Axis_Click(object sender, RoutedEventArgs e)
    {
        if (sender is FrameworkElement { Tag: string axis })
        {
            selectedAxis = axis switch
            {
                "Y" => Axis.Y,
                "Z" => Axis.Z,
                _ => Axis.X
            };
            RenderViewport();
        }
    }

    private void ResetPoint_Click(object sender, RoutedEventArgs e)
    {
        ApplySelectedOffset(CubeDeformationOffset.Zero);
    }

    private void ResetBlock_Click(object sender, RoutedEventArgs e)
    {
        if (SelectedGrid is not MyObjectBuilder_CubeGrid grid ||
            SelectedBlock is not MyObjectBuilder_CubeBlock block)
        {
            return;
        }

        CubeDeformationService.ResetBlock(grid, block);
        HasUnsavedChanges = true;
        ReloadSelection();
    }

    private void ResetView_Click(object sender, RoutedEventArgs e)
    {
        yaw = -0.72;
        pitch = 0.5;
        zoom = 92;
        RenderViewport();
    }

    private void Viewport_PointerPressed(object sender, PointerRoutedEventArgs e)
    {
        var currentPoint = e.GetCurrentPoint(viewport);
        dragStart = currentPoint.Position;

        if (currentPoint.Properties.IsRightButtonPressed)
        {
            isRotating = true;
            dragStartYaw = yaw;
            dragStartPitch = pitch;
            viewport.CapturePointer(e.Pointer);
            e.Handled = true;
            return;
        }

        if (currentPoint.Properties.IsLeftButtonPressed &&
            e.OriginalSource is FrameworkElement { Tag: int pointIndex })
        {
            SelectPoint(pointIndex);
            draggedPointIndex = pointIndex;
            dragStartOffset = controlPoints[pointIndex].Offset;
            viewport.CapturePointer(e.Pointer);
            e.Handled = true;
        }
    }

    private void Viewport_PointerMoved(object sender, PointerRoutedEventArgs e)
    {
        var position = e.GetCurrentPoint(viewport).Position;
        var delta = new Vector2((float)(position.X - dragStart.X), (float)(position.Y - dragStart.Y));

        if (isRotating)
        {
            yaw = dragStartYaw + delta.X * 0.01;
            pitch = Math.Max(-1.2, Math.Min(1.2, dragStartPitch + delta.Y * 0.01));
            RenderViewport();
            e.Handled = true;
            return;
        }

        if (draggedPointIndex < 0 || draggedPointIndex != selectedPointIndex)
        {
            return;
        }

        var projectedAxis = GetProjectedAxis(selectedAxis);
        var axisLengthSquared = projectedAxis.LengthSquared();
        if (axisLengthSquared < 0.001f)
        {
            return;
        }

        var amount = Vector2.Dot(delta, projectedAxis) / axisLengthSquared;
        var offset = selectedAxis switch
        {
            Axis.Y => new CubeDeformationOffset(dragStartOffset.X, dragStartOffset.Y + amount, dragStartOffset.Z),
            Axis.Z => new CubeDeformationOffset(dragStartOffset.X, dragStartOffset.Y, dragStartOffset.Z + amount),
            _ => new CubeDeformationOffset(dragStartOffset.X + amount, dragStartOffset.Y, dragStartOffset.Z)
        };
        ApplySelectedOffset(offset);
        e.Handled = true;
    }

    private void Viewport_PointerReleased(object sender, PointerRoutedEventArgs e) => FinishPointerOperation(e);

    private void Viewport_PointerCanceled(object sender, PointerRoutedEventArgs e) => FinishPointerOperation(e);

    private void FinishPointerOperation(PointerRoutedEventArgs e)
    {
        draggedPointIndex = -1;
        isRotating = false;
        viewport.ReleasePointerCapture(e.Pointer);
    }

    private void Viewport_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
    {
        var delta = e.GetCurrentPoint(viewport).Properties.MouseWheelDelta;
        zoom = Math.Max(48, Math.Min(150, zoom + delta / 12d));
        RenderViewport();
        e.Handled = true;
    }

    private void Viewport_SizeChanged(object sender, SizeChangedEventArgs e) => RenderViewport();

    private void RenderViewport()
    {
        if (viewport is null)
        {
            return;
        }

        viewport.Children.Clear();
        if (controlPoints.Count == 0)
        {
            return;
        }

        var projectedPoints = controlPoints.Select(ProjectPoint).ToArray();
        foreach (var face in Faces)
        {
            var center = projectedPoints[face.CenterIndex].Screen;
            for (var index = 0; index < face.CornerIndices.Length; index++)
            {
                var nextIndex = (index + 1) % face.CornerIndices.Length;
                var triangle = new Polygon
                {
                    Fill = faceBrush,
                    IsHitTestVisible = false,
                    Points =
                    {
                        projectedPoints[face.CornerIndices[index]].Screen,
                        projectedPoints[face.CornerIndices[nextIndex]].Screen,
                        center
                    }
                };
                viewport.Children.Add(triangle);
            }
        }

        foreach (var edge in CubeEdges)
        {
            AddLine(projectedPoints[edge[0]].Screen, projectedPoints[edge[1]].Screen, edgeBrush, 1.5);
        }

        if (selectedPointIndex >= 0)
        {
            DrawAxisGizmo(projectedPoints[selectedPointIndex].Screen);
        }

        foreach (var item in projectedPoints
                     .Select((point, index) => new { point, index })
                     .OrderBy(item => item.point.Depth))
        {
            var controlPoint = controlPoints[item.index];
            var isSelected = item.index == selectedPointIndex;
            var diameter = isSelected ? 18d : controlPoint.Kind == CubeDeformationControlKind.Corner ? 14d : 12d;
            var handle = new Ellipse
            {
                Width = diameter,
                Height = diameter,
                Fill = controlPoint.Kind == CubeDeformationControlKind.Corner ? cornerBrush : faceCenterBrush,
                Stroke = isSelected ? selectedBrush : edgeBrush,
                StrokeThickness = isSelected ? 3 : 1,
                Tag = item.index,
                Translation = new(0, 0, isSelected ? 24 : 12)
            };
            Canvas.SetLeft(handle, item.point.Screen.X - diameter / 2);
            Canvas.SetTop(handle, item.point.Screen.Y - diameter / 2);
            viewport.Children.Add(handle);
        }
    }

    private void DrawAxisGizmo(Point origin)
    {
        var axisLength = 0.72f;
        AddLine(origin, Add(origin, GetProjectedAxis(Axis.X) * axisLength), xAxisBrush, selectedAxis == Axis.X ? 4 : 2);
        AddLine(origin, Add(origin, GetProjectedAxis(Axis.Y) * axisLength), yAxisBrush, selectedAxis == Axis.Y ? 4 : 2);
        AddLine(origin, Add(origin, GetProjectedAxis(Axis.Z) * axisLength), zAxisBrush, selectedAxis == Axis.Z ? 4 : 2);
    }

    private void AddLine(Point from, Point to, Brush brush, double thickness)
    {
        viewport.Children.Add(new Line
        {
            X1 = from.X,
            Y1 = from.Y,
            X2 = to.X,
            Y2 = to.Y,
            Stroke = brush,
            StrokeThickness = thickness,
            IsHitTestVisible = false
        });
    }

    private ProjectedPoint ProjectPoint(CubeDeformationControlPoint point)
    {
        var position = new Vector3(
            point.LocalX - 1 + point.Offset.X,
            point.LocalY - 1 + point.Offset.Y,
            point.LocalZ - 1 + point.Offset.Z);
        var rotated = Rotate(position);
        var width = viewport.ActualWidth > 0 ? viewport.ActualWidth : 640;
        var height = viewport.ActualHeight > 0 ? viewport.ActualHeight : 480;
        return new(
            new Point(width / 2 + rotated.X * zoom, height / 2 - rotated.Y * zoom),
            rotated.Z);
    }

    private Vector2 GetProjectedAxis(Axis axis)
    {
        var vector = axis switch
        {
            Axis.Y => Vector3.UnitY,
            Axis.Z => Vector3.UnitZ,
            _ => Vector3.UnitX
        };
        var rotated = Rotate(vector);
        return new((float)(rotated.X * zoom), (float)(-rotated.Y * zoom));
    }

    private Vector3 Rotate(Vector3 value)
    {
        var cosYaw = (float)Math.Cos(yaw);
        var sinYaw = (float)Math.Sin(yaw);
        var yawX = cosYaw * value.X + sinYaw * value.Z;
        var yawZ = -sinYaw * value.X + cosYaw * value.Z;

        var cosPitch = (float)Math.Cos(pitch);
        var sinPitch = (float)Math.Sin(pitch);
        return new(
            yawX,
            cosPitch * value.Y - sinPitch * yawZ,
            sinPitch * value.Y + cosPitch * yawZ);
    }

    private static Point Add(Point point, Vector2 vector) => new(point.X + vector.X, point.Y + vector.Y);

    private enum Axis
    {
        X,
        Y,
        Z
    }

    private readonly struct FaceDefinition(int centerIndex, int[] cornerIndices)
    {
        public int CenterIndex { get; } = centerIndex;

        public int[] CornerIndices { get; } = cornerIndices;
    }

    private readonly struct ProjectedPoint(Point screen, float depth)
    {
        public Point Screen { get; } = screen;

        public float Depth { get; } = depth;
    }
}