/* * GNU AFFERO GENERAL PUBLIC LICENSE * Version 3, 19 November 2007 * Copyright (C) 2007 Free Software Foundation, Inc. * Everyone is permitted to copy and distribute verbatim copies * of this license document, but changing it is not allowed. */ using Emgu.CV; using Emgu.CV.CvEnum; using Emgu.CV.Structure; using System; using System.Drawing; using System.Text; using System.Threading.Tasks; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Layers; namespace UVtools.Core.Operations; public sealed class OperationCalibrateXYZAccuracy : Operation { #region Members private decimal _layerHeight; private ushort _bottomLayers; private decimal _bottomExposure; private decimal _normalExposure; private ushort _topBottomMargin = 100; private ushort _leftRightMargin = 100; private decimal _displayWidth; private decimal _displayHeight; private decimal _xSize = 15; private decimal _ySize = 15; private decimal _zSize = 15; private bool _centerHoleRelief = true; private bool _hollowModel = true; private bool _mirrorOutput; private decimal _wallThickness = 3.0M; private decimal _observedXSize; private decimal _observedYSize; private decimal _observedZSize; private bool _outputTLObject; private bool _outputTCObject; private bool _outputTRObject; private bool _outputMLObject; private bool _outputMCObject = true; private bool _outputMRObject; private bool _outputBLObject; private bool _outputBCObject; private bool _outputBRObject; private decimal _drainHoleArea = 3; #endregion #region Overrides public override bool CanROI => false; public override bool CanCancel => false; public override LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.None; public override string IconClass => "fa-solid fa-cubes"; public override string Title => "XYZ Accuracy"; public override string Description => "Generates test models with various strategies and increments to verify the XYZ accuracy.\n" + "XYZ are accurate when the printed model match the expected size.\n" + "You must repeat this test when change any of the following: printer, LEDs, resin and exposure times.\n" + "Note: The current opened file will be overwritten with this test, use a dummy or a not needed file."; public override string ConfirmationText => $"generate the XYZ accuracy test?"; public override string ProgressTitle => $"Generating the XYZ accuracy test"; public override string ProgressAction => "Generated"; public override string? ValidateInternally() { var sb = new StringBuilder(); if (DisplayWidth <= 0) { sb.AppendLine("Display width must be a positive value."); } if (DisplayHeight <= 0) { sb.AppendLine("Display height must be a positive value."); } if (OutputObjects <= 0) { sb.AppendLine("No objects to output."); } return sb.ToString(); } public override string ToString() { var result = $"[Layer Height: {_layerHeight}] " + $"[Bottom layers: {_bottomLayers}] " + $"[Exposure: {_bottomExposure}/{_normalExposure}] " + $"[X: {_xSize} Y:{_ySize} Z:{_zSize}] " + $"[TB:{_topBottomMargin} LR:{_leftRightMargin}] " + $"[Model: {_outputTLObject.ToByte()}{_outputTCObject.ToByte()}{_outputTRObject.ToByte()}" + $"|{_outputMLObject.ToByte()}{_outputMCObject.ToByte()}{_outputMRObject.ToByte()}" + $"|{_outputBLObject.ToByte()}{_outputBCObject.ToByte()}{_outputBRObject.ToByte()}] " + $"[Hollow: {_hollowModel} @ {_wallThickness}mm] [Relief: {_centerHoleRelief}] [Mirror: {_mirrorOutput}]"; if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; return result; } #endregion #region Properties public decimal DisplayWidth { get => _displayWidth; set { if(!RaiseAndSetIfChanged(ref _displayWidth, FileFormat.RoundDisplaySize(value))) return; RaisePropertyChanged(nameof(Xppmm)); } } public decimal DisplayHeight { get => _displayHeight; set { if(!RaiseAndSetIfChanged(ref _displayHeight, FileFormat.RoundDisplaySize(value))) return; RaisePropertyChanged(nameof(Yppmm)); } } public decimal Xppmm => DisplayWidth > 0 ? Math.Round(SlicerFile.Resolution.Width / DisplayWidth, 2) : 0; public decimal Yppmm => DisplayHeight > 0 ? Math.Round(SlicerFile.Resolution.Height / DisplayHeight, 2) : 0; public decimal LayerHeight { get => _layerHeight; set { if(!RaiseAndSetIfChanged(ref _layerHeight, Layer.RoundHeight(value))) return; RaisePropertyChanged(nameof(BottomLayersMM)); RaisePropertyChanged(nameof(LayerCount)); RaisePropertyChanged(nameof(RealZSize)); RaisePropertyChanged(nameof(ObservedZSize)); } } public ushort Microns => (ushort)(LayerHeight * 1000); public ushort BottomLayers { get => _bottomLayers; set { if(!RaiseAndSetIfChanged(ref _bottomLayers, value)) return; RaisePropertyChanged(nameof(BottomLayersMM)); } } public decimal BottomLayersMM => Layer.RoundHeight(LayerHeight * BottomLayers); public decimal BottomExposure { get => _bottomExposure; set => RaiseAndSetIfChanged(ref _bottomExposure, Math.Round(value, 2)); } public decimal NormalExposure { get => _normalExposure; set => RaiseAndSetIfChanged(ref _normalExposure, Math.Round(value, 2)); } public ushort TopBottomMargin { get => _topBottomMargin; set => RaiseAndSetIfChanged(ref _topBottomMargin, value); } public ushort LeftRightMargin { get => _leftRightMargin; set => RaiseAndSetIfChanged(ref _leftRightMargin, value); } public decimal XSize { get => _xSize; set { if(!RaiseAndSetIfChanged(ref _xSize, Math.Round(value, 2))) return; RaisePropertyChanged(nameof(RealXSize)); RaisePropertyChanged(nameof(ObservedXSize)); } } public decimal YSize { get => _ySize; set { if(!RaiseAndSetIfChanged(ref _ySize, Math.Round(value, 2))) return; RaisePropertyChanged(nameof(RealYSize)); RaisePropertyChanged(nameof(ObservedYSize)); } } public decimal ZSize { get => _zSize; set { if(!RaiseAndSetIfChanged(ref _zSize, Math.Round(value, 2))) return; RaisePropertyChanged(nameof(LayerCount)); RaisePropertyChanged(nameof(RealZSize)); RaisePropertyChanged(nameof(ObservedZSize)); } } public decimal RealXSize { get { decimal pixels = _xSize * Xppmm; if (pixels <= 0) return 0; return Math.Round(_xSize - (pixels - Math.Truncate(pixels)) / Xppmm, 2); } } public decimal RealYSize { get { decimal pixels = _ySize * Yppmm; if (pixels <= 0) return 0; return Math.Round(_ySize - (pixels - Math.Truncate(pixels)) / Yppmm, 2); } } public decimal RealZSize => LayerCount * _layerHeight; public uint XPixels => (uint)(XSize * Xppmm); public uint YPixels => (uint)(YSize * Yppmm); public uint LayerCount => (uint)(ZSize / LayerHeight); public decimal DrainHoleArea { get => _drainHoleArea; set => RaiseAndSetIfChanged(ref _drainHoleArea, value); } public bool CenterHoleRelief { get => _centerHoleRelief; set => RaiseAndSetIfChanged(ref _centerHoleRelief, value); } public bool HollowModel { get => _hollowModel; set => RaiseAndSetIfChanged(ref _hollowModel, value); } public bool MirrorOutput { get => _mirrorOutput; set => RaiseAndSetIfChanged(ref _mirrorOutput, value); } public decimal WallThickness { get => _wallThickness; set { if(!RaiseAndSetIfChanged(ref _wallThickness, Math.Round(value, 2))) return; RaisePropertyChanged(nameof(WallThicknessRealXSize)); RaisePropertyChanged(nameof(WallThicknessRealYSize)); } } public decimal WallThicknessRealXSize { get { decimal pixels = _wallThickness * Xppmm; if (pixels <= 0) return 0; return Math.Round(_wallThickness - (pixels - Math.Truncate(pixels)) / Xppmm, 2); } } public decimal WallThicknessRealYSize { get { decimal pixels = _wallThickness * Yppmm; if (pixels <= 0) return 0; return Math.Round(_wallThickness - (pixels - Math.Truncate(pixels)) / Yppmm, 2); } } public uint WallThicknessXPixels => (uint)(WallThickness * Xppmm); public uint WallThicknessYPixels => (uint)(WallThickness * Yppmm); public bool OutputTLObject { get => _outputTLObject; set { if(!RaiseAndSetIfChanged(ref _outputTLObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public bool OutputTCObject { get => _outputTCObject; set { if (!RaiseAndSetIfChanged(ref _outputTCObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public bool OutputTRObject { get => _outputTRObject; set { if (!RaiseAndSetIfChanged(ref _outputTRObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public bool OutputMLObject { get => _outputMLObject; set { if (!RaiseAndSetIfChanged(ref _outputMLObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public bool OutputMCObject { get => _outputMCObject; set { if (!RaiseAndSetIfChanged(ref _outputMCObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public bool OutputMRObject { get => _outputMRObject; set { if (!RaiseAndSetIfChanged(ref _outputMRObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public bool OutputBLObject { get => _outputBLObject; set { if (!RaiseAndSetIfChanged(ref _outputBLObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public bool OutputBCObject { get => _outputBCObject; set { if (!RaiseAndSetIfChanged(ref _outputBCObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public bool OutputBRObject { get => _outputBRObject; set { if (!RaiseAndSetIfChanged(ref _outputBRObject, value)) return; RaisePropertyChanged(nameof(OutputObjects)); } } public byte OutputObjects => (byte) (_outputTLObject.ToByte() + _outputTCObject.ToByte() + _outputTRObject.ToByte() + _outputMLObject.ToByte() + _outputMCObject.ToByte() + _outputMRObject.ToByte() + _outputBLObject.ToByte() + _outputBCObject.ToByte() + _outputBRObject.ToByte()); public decimal ObservedXSize { get => _observedXSize; set { if(!RaiseAndSetIfChanged(ref _observedXSize, Math.Round(value, 2))) return; RaisePropertyChanged(nameof(ScaleXFactor)); } } public decimal ObservedYSize { get => _observedYSize; set { if(!RaiseAndSetIfChanged(ref _observedYSize, Math.Round(value, 2))) return; RaisePropertyChanged(nameof(ScaleYFactor)); } } public decimal ObservedZSize { get => _observedZSize; set { if(!RaiseAndSetIfChanged(ref _observedZSize, Math.Round(value, 2))) return; RaisePropertyChanged(nameof(ScaleZFactor)); } } // 15 - x // 14 - 100 public decimal ScaleXFactor => ObservedXSize > 0 && RealXSize > 0 ? Math.Round(RealXSize * 100 / ObservedXSize, 2) : 100; public decimal ScaleYFactor => ObservedYSize > 0 && RealYSize > 0 ? Math.Round(RealYSize * 100 / ObservedYSize, 2) : 100; public decimal ScaleZFactor => ObservedZSize > 0 && RealZSize > 0 ? Math.Round(RealZSize * 100 / ObservedZSize, 2) : 100; #endregion #region Constructor public OperationCalibrateXYZAccuracy() { } public OperationCalibrateXYZAccuracy(FileFormat slicerFile) : base(slicerFile) { } public override void InitWithSlicerFile() { base.InitWithSlicerFile(); if (_layerHeight <= 0) _layerHeight = (decimal)SlicerFile.LayerHeight; if (_bottomLayers <= 0) _bottomLayers = SlicerFile.BottomLayerCount; if (_bottomExposure <= 0) _bottomExposure = (decimal)SlicerFile.BottomExposureTime; if (_normalExposure <= 0) _normalExposure = (decimal)SlicerFile.ExposureTime; _mirrorOutput = SlicerFile.DisplayMirror != FlipDirection.None; if (SlicerFile.DisplayWidth > 0) DisplayWidth = (decimal)SlicerFile.DisplayWidth; if (SlicerFile.DisplayHeight > 0) DisplayHeight = (decimal)SlicerFile.DisplayHeight; } #endregion #region Enums #endregion #region Properties /*public override string ToString() { var result = $"[{_blurOperation}] [Size: {_size}]" + LayerRangeString; if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; return result; }*/ #endregion #region Equality private bool Equals(OperationCalibrateXYZAccuracy other) { return _layerHeight == other._layerHeight && _bottomLayers == other._bottomLayers && _bottomExposure == other._bottomExposure && _normalExposure == other._normalExposure && _topBottomMargin == other._topBottomMargin && _leftRightMargin == other._leftRightMargin && _xSize == other._xSize && _ySize == other._ySize && _zSize == other._zSize && _centerHoleRelief == other._centerHoleRelief && _hollowModel == other._hollowModel && _wallThickness == other._wallThickness && _observedXSize == other._observedXSize && _observedYSize == other._observedYSize && _observedZSize == other._observedZSize && _outputTLObject == other._outputTLObject && _outputTCObject == other._outputTCObject && _outputTRObject == other._outputTRObject && _outputMLObject == other._outputMLObject && _outputMCObject == other._outputMCObject && _outputMRObject == other._outputMRObject && _outputBLObject == other._outputBLObject && _outputBCObject == other._outputBCObject && _outputBRObject == other._outputBRObject && _mirrorOutput == other._mirrorOutput; } public override bool Equals(object? obj) { return ReferenceEquals(this, obj) || obj is OperationCalibrateXYZAccuracy other && Equals(other); } public override int GetHashCode() { var hashCode = new HashCode(); hashCode.Add(_layerHeight); hashCode.Add(_bottomLayers); hashCode.Add(_bottomExposure); hashCode.Add(_normalExposure); hashCode.Add(_topBottomMargin); hashCode.Add(_leftRightMargin); hashCode.Add(_xSize); hashCode.Add(_ySize); hashCode.Add(_zSize); hashCode.Add(_centerHoleRelief); hashCode.Add(_hollowModel); hashCode.Add(_mirrorOutput); hashCode.Add(_wallThickness); hashCode.Add(_observedXSize); hashCode.Add(_observedYSize); hashCode.Add(_observedZSize); hashCode.Add(_outputTLObject); hashCode.Add(_outputTCObject); hashCode.Add(_outputTRObject); hashCode.Add(_outputMLObject); hashCode.Add(_outputMCObject); hashCode.Add(_outputMRObject); hashCode.Add(_outputBLObject); hashCode.Add(_outputBCObject); hashCode.Add(_outputBRObject); return hashCode.ToHashCode(); } #endregion #region Methods public void SelectNoneObjects() { OutputTLObject = false; OutputTCObject = false; OutputTRObject = false; OutputMLObject = false; OutputMCObject = false; OutputMRObject = false; OutputBLObject = false; OutputBCObject = false; OutputBRObject = false; } public void SelectAllObjects() { OutputTLObject = true; OutputTCObject = true; OutputTRObject = true; OutputMLObject = true; OutputMCObject = true; OutputMRObject = true; OutputBLObject = true; OutputBCObject = true; OutputBRObject = true; } public void SelectCrossedObjects() { OutputTLObject = false; OutputTCObject = true; OutputTRObject = false; OutputMLObject = true; OutputMCObject = true; OutputMRObject = true; OutputBLObject = false; OutputBCObject = true; OutputBRObject = false; } public void SelectCenterObject() { OutputTLObject = false; OutputTCObject = false; OutputTRObject = false; OutputMLObject = false; OutputMCObject = true; OutputMRObject = false; OutputBLObject = false; OutputBCObject = false; OutputBRObject = false; } public void Sanitize() { for (ushort i = 0; i < 10000 && (_xSize * Xppmm).DecimalDigits() >= 1; i++) { XSize += 0.01M; } for (ushort i = 0; i < 10000 && (_ySize * Yppmm).DecimalDigits() >= 1; i++) { YSize += 0.01M; } for (ushort i = 0; i < 10000 && (_zSize / LayerHeight).DecimalDigits() >= 1; i++) { ZSize += 0.01M; } } public Mat[] GetLayers() { var layers = new Mat[3]; for (byte i = 0; i < layers.Length; i++) { layers[i] = EmguExtensions.InitMat(SlicerFile.Resolution); } int currentX = 0; int currentY = 0; string positionYStr = string.Empty; string positionStr = string.Empty; const FontFace fontFace = FontFace.HersheyDuplex; const byte fontStartX = 30; const byte fontStartY = 50; const double fontScale = 1.3; const byte fontThickness = 3; var xPixels = XPixels; var yPixels = YPixels; for (int y = 0; y < 3; y++) { switch (y) { case 0: currentY = _topBottomMargin; positionYStr = "T"; break; case 1: currentY = (int)(SlicerFile.Resolution.Height / 2 - yPixels / 2); positionYStr = "M"; break; case 2: currentY = (int)(SlicerFile.Resolution.Height - yPixels - _topBottomMargin); positionYStr = "B"; break; } for (int x = 0; x < 3; x++) { switch (x) { case 0: currentX = _leftRightMargin; positionStr = $"{positionYStr}L"; break; case 1: currentX = (int)(SlicerFile.Resolution.Width / 2 - xPixels / 2); positionStr = $"{positionYStr}C"; break; case 2: currentX = (int)(SlicerFile.Resolution.Width - xPixels - _leftRightMargin); positionStr = $"{positionYStr}R"; break; } for (var i = 0; i < layers.Length; i++) { if(y == 0 && x == 0 && !_outputTLObject) continue; if(y == 0 && x == 1 && !_outputTCObject) continue; if(y == 0 && x == 2 && !_outputTRObject) continue; if(y == 1 && x == 0 && !_outputMLObject) continue; if(y == 1 && x == 1 && !_outputMCObject) continue; if(y == 1 && x == 2 && !_outputMRObject) continue; if(y == 2 && x == 0 && !_outputBLObject) continue; if(y == 2 && x == 1 && !_outputBCObject) continue; if(y == 2 && x == 2 && !_outputBRObject) continue; var layer = layers[i]; CvInvoke.Rectangle(layer, new Rectangle(currentX, currentY, (int)xPixels, (int) yPixels), EmguExtensions.WhiteColor, -1); CvInvoke.PutText(layer, positionStr, new Point(currentX + fontStartX, currentY + fontStartY), fontFace, fontScale, EmguExtensions.BlackColor, fontThickness); CvInvoke.PutText(layer, $"{XSize},{YSize},{ZSize}", new Point(currentX + fontStartX, (int) (currentY + yPixels - fontStartY + 25)), fontFace, fontScale, EmguExtensions.BlackColor, fontThickness); if (CenterHoleRelief) { CvInvoke.Circle(layer, new Point((int) (currentX + xPixels / 2), (int) (currentY + yPixels / 2)), (int) (Math.Min(xPixels, yPixels) / 4), EmguExtensions.BlackColor, -1); } if (_hollowModel && i > 0 && _wallThickness > 0) { Size rectSize = new((int) (xPixels - WallThicknessXPixels * 2), (int) (yPixels - WallThicknessYPixels * 2)); Point rectLocation = new((int) (currentX + WallThicknessXPixels), (int) (currentY + WallThicknessYPixels)); CvInvoke.Rectangle(layers[i], new Rectangle(rectLocation, rectSize), EmguExtensions.BlackColor, -1); } if (i == 2 && _drainHoleArea > 0) { Size rectSize = new((int)xPixels, (int)(Yppmm * _drainHoleArea)); Point rectLocation = new(currentX, (int)(currentY + xPixels / 2 - rectSize.Height / 2)); CvInvoke.Rectangle(layers[i], new Rectangle(rectLocation, rectSize), EmguExtensions.BlackColor, -1); } } } } if (_mirrorOutput) { var flip = SlicerFile.DisplayMirror; if (flip == FlipDirection.None) flip = FlipDirection.Horizontally; Parallel.ForEach(layers, CoreSettings.ParallelOptions, mat => CvInvoke.Flip(mat, mat, (FlipType)flip)); } return layers; } public Mat GetThumbnail() { Mat thumbnail = EmguExtensions.InitMat(new Size(400, 200), 3); var fontFace = FontFace.HersheyDuplex; var fontScale = 1; var fontThickness = 2; const byte xSpacing = 45; const byte ySpacing = 45; CvInvoke.PutText(thumbnail, "UVtools", new Point(140, 35), fontFace, fontScale, new MCvScalar(255, 27, 245), fontThickness + 1); CvInvoke.Line(thumbnail, new Point(xSpacing, 0), new Point(xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3); CvInvoke.Line(thumbnail, new Point(xSpacing, ySpacing + 5), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3); CvInvoke.Line(thumbnail, new Point(thumbnail.Width - xSpacing, 0), new Point(thumbnail.Width - xSpacing, ySpacing + 5), new MCvScalar(255, 27, 245), 3); CvInvoke.PutText(thumbnail, "XYZ Accuracy Cal.", new Point(xSpacing, ySpacing * 2), fontFace, fontScale, new MCvScalar(0, 255, 255), fontThickness); CvInvoke.PutText(thumbnail, $"{Microns}um @ {BottomExposure}s/{NormalExposure}s", new Point(xSpacing, ySpacing * 3), fontFace, fontScale, EmguExtensions.WhiteColor, fontThickness); CvInvoke.PutText(thumbnail, $"{XSize} x {YSize} x {ZSize} mm", new Point(xSpacing, ySpacing * 4), fontFace, fontScale, EmguExtensions.WhiteColor, fontThickness); /*thumbnail.SetTo(EmguExtensions.Black3Byte); CvInvoke.Circle(thumbnail, new Point(400/2, 200/2), 200/2, EmguExtensions.White3Byte, -1); for (int angle = 0; angle < 360; angle+=20) { CvInvoke.Line(thumbnail, new Point(400 / 2, 200 / 2), new Point((int)(400 / 2 + 100 * Math.Cos(angle * Math.PI / 180)), (int)(200 / 2 + 100 * Math.Sin(angle * Math.PI / 180))), new MCvScalar(255, 27, 245), 3); } thumbnail.Save("D:\\Thumbnail.png");*/ return thumbnail; } protected override bool ExecuteInternally(OperationProgress progress) { progress.ItemCount = LayerCount; var newLayers = new Layer[LayerCount]; var layers = GetLayers(); var bottomLayer = new Layer(0, layers[0], SlicerFile) { IsModified = true }; var layer = new Layer(0, layers[1], SlicerFile) { IsModified = true }; var ventLayer = new Layer(0, layers[2], SlicerFile) { IsModified = true }; for (uint layerIndex = 0; layerIndex < LayerCount; layerIndex++) { newLayers[layerIndex] = SlicerFile.GetBottomOrNormalValue(layerIndex, bottomLayer.Clone(), (_hollowModel || _centerHoleRelief) && _drainHoleArea > 0 && layerIndex <= _bottomLayers + (int)(_drainHoleArea / _layerHeight) ? ventLayer.Clone() : layer.Clone()); progress++; } foreach (var mat in layers) { mat.Dispose(); } if (SlicerFile.ThumbnailsCount > 0) SlicerFile.SetThumbnails(GetThumbnail()); SlicerFile.SuppressRebuildPropertiesWork(() => { SlicerFile.LayerHeight = (float)LayerHeight; SlicerFile.BottomExposureTime = (float)BottomExposure; SlicerFile.ExposureTime = (float)NormalExposure; SlicerFile.BottomLayerCount = BottomLayers; SlicerFile.TransitionLayerCount = 0; SlicerFile.Layers = newLayers; }, true); return !progress.Token.IsCancellationRequested; } #endregion }