Files
UVtools/UVtools.WPF/MainWindow.PixelEditor.cs
T
Tiago Conceição b2617d149f v2.0.0
This release bump the major version due the introduction of .NET 5.0, the discontinuation old UVtools GUI project and the new calibration wizards.
* (Upgrade) From .NET Core 3.1 to .NET 5.0
* (Upgrade) From C# 8.0 to C# 9.0
* (Upgrade) From Avalonia preview6 to rc1
    * Bug: The per layer data gets hidden and not auto height on this rc1
* (Add) Setting - General - Windows / dialogs:
  * **Take into account the screen scale factor to limit the dialogs windows maximum size**: Due wrong information UVtools can clamp the windows maximum size when you have plenty more avaliable or when use in a secondary monitor. If is the case disable this option
  * **Horizontal limiting margin:** Limits windows and dialogs maximum width to the screen resolution less this margin
  * **Vertical limiting margin:** Limits windows and dialogs maximum height to the screen resolution less this margin
* (Add) Setting - General: Take into account the screen scale factor to limit the dialogs windows maximum size. Due wrong information UVtools can cap the windows maximum size when you have plenty more avaliable or when use in a secondary monitor. If is the case disable this option
* (Add) Ctrl + Shift + Z to undo and edit the last operation (If contain a valid operation)
* (Add) Allow to deselect the current selected profile
* (Add) Allow to set a default profile to load in when open a tool
* (Add) ENTER and ESC hotkeys to message box
* (Add) Pixel dimming: Brightness percent equivalent value
* (Add) Raft relief: Allow to define supports margin independent from wall margin for the "Relief" type
* (Add) Pixel editor: Allow to adjust the remove and add pixel brightness values
* (Add) Calibration Menu:
    * **Elephant foot:** Generates test models with various strategies and increments to verify the best method/values to remove the elephant foot.
    * **XYZ Accuracy:** Generates test models with various strategies and increments to verify the XYZ accuracy.
    * **Tolerance:** Generates test models with various strategies and increments to verify the part tolerances.
    * **Grayscale:** Generates test models with various strategies and increments to verify the LED power against the grayscale levels.
