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 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; } }