diff --git a/UVtools.WPF/App.axaml b/UVtools.WPF/App.axaml new file mode 100644 index 0000000..922d3b3 --- /dev/null +++ b/UVtools.WPF/App.axaml @@ -0,0 +1,8 @@ + + + + + + diff --git a/UVtools.WPF/App.axaml.cs b/UVtools.WPF/App.axaml.cs new file mode 100644 index 0000000..62a22e2 --- /dev/null +++ b/UVtools.WPF/App.axaml.cs @@ -0,0 +1,24 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace UVtools.WPF +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow(); + } + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/UVtools.WPF/Controls/AdvancedImageBox.cs b/UVtools.WPF/Controls/AdvancedImageBox.cs new file mode 100644 index 0000000..047feac --- /dev/null +++ b/UVtools.WPF/Controls/AdvancedImageBox.cs @@ -0,0 +1,1167 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Skia; +using Avalonia.Styling; +using UVtools.Core.Extensions; + + + +namespace UVtools.WPF.Controls +{ + public class AdvancedImageBox : ScrollViewer, IStyleable + { + #region Sub Classes + + /// + /// Represents available levels of zoom in an control + /// + public class ZoomLevelCollection : IList + { + #region Public Constructors + + /// + /// Initializes a new instance of the class. + /// + public ZoomLevelCollection() + { + List = new SortedList(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The default values to populate the collection with. + /// Thrown if the collection parameter is null + public ZoomLevelCollection(IEnumerable collection) + : this() + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + AddRange(collection); + } + + #endregion + + #region Public Class Properties + + /// + /// Returns the default zoom levels + /// + public static ZoomLevelCollection Default + { + get + { + return new ZoomLevelCollection(new[] + { + 7, 10, 15, 20, 25, 30, 50, 70, 100, 150, 200, 300, 400, 500, 600, 700, 800, 1200, 1600 + }); + } + } + + #endregion + + #region Public Properties + + /// + /// Gets the number of elements contained in the . + /// + /// + /// The number of elements contained in the . + /// + public int Count => List.Count; + + /// + /// Gets a value indicating whether the is read-only. + /// + /// true if this instance is read only; otherwise, false. + /// true if the is read-only; otherwise, false. + /// + public bool IsReadOnly => false; + + /// + /// Gets or sets the zoom level at the specified index. + /// + /// The index. + public int this[int index] + { + get => List.Values[index]; + set + { + List.RemoveAt(index); + Add(value); + } + } + + #endregion + + #region Protected Properties + + /// + /// Gets or sets the backing list. + /// + protected SortedList List { get; set; } + + #endregion + + #region Public Members + + /// + /// Adds an item to the . + /// + /// The object to add to the . + public void Add(int item) + { + List.Add(item, item); + } + + /// + /// Adds a range of items to the . + /// + /// The items to add to the collection. + /// Thrown if the collection parameter is null. + public void AddRange(IEnumerable collection) + { + if (collection == null) + { + throw new ArgumentNullException(nameof(collection)); + } + + foreach (int value in collection) + { + Add(value); + } + } + + /// + /// Removes all items from the . + /// + public void Clear() + { + List.Clear(); + } + + /// + /// Determines whether the contains a specific value. + /// + /// The object to locate in the . + /// true if is found in the ; otherwise, false. + public bool Contains(int item) + { + return List.ContainsKey(item); + } + + /// + /// Copies a range of elements this collection into a destination . + /// + /// The that receives the data. + /// A 64-bit integer that represents the index in the at which storing begins. + public void CopyTo(int[] array, int arrayIndex) + { + for (int i = 0; i < this.Count; i++) + { + array[arrayIndex + i] = this.List.Values[i]; + } + } + + /// + /// Finds the index of a zoom level matching or nearest to the specified value. + /// + /// The zoom level. + public int FindNearest(int zoomLevel) + { + int nearestValue = this.List.Values[0]; + int nearestDifference = Math.Abs(nearestValue - zoomLevel); + for (int i = 1; i < Count; i++) + { + int value = List.Values[i]; + int difference = Math.Abs(value - zoomLevel); + if (difference < nearestDifference) + { + nearestValue = value; + nearestDifference = difference; + } + } + return nearestValue; + } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// A that can be used to iterate through the collection. + public IEnumerator GetEnumerator() + { + return List.Values.GetEnumerator(); + } + + /// + /// Determines the index of a specific item in the . + /// + /// The object to locate in the . + /// The index of if found in the list; otherwise, -1. + public int IndexOf(int item) + { + return List.IndexOfKey(item); + } + + /// + /// Not implemented. + /// + /// The index. + /// The item. + /// Not implemented + public void Insert(int index, int item) + { + throw new NotImplementedException(); + } + + /// + /// Returns the next increased zoom level for the given current zoom. + /// + /// The current zoom level. + /// The next matching increased zoom level for the given current zoom if applicable, otherwise the nearest zoom. + public int NextZoom(int zoomLevel) + { + var index = IndexOf(this.FindNearest(zoomLevel)); + if (index < this.Count - 1) + { + index++; + } + + return this[index]; + } + + /// + /// Returns the next decreased zoom level for the given current zoom. + /// + /// The current zoom level. + /// The next matching decreased zoom level for the given current zoom if applicable, otherwise the nearest zoom. + public int PreviousZoom(int zoomLevel) + { + var index = IndexOf(FindNearest(zoomLevel)); + if (index > 0) + { + index--; + } + + return this[index]; + } + + /// + /// Removes the first occurrence of a specific object from the . + /// + /// The object to remove from the . + /// true if was successfully removed from the ; otherwise, false. This method also returns false if is not found in the original . + public bool Remove(int item) + { + return List.Remove(item); + } + + /// + /// Removes the element at the specified index of the . + /// + /// The zero-based index of the element to remove. + public void RemoveAt(int index) + { + List.RemoveAt(index); + } + + /// + /// Copies the elements of the to a new array. + /// + /// An array containing copies of the elements of the . + public int[] ToArray() + { + int[] results; + + results = new int[Count]; + CopyTo(results, 0); + + return results; + } + + #endregion + + #region IList Members + + /// + /// Returns an enumerator that iterates through a collection. + /// + /// An object that can be used to iterate through the collection. + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + } + + #endregion + + #region Enums + + /// + /// Determines the sizing mode of an image hosted in an control. + /// + public enum SizeModes : byte + { + /// + /// The image is disiplayed according to current zoom and scroll properties. + /// + Normal, + + /// + /// The image is stretched to fill the client area of the control. + /// + Stretch, + + /// + /// The image is stretched to fill as much of the client area of the control as possible, whilst retaining the same aspect ratio for the width and height. + /// + //Fit + } + + [Flags] + public enum PanMouseButtons : byte + { + None = 0, + LeftButton = 1, + MiddleButton = 2, + RightButton = 4 + } + + /// + /// Describes the zoom action occuring + /// + [Flags] + public enum ImageZoomActions : byte + { + /// + /// No action. + /// + None = 0, + + /// + /// The control is increasing the zoom. + /// + ZoomIn = 1, + + /// + /// The control is decreasing the zoom. + /// + ZoomOut = 2, + + /// + /// The control zoom was reset. + /// + ActualSize = 4 + } + + #endregion + + #region Constants + public static readonly int MinZoom = 10; + public static readonly int MaxZoom = 3500; + #endregion + + public static StyledProperty GridCellSizeProperty = + AvaloniaProperty.Register(nameof(GridCellSize), 8, false, BindingMode.TwoWay); + + /// + /// Gets or sets the basic cell size + /// + public byte GridCellSize + { + get => GetValue(GridCellSizeProperty); + set => SetValue(GridCellSizeProperty, value); + } + + /// + /// Gets or sets the color used to create the checkerboard style background + /// + public ISolidColorBrush GridColor { get; set; } = Brushes.Gainsboro; + + /// + /// Gets or sets the color used to create the checkerboard style background + /// + public ISolidColorBrush GridColorAlternate { get; set; } = Brushes.White; + + /// + /// Gets or sets the image to be displayed + /// + public Bitmap Image + { + get => _image; + set + { + _image = value; + SizedContainer.Width = _image.Size.Width; + SizedContainer.Height = _image.Size.Height; + InvalidateVisual(); + } + } + + public bool IsHorizontalBarVisible + { + get + { + if (Image is null) return false; + return ScaledImageWidth > Viewport.Width; + } + } + + public bool IsVerticalBarVisible + { + get + { + if (Image is null) return false; + return ScaledImageHeight > Viewport.Height; + } + } + + /// + /// Gets or sets if the checkerboard background should be displayed + /// + public bool ShowGrid { get; set; } = true; + + public bool IsPanning + { + get => _isPanning; + protected set + { + if (_isPanning == value) return; + _isPanning = value; + _startScrollPosition = Offset; + + if (value) + { + Cursor = new Cursor(StandardCursorType.SizeAll); + //this.OnPanStart(EventArgs.Empty); + } + else + { + Cursor = Cursor.Default; + //this.OnPanEnd(EventArgs.Empty); + } + } + } + + public Point CenterPoint + { + get + { + Rect viewport = GetImageViewPort(); + return new Point(viewport.Width / 2, viewport.Height / 2); + } + } + + public bool AutoPan { get; set; } = true; + + public PanMouseButtons PanWithMouseButtons { get; set; } = PanMouseButtons.LeftButton | PanMouseButtons.MiddleButton | PanMouseButtons.RightButton; + + public bool PanWithArrows { get; set; } = true; + + public bool InvertMouse { get; set; } = false; + + public bool AutoCenter { get; set; } = true; + + public SizeModes SizeMode { get; set; } = SizeModes.Normal; + + private bool _allowZoom = true; + public virtual bool AllowZoom + { + get => _allowZoom; + set + { + if (AllowZoom != value) + { + _allowZoom = value; + + //this.OnAllowZoomChanged(EventArgs.Empty); + } + } + } + + ZoomLevelCollection _zoomLevels = ZoomLevelCollection.Default; + /// + /// Gets or sets the zoom levels. + /// + /// The zoom levels. + public virtual ZoomLevelCollection ZoomLevels + { + get => _zoomLevels; + set + { + if (ZoomLevels != value) + { + _zoomLevels = value; + + //this.OnZoomLevelsChanged(EventArgs.Empty); + } + } + } + + private int _zoom = 100; + /// + /// Gets or sets the zoom. + /// + /// The zoom. + public virtual int Zoom + { + get => _zoom; + set => SetZoom(value, value > Zoom ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut); + } + + public virtual bool IsActualSize => Zoom == 100; + + + //Our render target we compile everything to and present to the user + private RenderTargetBitmap RenderTarget; + private ISkiaDrawingContextImpl SkiaContext; + private Point _startMousePosition; + private Vector _startScrollPosition; + private bool _isPanning; + private Bitmap _image; + + public ContentControl FillContainer { get; } = new ContentControl + { + Background = Brushes.Transparent + }; + + public ContentControl SizedContainer { get; } = new ContentControl + { + Background = Brushes.Transparent + }; + + Type IStyleable.StyleKey => typeof(ScrollViewer); + + public AdvancedImageBox() + { + Content = FillContainer; + FillContainer.Content = SizedContainer; + FillContainer.PointerWheelChanged += FillContainerOnPointerWheelChanged; + + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + + //Container.PointerMoved += ScrollViewerOnPointerMoved; + //Container.PointerPressed += ScrollViewerOnPointerPressed; + //Container.PointerReleased += ScrollViewerOnPointerReleased; + + PropertyChanged += OnPropertyChanged; + } + + private void FillContainerOnPointerWheelChanged(object? sender, PointerWheelEventArgs e) + { + e.Handled = true; + if (AllowZoom && SizeMode == SizeModes.Normal) + { + // The MouseWheel event can contain multiple "spins" of the wheel so we need to adjust accordingly + double spins = Math.Abs(e.Delta.Y); + //Debug.WriteLine(e.GetPosition(this)); + // TODO: Really should update the source method to handle multiple increments rather than calling it multiple times + for (int i = 0; i < spins; i++) + { + ProcessMouseZoom(e.Delta.Y > 0, e.GetPosition(this)); + } + + //InvalidateVisual(); + } + } + + private void ProcessMouseZoom(bool isZoomIn, Point cursorPosition) + => PerformZoom(isZoomIn ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut, true, cursorPosition); + + /// + /// Returns an appropriate zoom level based on the specified action, relative to the current zoom level. + /// + /// The action to determine the zoom level. + /// Thrown if an unsupported action is specified. + private int GetZoomLevel(ImageZoomActions action) + { + int result; + + switch (action) + { + case ImageZoomActions.None: + result = Zoom; + break; + case ImageZoomActions.ZoomIn: + result = ZoomLevels.NextZoom(Zoom); + break; + case ImageZoomActions.ZoomOut: + result = ZoomLevels.PreviousZoom(Zoom); + break; + case ImageZoomActions.ActualSize: + result = 100; + break; + default: + throw new ArgumentOutOfRangeException(nameof(action)); + } + + return result; + } + + /// + /// Resets the property whilsts retaining the original . + /// + protected void RestoreSizeMode() + { + if (SizeMode != SizeModes.Normal) + { + var previousZoom = Zoom; + SizeMode = SizeModes.Normal; + Zoom = previousZoom; // Stop the zoom getting reset to 100% before calculating the new zoom + } + } + + private void PerformZoom(ImageZoomActions action, bool preservePosition) + => PerformZoom(action, preservePosition, CenterPoint); + + private void PerformZoom(ImageZoomActions action, bool preservePosition, Point relativePoint) + { + Point currentPixel = PointToImage(relativePoint); + int currentZoom = Zoom; + int newZoom = GetZoomLevel(action); + + RestoreSizeMode(); + SetZoom(newZoom, action); + + if (preservePosition && Zoom != currentZoom) + { + ScrollTo(currentPixel, relativePoint); + } + } + + /// + /// Determines whether the specified point is located within the image view port + /// + /// The point. + /// + /// true if the specified point is located within the image view port; otherwise, false. + /// + public virtual bool IsPointInImage(Point point) + => GetImageViewPort().Contains(point); + + /// + /// Determines whether the specified point is located within the image view port + /// + /// The X co-ordinate of the point to check. + /// The Y co-ordinate of the point to check. + /// + /// true if the specified point is located within the image view port; otherwise, false. + /// + public bool IsPointInImage(int x, int y) + => IsPointInImage(new Point(x, y)); + + /// + /// Determines whether the specified point is located within the image view port + /// + /// The X co-ordinate of the point to check. + /// The Y co-ordinate of the point to check. + /// + /// true if the specified point is located within the image view port; otherwise, false. + /// + public bool IsPointInImage(double x, double y) + => IsPointInImage(new Point(x, y)); + + /// + /// Converts the given client size point to represent a coordinate on the source image. + /// + /// The source point. + /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point + public Point PointToImage(Point point) + => PointToImage(point, false); + + /// + /// Converts the given client size point to represent a coordinate on the source image. + /// + /// The X co-ordinate of the point to convert. + /// The Y co-ordinate of the point to convert. + /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point + public Point PointToImage(double x, double y) + => PointToImage(x, y, false); + + /// + /// Converts the given client size point to represent a coordinate on the source image. + /// + /// The X co-ordinate of the point to convert. + /// The Y co-ordinate of the point to convert. + /// + /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. + /// + /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point + public Point PointToImage(double x, double y, bool fitToBounds) + => PointToImage(new Point(x, y), fitToBounds); + + /// + /// Converts the given client size point to represent a coordinate on the source image. + /// + /// The X co-ordinate of the point to convert. + /// The Y co-ordinate of the point to convert. + /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point + public Point PointToImage(int x, int y) + { + return PointToImage(x, y, false); + } + + /// + /// Converts the given client size point to represent a coordinate on the source image. + /// + /// The source point. + /// + /// if set to true and the point is outside the bounds of the source image, it will be mapped to the nearest edge. + /// + /// Point.Empty if the point could not be matched to the source image, otherwise the new translated point + public virtual Point PointToImage(Point point, bool fitToBounds) + { + double x; + double y; + + var viewport = GetImageViewPort(); + + if (!fitToBounds || viewport.Contains(point)) + { + x = (point.X + Offset.X - viewport.X) / ZoomFactor; + y = (point.Y + Offset.Y - viewport.Y) / ZoomFactor; + + if (fitToBounds) + { + x = x.Clamp(0, Image.Size.Width); + y = y.Clamp(0, Image.Size.Height); + } + } + else + { + x = 0; // Return Point.Empty if we couldn't match + y = 0; + } + + return new Point(x, y); + } + + /// + /// Scrolls the control to the given point in the image, offset at the specified display point + /// + /// The X co-ordinate of the point to scroll to. + /// The Y co-ordinate of the point to scroll to. + /// The X co-ordinate relative to the x parameter. + /// The Y co-ordinate relative to the y parameter. + public void ScrollTo(double x, double y, double relativeX, double relativeY) + => ScrollTo(new Point(x, y), new Point(relativeX, relativeY)); + + /// + /// Scrolls the control to the given point in the image, offset at the specified display point + /// + /// The X co-ordinate of the point to scroll to. + /// The Y co-ordinate of the point to scroll to. + /// The X co-ordinate relative to the x parameter. + /// The Y co-ordinate relative to the y parameter. + public void ScrollTo(int x, int y, int relativeX, int relativeY) + => ScrollTo(new Point(x, y), new Point(relativeX, relativeY)); + + /// + /// Scrolls the control to the given point in the image, offset at the specified display point + /// + /// The point of the image to attempt to scroll to. + /// The relative display point to offset scrolling by. + public virtual void ScrollTo(Point imageLocation, Point relativeDisplayPoint) + { + var x = HorizontalScrollBarMaximum - imageLocation.X * ZoomFactor + relativeDisplayPoint.X; + var y = VerticalScrollBarMaximum - imageLocation.Y * ZoomFactor + relativeDisplayPoint.Y; + + Offset = new Vector(x, y); + Debug.WriteLine($"{Offset} | {HorizontalScrollBarMaximum},{VerticalScrollBarMaximum} | {HorizontalScrollBarValue},{VerticalScrollBarValue}"); + } + + /// + /// Updates the current zoom. + /// + /// The new zoom value. + /// The zoom actions that caused the value to be updated. + /// The source of the zoom operation. + private void SetZoom(int value, ImageZoomActions actions) + { + var previousZoom = Zoom; + value = value.Clamp(MinZoom, MaxZoom); + + if (_zoom != value) + { + _zoom = value; + if (!UpdateViewPort()) + { + InvalidateArrange(); + } + + + //this.OnZoomChanged(EventArgs.Empty); + //this.OnZoomed(new ImageBoxZoomEventArgs(actions, source, previousZoom, this.Zoom)); + } + } + + /// + /// Zooms into the image + /// + public virtual void ZoomIn() + => ZoomIn(true); + + /// + /// Zooms into the image + /// + /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. + public virtual void ZoomIn(bool preservePosition) + { + //this.PerformZoomIn(ImageBoxActionSources.Unknown, preservePosition); + } + + /// + /// Zooms out of the image + /// + public virtual void ZoomOut() + => ZoomOut(true); + + /// + /// Zooms out of the image + /// + /// true if the current scrolling position should be preserved relative to the new zoom level, false to reset. + public virtual void ZoomOut(bool preservePosition) + { + // this.PerformZoomOut(ImageBoxActionSources.Unknown, preservePosition); + } + + /// + /// Zooms to the maximum size for displaying the entire image within the bounds of the control. + /// + public virtual void ZoomToFit() + { + if (Image is null) return; + + double zoom; + double aspectRatio; + + if (Image.Size.Width > Image.Size.Height) + { + aspectRatio = Viewport.Width / Image.Size.Width; + zoom = aspectRatio * 100.0; + + if (Viewport.Height < Image.Size.Height * zoom / 100.0) + { + aspectRatio = Viewport.Height / Image.Size.Height; + zoom = aspectRatio * 100.0; + } + } + else + { + aspectRatio = Viewport.Height / Image.Size.Height; + zoom = aspectRatio * 100.0; + + if (Viewport.Width < Image.Size.Width * zoom / 100.0) + { + aspectRatio = Viewport.Width / Image.Size.Width; + zoom = aspectRatio * 100.0; + } + } + + Zoom = (int)Math.Round(Math.Floor(zoom)); + } + + /// + /// Centers the given point in the image in the center of the control + /// + /// The point of the image to attempt to center. + public virtual void CenterAt(Point imageLocation) + => ScrollTo(imageLocation, new Point(Viewport.Width / 2, Viewport.Height / 2)); + + /// + /// Centers the given point in the image in the center of the control + /// + /// The X co-ordinate of the point to center. + /// The Y co-ordinate of the point to center. + public void CenterAt(int x, int y) + => CenterAt(new Point(x, y)); + + /// + /// Centers the given point in the image in the center of the control + /// + /// The X co-ordinate of the point to center. + /// The Y co-ordinate of the point to center. + public void CenterAt(double x, double y) + => CenterAt(new Point(x, y)); + + /// + /// Resets the viewport to show the center of the image. + /// + public virtual void CenterToImage() + { + Offset = new Point(HorizontalScrollBarMaximum / 2, VerticalScrollBarMaximum / 2); + } + + private bool UpdateViewPort() + { + if (Image is null) return false; + + var scaledImageWidth = ScaledImageWidth; + var scaledImageHeight = ScaledImageHeight; + var width = scaledImageWidth <= Viewport.Width ? Viewport.Width : scaledImageWidth; + var height = scaledImageHeight <= Viewport.Height ? Viewport.Height : scaledImageHeight; + + bool changed = false; + if (SizedContainer.Width != width) + { + SizedContainer.Width = width; + changed = true; + } + + if (SizedContainer.Height != height) + { + SizedContainer.Height = height; + changed = true; + } + + if (changed) + { + Debug.WriteLine($"Update ViewPort: {DateTime.Now.Ticks}"); + InvalidateArrange(); + } + + return changed; + } + + /// + /// Resets the zoom to 100%. + /// + /// The source that initiated the action. + private void PerformActualSize() + { + SizeMode = SizeModes.Normal; + SetZoom(100, ImageZoomActions.ActualSize | (Zoom < 100 ? ImageZoomActions.ZoomIn : ImageZoomActions.ZoomOut)); + } + + private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) + { + //Debug.WriteLine(e.Property.Name); + if (e.Property.Name == nameof(VerticalScrollBarValue) || + e.Property.Name == nameof(HorizontalScrollBarValue)) + { + InvalidateArrange(); + return; + } + + if(e.Property.Name.Equals(nameof(Viewport))) + { + //if (SupressViewPortPropertyChange) return; + UpdateViewPort(); + return; + } + } + + #region Overrides + + /*public override void EndInit() + { + base.EndInit(); + SKPaint SKBrush = new SKPaint { IsAntialias = true, Color = new SKColor(0, 0, 0) }; + SKBrush.Shader = SKShader.CreateColor(SKBrush.Color); + //RenderTarget = new RenderTargetBitmap(new PixelSize((int)Viewport.Width, (int)Viewport.Height), new Vector(96, 96)); + + //var context = RenderTarget.CreateDrawingContext(null); + //SkiaContext = (context as ISkiaDrawingContextImpl); + //SkiaContext.SkCanvas.Clear(new SKColor(100, 100, 255)); + }*/ + + #endregion + + #region Methods + + + + public void LoadImage(string path) + { + Image = new Bitmap(path); + //ImageControl.Source = Image; + //ImageControl.InvalidateVisual(); + } + + public override void Render(DrawingContext context) + { + Debug.WriteLine($"Render: {DateTime.Now.Ticks}"); + // base.Render(context); + if (ShowGrid) + { + // draw the background + var currentColor = GridColor; + for (int y = 0; y < Viewport.Height; y += GridCellSize) + { + var firstRowColor = currentColor; + for (int x = 0; x < Viewport.Width; x += GridCellSize) + { + context.FillRectangle(currentColor, new Rect(x, y, GridCellSize, GridCellSize)); + currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor; + } + + if(firstRowColor == currentColor) currentColor = ReferenceEquals(currentColor, GridColor) ? GridColorAlternate : GridColor; + } + + } + else + { + context.FillRectangle(Background, new Rect(0, 0, Viewport.Width, Viewport.Height)); + //SkiaContext.SkCanvas.Clear(new SKColor(100, 100, 255)); + } + + if (Image is null) return; + + context.DrawImage(Image, 1.0, + GetSourceImageRegion(), + GetImageViewPort() + ); + /*using (var surface = SKSurface.Create(SkiaContext.GrContext, RenderTarget)) + { + surface.Canvas.DrawImage(Image, 0f, 0f); + surface.Canvas.Flush(); + var textureImage = surface.Snapshot(); //This should be texture backed + }*/ + + /*context.DrawImage(RenderTarget, 1.0, + new Rect(0, 0, RenderTarget.PixelSize.Width, RenderTarget.PixelSize.Height), + new Rect(0, 0, Width, Height) + );*/ + } + + /// + /// Gets the source image region. + /// + /// + public virtual Rect GetSourceImageRegion() + { + if (Image is null) return new Rect(0, 0, 0, 0); + + if (SizeMode != SizeModes.Stretch) + { + var viewPort = GetImageViewPort(); + var sourceLeft = Offset.X / ZoomFactor; + var sourceTop = Offset.Y / ZoomFactor; + var sourceWidth = viewPort.Width / ZoomFactor; + var sourceHeight = viewPort.Height / ZoomFactor; + + return new Rect(sourceLeft, sourceTop, sourceWidth, sourceHeight); + } + + return new Rect(0, 0, Image.Size.Width, Image.Size.Height); + } + + /// + /// Gets the image view port. + /// + /// + public virtual Rect GetImageViewPort() + { + if (Viewport.Width == 0 && Viewport.Height == 0) return Rect.Empty; + + double xOffset = 0; + double yOffset = 0; + double width; + double height; + + if (SizeMode != SizeModes.Stretch) + { + if (AutoCenter) + { + xOffset = !IsHorizontalBarVisible ? (Viewport.Width - ScaledImageWidth) / 2 : 0; + yOffset = !IsVerticalBarVisible ? (Viewport.Height - ScaledImageHeight) / 2 : 0; + } + + width = Math.Min(ScaledImageWidth - Math.Abs(Offset.X), Viewport.Width); + height = Math.Min(ScaledImageHeight - Math.Abs(Offset.Y), Viewport.Height); + } + else + { + width = Viewport.Width; + height = Viewport.Height; + } + + return new Rect(xOffset, yOffset, width, height); + } + + /// + /// Gets the width of the scaled image. + /// + /// The width of the scaled image. + protected virtual double ScaledImageWidth => Image.Size.Width * ZoomFactor; + + /// + /// Gets the height of the scaled image. + /// + /// The height of the scaled image. + protected virtual double ScaledImageHeight => Image.Size.Height * ZoomFactor; + + public double ZoomFactor => Zoom / 100.0; + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (e.Handled) return; + + var pointer = e.GetCurrentPoint(this); + if (!( + pointer.Properties.IsLeftButtonPressed && (PanWithMouseButtons & PanMouseButtons.LeftButton) != 0 || + pointer.Properties.IsMiddleButtonPressed && (PanWithMouseButtons & PanMouseButtons.MiddleButton) != 0 || + pointer.Properties.IsRightButtonPressed && (PanWithMouseButtons & PanMouseButtons.RightButton) != 0 + ) + || !AutoPan || IsPanning || Image == null) return; + var location = pointer.Position; + + if (location.X > Viewport.Width) return; + if (location.Y > Viewport.Height) return; + _startMousePosition = location; + IsPanning = true; + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + if (e.Handled) return; + + if (IsPanning) IsPanning = false; + } + + protected override void OnPointerMoved(PointerEventArgs e) + { + base.OnPointerMoved(e); + if (e.Handled) return; + + if (!IsPanning) return; + var pointer = e.GetCurrentPoint(this); + var location = pointer.Position; + + double x; + double y; + + if (!InvertMouse) + { + x = _startScrollPosition.X + (_startMousePosition.X - location.X); + y = _startScrollPosition.Y + (_startMousePosition.Y - location.Y); + } + else + { + x = (_startScrollPosition.X - (_startMousePosition.X - location.X)); + y = (_startScrollPosition.Y - (_startMousePosition.Y - location.Y)); + } + + Offset = new Point(x, y); + e.Handled = true; + } + #endregion + } +} diff --git a/UVtools.WPF/Extensions/MatrixHelper.cs b/UVtools.WPF/Extensions/MatrixHelper.cs new file mode 100644 index 0000000..f8b1e78 --- /dev/null +++ b/UVtools.WPF/Extensions/MatrixHelper.cs @@ -0,0 +1,131 @@ +using System; +using Avalonia; + +namespace UVtools.WPF.Extensions +{ + /// + /// Avalonia Matrix helper methods. + /// + public static class MatrixHelper + { + /// + /// Creates a translation matrix using the specified offsets. + /// + /// X-coordinate offset. + /// Y-coordinate offset. + /// The created translation matrix. + public static Matrix Translate(double offsetX, double offsetY) + { + return new Matrix(1.0, 0.0, 0.0, 1.0, offsetX, offsetY); + } + + /// + /// Prepends a translation around the center of provided matrix. + /// + /// The matrix to prepend translation. + /// X-coordinate offset. + /// Y-coordinate offset. + /// The created translation matrix. + public static Matrix TranslatePrepend(Matrix matrix, double offsetX, double offsetY) + { + return Translate(offsetX, offsetY) * matrix; + } + + /// + /// Creates a matrix that scales along the x-axis and y-axis. + /// + /// Scaling factor that is applied along the x-axis. + /// Scaling factor that is applied along the y-axis. + /// The created scaling matrix. + public static Matrix Scale(double scaleX, double scaleY) + { + return new Matrix(scaleX, 0, 0, scaleY, 0.0, 0.0); + } + + /// + /// Creates a matrix that is scaling from a specified center. + /// + /// Scaling factor that is applied along the x-axis. + /// Scaling factor that is applied along the y-axis. + /// The center X-coordinate of the scaling. + /// The center Y-coordinate of the scaling. + /// The created scaling matrix. + public static Matrix ScaleAt(double scaleX, double scaleY, double centerX, double centerY) + { + return new Matrix(scaleX, 0, 0, scaleY, centerX - (scaleX * centerX), centerY - (scaleY * centerY)); + } + + /// + /// Prepends a scale around the center of provided matrix. + /// + /// The matrix to prepend scale. + /// Scaling factor that is applied along the x-axis. + /// Scaling factor that is applied along the y-axis. + /// The center X-coordinate of the scaling. + /// The center Y-coordinate of the scaling. + /// The created scaling matrix. + public static Matrix ScaleAtPrepend(Matrix matrix, double scaleX, double scaleY, double centerX, double centerY) + { + return ScaleAt(scaleX, scaleY, centerX, centerY) * matrix; + } + + /// + /// Creates a skew matrix. + /// + /// Angle of skew along the X-axis in radians. + /// Angle of skew along the Y-axis in radians. + /// When the method completes, contains the created skew matrix. + public static Matrix Skew(float angleX, float angleY) + { + return new Matrix(1.0, Math.Tan(angleX), Math.Tan(angleY), 1.0, 0.0, 0.0); + } + + /// + /// Creates a matrix that rotates. + /// + /// Angle of rotation in radians. Angles are measured clockwise when looking along the rotation axis. + /// The created rotation matrix. + public static Matrix Rotation(double radians) + { + double cos = Math.Cos(radians); + double sin = Math.Sin(radians); + return new Matrix(cos, sin, -sin, cos, 0, 0); + } + + /// + /// Creates a matrix that rotates about a specified center. + /// + /// Angle of rotation in radians. + /// The center X-coordinate of the rotation. + /// The center Y-coordinate of the rotation. + /// The created rotation matrix. + public static Matrix Rotation(double angle, double centerX, double centerY) + { + return Translate(-centerX, -centerY) * Rotation(angle) * Translate(centerX, centerY); + } + + /// + /// Creates a matrix that rotates about a specified center. + /// + /// Angle of rotation in radians. + /// The center of the rotation. + /// The created rotation matrix. + public static Matrix Rotation(double angle, Vector center) + { + return Translate(-center.X, -center.Y) * Rotation(angle) * Translate(center.X, center.Y); + } + + /// + /// Transforms a point by this matrix. + /// + /// The matrix to use as a transformation matrix. + /// >The original point to apply the transformation. + /// The result of the transformation for the input point. + public static Point TransformPoint(Matrix matrix, Point point) + { + return new Point( + (point.X * matrix.M11) + (point.Y * matrix.M21) + matrix.M31, + (point.X * matrix.M12) + (point.Y * matrix.M22) + matrix.M32); + } + } +} diff --git a/UVtools.WPF/Extensions/PrimitivesExtensions.cs b/UVtools.WPF/Extensions/PrimitivesExtensions.cs new file mode 100644 index 0000000..d4ff66c --- /dev/null +++ b/UVtools.WPF/Extensions/PrimitivesExtensions.cs @@ -0,0 +1,12 @@ +using Avalonia; + +namespace UVtools.WPF.Extensions +{ + public static class PrimitivesExtensions + { + public static bool IsEmpty(this Point point) + { + return point.X == 0 && point.Y == 0; + } + } +} diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml new file mode 100644 index 0000000..0e97bee --- /dev/null +++ b/UVtools.WPF/MainWindow.axaml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs new file mode 100644 index 0000000..f4b19ff --- /dev/null +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -0,0 +1,42 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using UVtools.WPF.Controls; + +namespace UVtools.WPF +{ + public class MainWindow : Window + { + public AdvancedImageBox LayerImage; + public Button ZoomToFitButton; + public Button CenterButton; + public MainWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + LayerImage = this.FindControl("Layer.ImageOld"); + ZoomToFitButton = this.FindControl