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