* (Change) PW0, PWS, PWMX, PWMO, PWMS, PWX file formats to ignore preview validation and allow variations on the file format (#111)
* (Change) Tool - Edit print parameters: Increments from 0.01 to 0.5
* (Change) Tool - Resize: Increments from 0.01 to 0.1
* (Change) Tool - Rotate: Increments from 0.01 to 1
* (Change) Tool - Calculator: Increments from 0.01 to 0.5 and 1
* (Fix) PW0, PWS, PWMX, PWMO, PWMS, PWX file formats to replicate missing bottom properties cloned from normal properties
* (Fix) Drain holes to build plate were considered as traps, changed to be drains as when removing object resin will flow outwards
* (Fix) When unable to save the file it will change extension and not delete the temporary file
* (Fix) Pixel dimming wasn't saving all the fields on profiles
* (Fix) Prevent a rare startup crash when using demo file
* (Fix) Tool - Solifiy: Increase AA clean up threshold range, previous value wasn't solidifing when model has darker tones
* (Fix) Sanitize per layer settings, due some slicers are setting 0 at some properties that can cause problems with UVtools calculations, those values are now sanitized and set to the general value if 0
* (Fix) Update partial islands:
    * Was leaving visible issues when the result returns an empty list of new issues
    * Was jumping some modified sequential layers
    * Was not updating the issue tracker map
* (Fix) Edit print parameters was not updating the layer data table information
2020-12-25 04:38:28 +00:00

458 lines
19 KiB
C#

/*
* GNU AFFERO GENERAL PUBLIC LICENSE
* Version 3, 19 November 2007
* Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
* Everyone is permitted to copy and distribute verbatim copies
* of this license document, but changing it is not allowed.
*/
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Skia;
using Avalonia.Threading;
using DynamicData;
using Emgu.CV;
using Emgu.CV.CvEnum;
using Emgu.CV.Structure;
using MessageBox.Avalonia.Enums;
using SkiaSharp;
using UVtools.Core;
using UVtools.Core.Extensions;
using UVtools.Core.PixelEditor;
using UVtools.WPF.Extensions;
namespace UVtools.WPF
{
public partial class MainWindow
{
public ObservableCollection<PixelOperation> Drawings { get; } = new ObservableCollection<PixelOperation>();
public DataGrid DrawingsGrid;
private int _selectedPixelOperationTabIndex;
public PixelDrawing DrawingPixelDrawing { get; } = new PixelDrawing();
public PixelText DrawingPixelText { get; } = new PixelText();
public PixelEraser DrawingPixelEraser { get; } = new PixelEraser();
public PixelSupport DrawingPixelSupport { get; } = new PixelSupport();
public PixelDrainHole DrawingPixelDrainHole { get; } = new PixelDrainHole();
public int SelectedPixelOperationTabIndex
{
get => _selectedPixelOperationTabIndex;
set => RaiseAndSetIfChanged(ref _selectedPixelOperationTabIndex, value);
}
public void InitPixelEditor()
{
DrawingsGrid = this.FindControl<DataGrid>("DrawingsGrid");
DrawingsGrid.KeyUp += DrawingsGridOnKeyUp;
DrawingsGrid.SelectionChanged += DrawingsGridOnSelectionChanged;
DrawingsGrid.CellPointerPressed += DrawingsGridOnCellPointerPressed;
}
private void DrawingsGridOnSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (!(DrawingsGrid.SelectedItem is PixelOperation operation))
{
ShowLayer();
return;
}
Point location = GetTransposedPoint(operation.Location, false);
if (Settings.LayerPreview.ZoomIssues ^ (_globalModifiers & KeyModifiers.Alt) != 0)
{
CenterLayerAt(new Rectangle(location, operation.Size), AppSettings.LockedZoomLevel);
}
else
{
CenterLayerAt(location);
}
ForceUpdateActualLayer(operation.LayerIndex);
}
private void DrawingsGridOnCellPointerPressed(object? sender, DataGridCellPointerPressedEventArgs e)
{
if (e.PointerPressedEventArgs.ClickCount == 2) return;
if (!(DrawingsGrid.SelectedItem is LayerIssue issue)) return;
// Double clicking an issue will center and zoom into the
// selected issue. Left click on an issue will zoom to fit.
var pointer = e.PointerPressedEventArgs.GetCurrentPoint(DrawingsGrid);
if (pointer.Properties.IsRightButtonPressed)
{
ZoomToFit();
return;
}
}
private void DrawingsGridOnKeyUp(object? sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
DrawingsGrid.SelectedItems.Clear();
break;
case Key.Multiply:
var selectedItems = DrawingsGrid.SelectedItems.OfType<PixelOperation>().ToList();
DrawingsGrid.SelectedItems.Clear();
foreach (PixelOperation item in Drawings)
{
if (!selectedItems.Contains(item))
DrawingsGrid.SelectedItems.Add(item);
}
break;
case Key.Delete:
OnClickDrawingRemove();
break;
}
}
public void OnClickDrawingRemove()
{
if (DrawingsGrid.SelectedItems.Count == 0) return;
Drawings.RemoveMany(DrawingsGrid.SelectedItems.Cast<PixelOperation>());
ShowLayer();
}
public async void OnClickDrawingClear()
{
if (Drawings.Count == 0) return;
if (await this.MessageBoxQuestion($"Are you sure you want to clear {Drawings.Count} operations?",
"Clear pixel editor operations?") != ButtonResult.Yes) return;
Drawings.Clear();
ShowLayer();
}
void DrawPixel(bool isAdd, Point location, KeyModifiers keyModifiers)
{
//Stopwatch sw = Stopwatch.StartNew();
//var point = pbLayer.PointToImage(location);
Point realLocation = GetTransposedPoint(location);
if ((keyModifiers & KeyModifiers.Control) != 0)
{
var removeItems = Drawings.Where(item =>
{
Rectangle rect = new Rectangle(item.Location, item.Size);
rect.X -= item.Size.Width / 2;
rect.Y -= item.Size.Height / 2;
return rect.Contains(realLocation);
});
if (removeItems.Any())
{
Drawings.RemoveMany(removeItems);
ShowLayer();
}
return;
}
WriteableBitmap bitmap = (WriteableBitmap)LayerImageBox.Image;
//var context = CreateRenderTarget().CreateDrawingContext(bitmap);
//Bitmap bmp = pbLayer.Image as Bitmap;
if (SelectedPixelOperationTabIndex == (byte)PixelOperation.PixelOperationType.Drawing)
{
uint minLayer = Math.Max(0, _actualLayer - DrawingPixelDrawing.LayersBelow);
uint maxLayer = Math.Min(SlicerFile.LayerCount - 1, _actualLayer + DrawingPixelDrawing.LayersAbove);
for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++)
{
var operationDrawing = new PixelDrawing(layerIndex, realLocation, DrawingPixelDrawing.LineType,
DrawingPixelDrawing.BrushShape, DrawingPixelDrawing.BrushSize, DrawingPixelDrawing.Thickness, DrawingPixelDrawing.RemovePixelBrightness, DrawingPixelDrawing.PixelBrightness, isAdd);
//if (PixelHistory.Contains(operation)) continue;
Drawings.Add(operationDrawing);
if (layerIndex == _actualLayer)
{
var color = isAdd
? Settings.PixelEditor.AddPixelColor
: Settings.PixelEditor.RemovePixelColor;
if (operationDrawing.BrushSize == 1)
{
unsafe
{
using (var framebuffer = bitmap.Lock())
{
var data = (uint*)framebuffer.Address.ToPointer();
data[bitmap.GetPixelPos(location)] =
color.ToUint32();
}
}
LayerImageBox.InvalidateArrange();
// LayerCache.ImageBgr.SetByte(operationDrawing.Location.X, operationDrawing.Location.Y,
// new[] { color.B, color.G, color.R });
continue;
}
switch (operationDrawing.BrushShape)
{
case PixelDrawing.BrushShapeType.Rectangle:
int shiftPos = operationDrawing.BrushSize / 2;
LayerCache.Canvas.DrawRect(location.X - shiftPos, location.Y - shiftPos,
operationDrawing.BrushSize,
operationDrawing.BrushSize,
new SKPaint
{
IsAntialias = operationDrawing.LineType == LineType.AntiAlias,
Color = new SKColor(color.ToUint32()),
IsStroke = operationDrawing.Thickness >= 0,
StrokeWidth = operationDrawing.Thickness
} );
/*CvInvoke.Rectangle(LayerCache.ImageBgr, GetTransposedRectangle(operationDrawing.Rectangle),
new MCvScalar(color.B, color.G, color.R), operationDrawing.Thickness,
operationDrawing.LineType);*/
break;
case PixelDrawing.BrushShapeType.Circle:
LayerCache.Canvas.DrawCircle(location.X, location.Y, operationDrawing.BrushSize / 2f,
new SKPaint
{
IsAntialias = operationDrawing.LineType == LineType.AntiAlias,
Color = new SKColor(color.ToUint32()),
IsStroke = operationDrawing.Thickness >= 0,
StrokeWidth = operationDrawing.Thickness
});
/*CvInvoke.Circle(LayerCache.ImageBgr, location, operationDrawing.BrushSize / 2,
new MCvScalar(color.B, color.G, color.R), operationDrawing.Thickness,
operationDrawing.LineType);*/
break;
default:
throw new ArgumentOutOfRangeException();
}
LayerImageBox.InvalidateVisual();
//RefreshLayerImage();
}
}
}
else if (SelectedPixelOperationTabIndex == (byte)PixelOperation.PixelOperationType.Text)
{
if (string.IsNullOrEmpty(DrawingPixelText.Text) || DrawingPixelText.FontScale < 0.2) return;
uint minLayer = Math.Max(0, ActualLayer - DrawingPixelText.LayersBelow);
uint maxLayer = Math.Min(SlicerFile.LayerCount - 1,
ActualLayer + DrawingPixelText.LayersAbove);
for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++)
{
var operationText = new PixelText(layerIndex, realLocation, DrawingPixelText.LineType,
DrawingPixelText.Font, DrawingPixelText.FontScale, DrawingPixelText.Thickness,
DrawingPixelText.Text, DrawingPixelText.Mirror, DrawingPixelText.RemovePixelBrightness, DrawingPixelText.PixelBrightness, isAdd);
//if (PixelHistory.Contains(operation)) continue;
//PixelHistory.Add(operation);
Drawings.Add(operationText);
/*var color = isAdd
? Settings.PixelEditor.AddPixelColor : Settings.PixelEditor.RemovePixelColor;
if (layerIndex == _actualLayer)
{
CvInvoke.PutText(LayerCache.ImageBgr, operationText.Text, location,
operationText.Font, operationText.FontScale, new MCvScalar(color.B, color.G, color.R),
operationText.Thickness, operationText.LineType, operationText.Mirror);
RefreshLayerImage();
}*/
}
ShowLayer();
return;
}
else if (SelectedPixelOperationTabIndex == (byte)PixelOperation.PixelOperationType.Eraser)
{
if (LayerCache.Image.GetByte(realLocation) < 10) return;
uint minLayer = Math.Max(0, ActualLayer - DrawingPixelEraser.LayersBelow);
uint maxLayer = Math.Min(SlicerFile.LayerCount - 1,
ActualLayer + DrawingPixelEraser.LayersAbove);
for (uint layerIndex = minLayer; layerIndex <= maxLayer; layerIndex++)
{
var operationEraser = new PixelEraser(layerIndex, realLocation, DrawingPixelEraser.PixelBrightness);
//if (PixelHistory.Contains(operation)) continue;
Drawings.Add(operationEraser);
/*if (layerIndex == _actualLayer)
{
for (int i = 0; i < LayerCache.LayerContours.Size; i++)
{
if (CvInvoke.PointPolygonTest(LayerCache.LayerContours[i], operationEraser.Location, false) >= 0)
{
CvInvoke.DrawContours(LayerCache.ImageBgr, LayerCache.LayerContours, i,
new MCvScalar(Settings.PixelEditor.RemovePixelColor.B, Settings.PixelEditor.RemovePixelColor.G, Settings.PixelEditor.RemovePixelColor.R), -1);
RefreshLayerImage();
break;
}
}
}*/
}
ShowLayer();
return;
}
else if (SelectedPixelOperationTabIndex == (byte)PixelOperation.PixelOperationType.Supports)
{
if (_actualLayer == 0) return;
var operationSupport = new PixelSupport(ActualLayer, realLocation,
DrawingPixelSupport.TipDiameter, DrawingPixelSupport.PillarDiameter,
DrawingPixelSupport.BaseDiameter, DrawingPixelSupport.PixelBrightness);
//if (PixelHistory.Contains(operation)) return;
Drawings.Add(operationSupport);
CvInvoke.Circle(LayerCache.ImageBgr, location, operationSupport.TipDiameter / 2,
new MCvScalar(Settings.PixelEditor.SupportsColor.B, Settings.PixelEditor.SupportsColor.G, Settings.PixelEditor.SupportsColor.R), -1);
RefreshLayerImage();
}
else if (SelectedPixelOperationTabIndex == (byte)PixelOperation.PixelOperationType.DrainHole)
{
if (_actualLayer == 0) return;
var operationDrainHole = new PixelDrainHole(ActualLayer, realLocation, DrawingPixelDrainHole.Diameter);
//if (PixelHistory.Contains(operation)) return;
Drawings.Add(operationDrainHole);
CvInvoke.Circle(LayerCache.ImageBgr, location, operationDrainHole.Diameter / 2,
new MCvScalar(Settings.PixelEditor.DrainHoleColor.B, Settings.PixelEditor.DrainHoleColor.G, Settings.PixelEditor.DrainHoleColor.R), -1);
RefreshLayerImage();
}
else
{
throw new NotImplementedException("Missing pixel operation");
}
}
public async void DrawModifications(bool exitEditor)
{
if (Drawings.Count == 0)
{
if (exitEditor && !ReferenceEquals(LastSelectedTabItem, TabPixelEditor))
{
SelectedTabItem = LastSelectedTabItem;
}
return;
}
ButtonResult result;
if (exitEditor)
{
result = await this.MessageBoxQuestion(
"There are edit operations that have not been applied. " +
"Would you like to apply all operations before closing the editor?",
"Closing image editor?", ButtonEnum.YesNoCancel);
}
else
{
result = await this.MessageBoxQuestion(
"Are you sure you want to apply all operations?",
"Apply image editor changes?");
// For the "apply" case, We aren't exiting the editor, so map "No" to "Cancel" here
// in order to prevent pixel history from being cleared.
result = result == ButtonResult.No ? ButtonResult.Cancel : ButtonResult.Yes;
}
if (result == ButtonResult.Cancel)
{
IsPixelEditorActive = true;
return;
}
if (result == ButtonResult.Yes)
{
IsGUIEnabled = false;
Clipboard.Snapshot();
var task = await Task.Factory.StartNew(async () =>
{
ShowProgressWindow("Drawing pixels");
try
{
SlicerFile.LayerManager.DrawModifications(Drawings, ProgressWindow.RestartProgress());
}
catch (OperationCanceledException)
{
}
catch (Exception ex)
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
await this.MessageBoxError(ex.ToString(), "Drawing operation failed!");
});
}
return false;
});
IsGUIEnabled = true;
Clipboard.Clip($"Draw {Drawings.Count} modifications");
if (Settings.PixelEditor.PartialUpdateIslandsOnEditing)
{
List<uint> whiteListLayers = new List<uint>();
foreach (var item in Drawings)
{
/*if (item.OperationType != PixelOperation.PixelOperationType.Drawing &&
item.OperationType != PixelOperation.PixelOperationType.Text &&
item.OperationType != PixelOperation.PixelOperationType.Eraser &&
item.OperationType != PixelOperation.PixelOperationType.Supports) continue;*/
if (!whiteListLayers.Contains(item.LayerIndex))
whiteListLayers.Add(item.LayerIndex);
uint nextLayer = item.LayerIndex + 1;
if (nextLayer < SlicerFile.LayerCount &&
!whiteListLayers.Contains(nextLayer))
{
whiteListLayers.Add(nextLayer);
}
}
await UpdateIslandsOverhangs(whiteListLayers);
}
}
Drawings.Clear();
ShowLayer();
if (exitEditor || (Settings.PixelEditor.CloseEditorOnApply && result == ButtonResult.Yes))
{
IsPixelEditorActive = false;
if (!ReferenceEquals(LastSelectedTabItem, TabPixelEditor))
{
SelectedTabItem = LastSelectedTabItem;
}
}
CanSave = true;
}
}
}