/* * 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 K4os.Compression.LZ4; using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.IO; using System.IO.Compression; using System.Linq; using System.Text.Json.Serialization; using System.Xml.Serialization; using UVtools.Core.EmguCV; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Objects; using UVtools.Core.Operations; namespace UVtools.Core.Layers; #region Enums public enum LayerCompressionCodec : byte { [Description("PNG: Compression=High Speed=Slow (Use with low RAM)")] Png, [Description("GZip: Compression=Medium Speed=Medium (Optimal)")] GZip, [Description("Deflate: Compression=Medium Speed=Medium (Optimal)")] Deflate, [Description("LZ4: Compression=Low Speed=Fast (Use with high RAM)")] Lz4, //[Description("None: Compression=None Speed=Fastest (Your soul belongs to RAM)")] //None } #endregion /// /// Represent a Layer /// public class Layer : BindableBase, IEquatable, IEquatable { #region Constants public const byte HeightPrecision = 3; public const decimal HeightPrecisionIncrement = 0.001M; public const decimal MinimumHeight = 0.01M; public const decimal MaximumHeight = 0.2M; #endregion #region Members public object Mutex = new(); private LayerCompressionCodec _compressionCodec; private byte[]? _compressedBytes; private uint _nonZeroPixelCount; private Rectangle _boundingRectangle = Rectangle.Empty; private uint _firstPixelIndex; private uint _lastPixelIndex; private Point _firstPixelPosition; private Point _lastPixelPosition; private bool _isModified; private uint _index; private uint _resolutionX; private uint _resolutionY; private float _positionZ; private float _lightOffDelay; private float _waitTimeBeforeCure; private float _exposureTime; private float _waitTimeAfterCure; private float _liftHeight = FileFormat.DefaultLiftHeight; private float _liftSpeed = FileFormat.DefaultLiftSpeed; private float _liftHeight2 = FileFormat.DefaultLiftHeight2; private float _liftSpeed2 = FileFormat.DefaultLiftSpeed2; private float _waitTimeAfterLift; private float _retractSpeed = FileFormat.DefaultRetractSpeed; private float _retractHeight2 = FileFormat.DefaultRetractHeight2; private float _retractSpeed2 = FileFormat.DefaultRetractSpeed2; private byte _lightPWM = FileFormat.DefaultLightPWM; private float _materialMilliliters; private EmguContours? _contours; #endregion #region Properties /// /// Gets or sets the parent SlicerFile /// public FileFormat SlicerFile { get; set; } /// /// Image resolution X /// public uint ResolutionX { get => _resolutionX; set => RaiseAndSetIfChanged(ref _resolutionX, value); } /// /// Image resolution Y /// public uint ResolutionY { get => _resolutionY; set => RaiseAndSetIfChanged(ref _resolutionY, value); } /// /// Image resolution /// public Size Resolution { get => new((int)ResolutionX, (int)ResolutionY); set { ResolutionX = (uint)value.Width; ResolutionY = (uint)value.Height; RaisePropertyChanged(); } } /// /// Gets the number of non zero pixels on this layer image /// public uint NonZeroPixelCount { get => _nonZeroPixelCount; internal set { if (!RaiseAndSetIfChanged(ref _nonZeroPixelCount, value)) return; RaisePropertyChanged(nameof(NonZeroPixelRatio)); RaisePropertyChanged(nameof(NonZeroPixelPercentage)); RaisePropertyChanged(nameof(Area)); RaisePropertyChanged(nameof(Volume)); MaterialMilliliters = -1; // Recalculate } } /// /// Gets the ratio between non zero pixels and display number of pixels /// public double NonZeroPixelRatio { get { var displayPixelCount = SlicerFile.DisplayPixelCount; if (displayPixelCount == 0) return double.NaN; return (double)_nonZeroPixelCount / displayPixelCount; } } /// /// Gets the percentage of non zero pixels relative to the display number of pixels /// public double NonZeroPixelPercentage { get { var pixelRatio = NonZeroPixelRatio; if (double.IsNaN(pixelRatio)) return double.NaN; return pixelRatio * 100.0; } } /// /// Gets if this layer is empty/all black pixels /// public bool IsEmpty => _nonZeroPixelCount == 0; /// /// Gets if this layer is a dummy layer to bypass a firmware constrain, that is contain at most one pixel and exposure time no more than 0.01s /// public bool IsDummy => _nonZeroPixelCount <= 1 || _exposureTime <= 0.01; /// /// Gets the layer area (XY) in mm^2 /// Pixel size * number of pixels /// public float Area => GetArea(3); /// /// Gets the layer volume (XYZ) in mm^3 /// Pixel size * number of pixels * layer height /// public float Volume => GetVolume(3); /// /// Gets the bounding rectangle for the image area /// public Rectangle BoundingRectangle { get => _boundingRectangle; internal set { RaiseAndSetIfChanged(ref _boundingRectangle, value); RaisePropertyChanged(nameof(BoundingRectangleMillimeters)); } } /// /// Gets the bounding rectangle for the image area in millimeters /// public RectangleF BoundingRectangleMillimeters { get { var pixelSize = SlicerFile.PixelSize; return new RectangleF( (float) Math.Round(_boundingRectangle.X * pixelSize.Width, 2), (float)Math.Round(_boundingRectangle.Y * pixelSize.Height, 2), (float)Math.Round(_boundingRectangle.Width * pixelSize.Width, 2), (float)Math.Round(_boundingRectangle.Height * pixelSize.Height, 2)); } } /// /// Gets the first pixel index on the /// public uint BoundingRectangleFirstPixelIndex => (uint)(BoundingRectangle.Y * ResolutionX + BoundingRectangle.X); /// /// Gets the last pixel index on the /// public uint BoundingRectangleLastPixelIndex => (uint)(BoundingRectangle.Bottom * ResolutionX + BoundingRectangle.Right); /// /// Gets the first pixel on the /// public Point BoundingRectangleFirstPixelPosition => BoundingRectangle.Location; /// /// Gets the last pixel on the /// public Point BoundingRectangleLastPixelPosition => new (BoundingRectangle.Right, BoundingRectangle.Bottom); /// /// Gets the first pixel index on this layer /// public uint FirstPixelIndex { get => _firstPixelIndex; private set => RaiseAndSetIfChanged(ref _firstPixelIndex, value); } /// /// Gets the last pixel index on this layer /// public uint LastPixelIndex { get => _lastPixelIndex; private set => RaiseAndSetIfChanged(ref _lastPixelIndex, value); } /// /// Gets the first pixel on this layer /// public Point FirstPixelPosition { get => _firstPixelPosition; private set => RaiseAndSetIfChanged(ref _firstPixelPosition, value); } /// /// Gets the last pixel on this layer /// public Point LastPixelPosition { get => _lastPixelPosition; private set => RaiseAndSetIfChanged(ref _lastPixelPosition, value); } /// /// Gets if is the first layer /// public bool IsFirstLayer => _index == 0; /// /// Gets if layer is between first and last layer, aka, not first nor last layer /// public bool IsIntermediateLayer => !IsFirstLayer && !IsLastLayer; /// /// Gets if is the last layer /// public bool IsLastLayer => _index >= SlicerFile.LastLayerIndex; /// /// Gets if is in the bottom layer group /// public bool IsBottomLayer => _index < SlicerFile.BottomLayerCount; /// /// Gets if is in the bottom layer group by count and height /// public bool IsBottomLayerByHeight { get { var bottomLayers = SlicerFile.BottomLayerCount; return _index < bottomLayers || PositionZ / SlicerFile.LayerHeight <= bottomLayers; } } /// /// Gets if is in the normal layer group /// public bool IsNormalLayer => !IsBottomLayer; /// /// Gets if this layer is also an transition layer /// public bool IsTransitionLayer => SlicerFile.TransitionLayerCount > 0 && Index >= SlicerFile.BottomLayerCount && Index < SlicerFile.BottomLayerCount + SlicerFile.TransitionLayerCount; /// /// Gets the previous layer, returns null if no previous layer /// public Layer? PreviousLayer { get { if (IsFirstLayer || _index > SlicerFile.Count) return null; return SlicerFile[_index - 1]; } } /// /// Gets the previous layer if available, otherwise return the calling layer itself /// public Layer PreviousLayerOrThis { get { if (IsFirstLayer || _index > SlicerFile.Count) return this; return SlicerFile[_index - 1]; } } /// /// Gets the previous layer with a different height from the current, returns null if no previous layer /// public Layer? PreviousHeightLayer { get { if (IsFirstLayer || _index > SlicerFile.Count) return null; for (int i = (int)_index - 1; i >= 0; i--) { if (SlicerFile[i].PositionZ < _positionZ) return SlicerFile[i]; } return null; } } /// /// Gets the previous layer matching at least pixels, returns null if no previous layer /// public Layer? GetPreviousLayerWithAtLeastPixelCountOf(uint numberOfPixels) { if (IsFirstLayer || _index > SlicerFile.Count) return null; for (int i = (int)_index - 1; i >= 0; i--) { if (SlicerFile[i].NonZeroPixelCount >= numberOfPixels) return SlicerFile[i]; } return null; } /// /// Gets the next layer, returns null if no next layer /// public Layer? NextLayer { get { if (_index >= SlicerFile.LastLayerIndex) return null; return SlicerFile[_index + 1]; } } /// /// Gets the next layer if available, otherwise return the calling layer itself /// public Layer NextLayerOrThis { get { if (_index >= SlicerFile.LastLayerIndex) return this; return SlicerFile[_index + 1]; } } /// /// Gets the next layer with a different height from the current, returns null if no next layer /// public Layer? NextHeightLayer { get { if (_index >= SlicerFile.LastLayerIndex) return null; for (var i = _index + 1; i < SlicerFile.LayerCount; i++) { if (SlicerFile[i].PositionZ > _positionZ) return SlicerFile[i]; } return null; } } /// /// Gets the next layer matching at least pixels, returns null if no next layer /// public Layer? GetNextLayerWithAtLeastPixelCountOf(uint numberOfPixels) { if (_index >= SlicerFile.LastLayerIndex) return null; for (var i = _index + 1; i < SlicerFile.LayerCount; i++) { if (SlicerFile[i].NonZeroPixelCount >= numberOfPixels) return SlicerFile[i]; } return null; } /// /// Gets the layer index /// public uint Index { get => _index; set { if(!RaiseAndSetIfChanged(ref _index, value)) return; RaisePropertyChanged(nameof(Number)); } } /// /// Gets the layer number, 1 started /// public uint Number => _index + 1; /// /// Gets or sets the absolute layer position on Z in mm /// public float PositionZ { get => _positionZ; set { //if (value < 0) throw new ArgumentOutOfRangeException(nameof(PositionZ), "Value can't be negative"); if (!RaiseAndSetIfChanged(ref _positionZ, RoundHeight(value))) return; RaisePropertyChanged(nameof(RelativePositionZ)); RaisePropertyChanged(nameof(LayerHeight)); //MaterialMilliliters = -1; // Recalculate } } /// /// Gets the relative layer position on Z in mm (Relative to the previous layer) /// public float RelativePositionZ { get { var previousLayer = PreviousLayer; return previousLayer is null ? _positionZ : RoundHeight(_positionZ - previousLayer.PositionZ); } set => PositionZ = _positionZ - RelativePositionZ + value; } /// /// Gets or sets the wait time in seconds before cure the layer /// AKA: Light-off delay /// Chitubox: Rest time after retract /// Lychee: Wait before print /// public float WaitTimeBeforeCure { get => _waitTimeBeforeCure; set { value = (float)Math.Round(value, 2); if (value < 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomWaitTimeBeforeCure, SlicerFile.WaitTimeBeforeCure); if (!RaiseAndSetIfChanged(ref _waitTimeBeforeCure, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets or sets the exposure time in seconds /// public float ExposureTime { get => _exposureTime; set { value = (float)Math.Round(value, 2); if (value < 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomExposureTime, SlicerFile.ExposureTime); if(!RaiseAndSetIfChanged(ref _exposureTime, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets or sets the wait time in seconds after cure the layer /// Chitubox: Rest time before lift /// Lychee: Wait after print /// public float WaitTimeAfterCure { get => _waitTimeAfterCure; set { value = (float)Math.Round(value, 2); if (value < 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomWaitTimeAfterCure, SlicerFile.WaitTimeAfterCure); if (!RaiseAndSetIfChanged(ref _waitTimeAfterCure, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets or sets the layer off time in seconds /// public float LightOffDelay { get => _lightOffDelay; set { value = (float)Math.Round(value, 2); if (value < 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLightOffDelay, SlicerFile.LightOffDelay); if(!RaiseAndSetIfChanged(ref _lightOffDelay, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets: Total lift height (lift1 + lift2) /// Sets: Lift1 with value and lift2 with 0 /// public float LiftHeightTotal { get => (float)Math.Round(_liftHeight + _liftHeight2, 2); set { LiftHeight = (float)Math.Round(value, 2); LiftHeight2 = 0; } } /// /// Gets or sets the lift height in mm /// public float LiftHeight { get => _liftHeight; set { value = (float)Math.Round(value, 2); if (value < 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLiftHeight, SlicerFile.LiftHeight); if(!RaiseAndSetIfChanged(ref _liftHeight, value)) return; RaisePropertyChanged(nameof(LiftHeightTotal)); RetractHeight2 = _retractHeight2; // Sanitize SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets or sets the speed in mm/min /// public float LiftSpeed { get => _liftSpeed; set { value = (float)Math.Round(value, 2); if (value <= 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLiftSpeed, SlicerFile.LiftSpeed); if(!RaiseAndSetIfChanged(ref _liftSpeed, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets or sets the lift height in mm /// public float LiftHeight2 { get => _liftHeight2; set { value = (float)Math.Round(value, 2); if (value < 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLiftHeight2, SlicerFile.LiftHeight2); if (!RaiseAndSetIfChanged(ref _liftHeight2, value)) return; RaisePropertyChanged(nameof(LiftHeightTotal)); RetractHeight2 = _retractHeight2; // Sanitize SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets or sets the speed in mm/min /// public float LiftSpeed2 { get => _liftSpeed2; set { value = (float)Math.Round(value, 2); if (value <= 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLiftSpeed2, SlicerFile.LiftSpeed2); if (!RaiseAndSetIfChanged(ref _liftSpeed2, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } public float WaitTimeAfterLift { get => _waitTimeAfterLift; set { value = (float)Math.Round(value, 2); if (value < 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomWaitTimeAfterLift, SlicerFile.WaitTimeAfterLift); if (!RaiseAndSetIfChanged(ref _waitTimeAfterLift, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets: Total retract height (retract1 + retract2) alias of /// public float RetractHeightTotal => LiftHeightTotal; /// /// Gets the retract height in mm /// public float RetractHeight => (float)Math.Round(LiftHeightTotal - _retractHeight2, 2); /// /// Gets the speed in mm/min for the retracts /// public float RetractSpeed { get => _retractSpeed; set { value = (float)Math.Round(value, 2); if (value <= 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomRetractSpeed, SlicerFile.RetractSpeed); if (!RaiseAndSetIfChanged(ref _retractSpeed, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets or sets the second retract height in mm /// public virtual float RetractHeight2 { get => _retractHeight2; set { value = Math.Clamp((float)Math.Round(value, 2), 0, RetractHeightTotal); RaiseAndSetIfChanged(ref _retractHeight2, value); RaisePropertyChanged(nameof(RetractHeight)); RaisePropertyChanged(nameof(RetractHeightTotal)); SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets the speed in mm/min for the retracts /// public virtual float RetractSpeed2 { get => _retractSpeed2; set { value = (float)Math.Round(value, 2); if (value <= 0) value = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomRetractSpeed2, SlicerFile.RetractSpeed2); if (!RaiseAndSetIfChanged(ref _retractSpeed2, value)) return; SlicerFile.UpdatePrintTimeQueued(); } } /// /// Gets or sets the pwm value from 0 to 255 /// public byte LightPWM { get => _lightPWM; set { //if (value == 0) value = SlicerFile.GetInitialLayerValueOrNormal(Index, SlicerFile.BottomLightPWM, SlicerFile.LightPWM); //if (value == 0) value = FileFormat.DefaultLightPWM; RaiseAndSetIfChanged(ref _lightPWM, value); } } /// /// Gets the minimum used speed in mm/min /// public float MinimumSpeed { get { float speed = float.MaxValue; if (LiftSpeed > 0) speed = Math.Min(speed, LiftSpeed); if (LiftSpeed2 > 0) speed = Math.Min(speed, LiftSpeed2); if (RetractSpeed > 0) speed = Math.Min(speed, RetractSpeed); if (RetractSpeed2 > 0) speed = Math.Min(speed, RetractSpeed2); if (Math.Abs(speed - float.MaxValue) < 0.01) return 0; return speed; } } /// /// Gets the maximum used speed in mm/min /// public float MaximumSpeed { get { float speed = LiftSpeed; speed = Math.Max(speed, LiftSpeed2); speed = Math.Max(speed, RetractSpeed); speed = Math.Max(speed, RetractSpeed2); return speed; } } /// /// Gets if this layer can be exposed to UV light /// public bool CanExpose => _exposureTime > 0 && _lightPWM > 0; /// /// Gets if this layer should be exposed to UV light, ie: if layer is empty or no exposure time then it useless to expose it /// public bool ShouldExpose => !IsEmpty && CanExpose; /// /// Gets the layer height in millimeters of this layer /// public float LayerHeight { get { if (IsFirstLayer) return _positionZ; var previousLayer = this; while ((previousLayer = previousLayer!.PreviousLayer) is not null) // This cycle returns the correct layer height if two or more layers have the same position z { var layerHeight = RoundHeight(_positionZ - previousLayer.PositionZ); //Debug.WriteLine($"Layer {_index}-{previousLayer.Index}: {_positionZ} - {previousLayer.PositionZ}: {layerHeight}"); if (layerHeight == 0f) continue; if (layerHeight < 0f) break; return layerHeight; } return SlicerFile.LayerHeight; } } /// /// Gets the computed material milliliters spent on this layer /// public float MaterialMilliliters { get => _materialMilliliters; set { if (SlicerFile is null) return; //var globalMilliliters = SlicerFile.MaterialMilliliters - _materialMilliliters; if (value < 0) { value = (float) Math.Round(GetVolume() / 1000f, 4); } if(!RaiseAndSetIfChanged(ref _materialMilliliters, value)) return; RaisePropertyChanged(nameof(MaterialMillilitersPercent)); SlicerFile.MaterialMilliliters = -1; // Recalculate global //ParentLayerManager.MaterialMillilitersTimer.Stop(); //if(!ParentLayerManager.MaterialMillilitersTimer.Enabled) // ParentLayerManager.MaterialMillilitersTimer.Start(); } } /// /// Gets the computed material milliliters percentage compared to the rest of the model /// public float MaterialMillilitersPercent => SlicerFile.MaterialMilliliters > 0 ? _materialMilliliters * 100 / SlicerFile.MaterialMilliliters : float.NaN; /// /// Gets or sets the compression method used to cache the image /// public LayerCompressionCodec CompressionCodec { get => _compressionCodec; set { if (!HaveImage) { RaiseAndSetIfChanged(ref _compressionCodec, value); return; } // Handle conversion if (_compressionCodec == value) return; using var mat = LayerMat; _compressionCodec = value; _compressedBytes = CompressMat(mat, value); RaisePropertyChanged(); } } /// /// Gets or sets layer image compressed data /// public byte[]? CompressedBytes { get => _compressedBytes; set { _compressedBytes = value; IsModified = true; SlicerFile.BoundingRectangle = Rectangle.Empty; _contours?.Dispose(); _contours = null; RaisePropertyChanged(); RaisePropertyChanged(nameof(HaveImage)); } } public byte[]? CompressedPngBytes { get { if (_compressedBytes is null) return null; if (_compressionCodec == LayerCompressionCodec.Png) return _compressedBytes; using var mat = LayerMat; return mat.GetPngByes(); } } /// /// True if this layer have an valid initialized image, otherwise false /// public bool HaveImage => _compressedBytes is not null && _compressedBytes.Length > 0; /// /// Gets or sets a new image instance /// [XmlIgnore] [JsonInclude] public Mat LayerMat { get { if (!HaveImage) return null!; Mat mat; switch (_compressionCodec) { case LayerCompressionCodec.Png: mat = new Mat(); CvInvoke.Imdecode(_compressedBytes, ImreadModes.Grayscale, mat); break; case LayerCompressionCodec.Lz4: mat = new Mat(Resolution, DepthType.Cv8U, 1); LZ4Codec.Decode(_compressedBytes.AsSpan(), mat.GetDataByteSpan()); break; case LayerCompressionCodec.GZip: { mat = new Mat(Resolution, DepthType.Cv8U, 1); unsafe { fixed (byte* pBuffer = _compressedBytes) { using var compressedStream = new UnmanagedMemoryStream(pBuffer, _compressedBytes!.Length); using var matStream = mat.GetUnmanagedMemoryStream(FileAccess.Write); using var gZipStream = new GZipStream(compressedStream, CompressionMode.Decompress); gZipStream.CopyTo(matStream); } } break; } case LayerCompressionCodec.Deflate: { mat = new Mat(Resolution, DepthType.Cv8U, 1); unsafe { fixed (byte* pBuffer = _compressedBytes) { using var compressedStream = new UnmanagedMemoryStream(pBuffer, _compressedBytes!.Length); using var matStream = mat.GetUnmanagedMemoryStream(FileAccess.Write); using var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress); deflateStream.CopyTo(matStream); } } break; } /*case LayerCompressionMethod.None: mat = new Mat(Resolution, DepthType.Cv8U, 1); //mat.SetBytes(_compressedBytes!); _compressedBytes.CopyTo(mat.GetDataByteSpan()); break;*/ default: throw new ArgumentOutOfRangeException(nameof(LayerMat)); } return mat; } set { if (value is not null) { CompressedBytes = CompressMat(value, _compressionCodec); _resolutionX = (uint)value.Width; _resolutionY = (uint)value.Height; } else { _resolutionX = 0; _resolutionY = 0; } GetBoundingRectangle(value, true); RaisePropertyChanged(); } } /// /// Gets the layer mat with roi of it bounding rectangle /// public MatRoi LayerMatBoundingRectangle => new(LayerMat, BoundingRectangle); /// /// Gets the layer mat with roi of model bounding rectangle /// public MatRoi LayerMatModelBoundingRectangle => new(LayerMat, SlicerFile.BoundingRectangle); /// /// Gets the layer mat with a specified roi /// /// Region of interest /// public MatRoi GetLayerMat(Rectangle roi) => new(LayerMat, roi); /// /// Gets the layer mat with bounding rectangle mat /// /// Margin from bounding rectangle /// public MatRoi GetLayerMatBoundingRectangle(int margin) => new(LayerMat, GetBoundingRectangle(margin)); /// /// Gets the layer mat with bounding rectangle mat /// /// X margin from bounding rectangle /// Y margin from bounding rectangle /// public MatRoi GetLayerMatBoundingRectangle(int marginX, int marginY) => new(LayerMat, GetBoundingRectangle(marginX, marginY)); /// /// Gets the layer mat with bounding rectangle mat /// /// Margin from bounding rectangle /// public MatRoi GetLayerMatBoundingRectangle(Size margin) => new(LayerMat, GetBoundingRectangle(margin)); /// /// Gets a new Brg image instance /// [XmlIgnore] [JsonInclude] public Mat BrgMat { get { var mat = LayerMat; if (mat is null) return null!; CvInvoke.CvtColor(mat, mat, ColorConversion.Gray2Bgr); return mat; } } /// /// Gets a computed layer filename, padding zeros are equal to layer count digits /// public string Filename => FormatFileNameWithLayerDigits("layer"); /// /// Gets if layer image has been modified /// public bool IsModified { get => _isModified; set => RaiseAndSetIfChanged(ref _isModified, value); } /// /// Gets if this layer have same value parameters as global settings /// public bool IsUsingGlobalParameters { get { if (SlicerFile is null) return false; // Cant verify if (IsBottomLayer) { if ( _positionZ != RoundHeight(SlicerFile.LayerHeight * Number) || _lightOffDelay != SlicerFile.BottomLightOffDelay || _waitTimeBeforeCure != SlicerFile.BottomWaitTimeBeforeCure || _exposureTime != SlicerFile.BottomExposureTime || _waitTimeAfterCure != SlicerFile.BottomWaitTimeAfterCure || _liftHeight != SlicerFile.BottomLiftHeight || _liftSpeed != SlicerFile.BottomLiftSpeed || _liftHeight2 != SlicerFile.BottomLiftHeight2 || _liftSpeed2 != SlicerFile.BottomLiftSpeed2 || _waitTimeAfterLift != SlicerFile.BottomWaitTimeAfterLift || _retractSpeed != SlicerFile.BottomRetractSpeed || _retractHeight2 != SlicerFile.BottomRetractHeight2 || _retractSpeed2 != SlicerFile.BottomRetractSpeed2 || _lightPWM != SlicerFile.BottomLightPWM ) return false; } else { if ( _positionZ != RoundHeight(SlicerFile.LayerHeight * Number) || _lightOffDelay != SlicerFile.LightOffDelay || _waitTimeBeforeCure != SlicerFile.WaitTimeBeforeCure || (!IsTransitionLayer && _exposureTime != SlicerFile.ExposureTime) || // Fix for can't edit settings on menu https://github.com/sn4k3/UVtools/issues/507 //_exposureTime != SlicerFile.ExposureTime || _waitTimeAfterCure != SlicerFile.WaitTimeAfterCure || _liftHeight != SlicerFile.LiftHeight || _liftSpeed != SlicerFile.LiftSpeed || _liftHeight2 != SlicerFile.LiftHeight2 || _liftSpeed2 != SlicerFile.LiftSpeed2 || _waitTimeAfterLift != SlicerFile.WaitTimeAfterLift || _retractSpeed != SlicerFile.RetractSpeed || _retractHeight2 != SlicerFile.RetractHeight2 || _retractSpeed2 != SlicerFile.RetractSpeed2 || _lightPWM != SlicerFile.LightPWM ) return false; } return true; } } /// /// True if this layer is using TSMC values, otherwise false /// public bool IsUsingTSMC => LiftHeight2 > 0 || RetractHeight2 > 0; /// /// Gets tree contours cache for this layer, however should call /// GetContours /// /// instead with instance. /// If not set it will calculate contours first /// public EmguContours Contours { get { if (_contours is not null) return _contours; using var matRoi = LayerMatBoundingRectangle; return GetContours(matRoi); } } /// /// Gets tree contours cache for this layer. If not set it will calculate contours first /// public EmguContours GetContours(IInputOutputArray mat, Point offset = default) { return _contours ??= new EmguContours(mat, RetrType.Tree, ChainApproxMethod.ChainApproxSimple, offset); } /// /// Gets tree contours cache for this layer. If not set it will calculate contours first /// public EmguContours GetContours(MatRoi matRoi) { return _contours ??= new EmguContours(matRoi.RoiMat, RetrType.Tree, ChainApproxMethod.ChainApproxSimple, matRoi.Roi.Location); } #endregion #region Constructor public Layer(uint index, FileFormat slicerFile, LayerCompressionCodec? compressionMethod = null) { compressionMethod ??= CoreSettings.DefaultLayerCompressionCodec; _compressionCodec = compressionMethod.Value; SlicerFile = slicerFile; _index = index; _resolutionX = slicerFile.ResolutionX; _resolutionY = slicerFile.ResolutionY; //if (slicerFile is null) return; _positionZ = SlicerFile.GetHeightFromLayer(index); ResetParameters(); } public Layer(uint index, byte[] pngBytes, FileFormat slicerFile, LayerCompressionCodec? compressionMethod = null) : this(index, slicerFile, compressionMethod) { if (_compressionCodec == LayerCompressionCodec.Png) { CompressedBytes = pngBytes; } else { var mat = new Mat(); CvInvoke.Imdecode(pngBytes, ImreadModes.Grayscale, mat); LayerMat = mat; } _isModified = false; } public Layer(uint index, Mat layerMat, FileFormat slicerFile, LayerCompressionCodec? compressionMethod = null) : this(index, slicerFile, compressionMethod) { LayerMat = layerMat; _isModified = false; } public Layer(uint index, Stream stream, FileFormat slicerFile, LayerCompressionCodec? compressionMethod = null) : this(index, stream.ToArray(), slicerFile, compressionMethod) { } public Layer(FileFormat slicerFile, LayerCompressionCodec? compressionMethod = null) : this(0, slicerFile, compressionMethod) { } public Layer(byte[] pngBytes, FileFormat slicerFile, LayerCompressionCodec? compressionMethod = null) : this(0, pngBytes, slicerFile, compressionMethod) { } public Layer(Mat layerMat, FileFormat slicerFile, LayerCompressionCodec? compressionMethod = null) : this(0, layerMat, slicerFile, compressionMethod) { } public Layer(Stream stream, FileFormat slicerFile, LayerCompressionCodec? compressionMethod = null) : this(0, stream, slicerFile, compressionMethod) { } #endregion #region Equatables public static bool operator ==(Layer obj1, Layer obj2) { return obj1.Equals(obj2); } public static bool operator !=(Layer obj1, Layer obj2) { return !obj1.Equals(obj2); } public static bool operator >(Layer obj1, Layer obj2) { return obj1.Index > obj2.Index; } public static bool operator <(Layer obj1, Layer obj2) { return obj1.Index < obj2.Index; } public static bool operator >=(Layer obj1, Layer obj2) { return obj1.Index >= obj2.Index; } public static bool operator <=(Layer obj1, Layer obj2) { return obj1.Index <= obj2.Index; } public bool Equals(uint other) { return Index == other; } public bool Equals(Layer? other) { if (other is null) return false; if (ReferenceEquals(this, other)) return true; if (_index != other._index) return false; if (_compressedBytes?.Length != other._compressedBytes?.Length) return false; return _compressedBytes.AsSpan().SequenceEqual(other._compressedBytes.AsSpan()); //return Equals(_compressedBytes, other._compressedBytes); } public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((Layer)obj); } public override int GetHashCode() { return (_compressedBytes != null ? _compressedBytes.GetHashCode() : 0); } private sealed class IndexRelationalComparer : IComparer { public int Compare(Layer? x, Layer? y) { if (ReferenceEquals(x, y)) return 0; if (ReferenceEquals(null, y)) return 1; if (ReferenceEquals(null, x)) return -1; return x.Index.CompareTo(y.Index); } } public static IComparer IndexComparer { get; } = new IndexRelationalComparer(); #endregion #region Formaters public override string ToString() { return $"{nameof(Index)}: {Index}, " + $"{nameof(Filename)}: {Filename}, " + $"{nameof(NonZeroPixelCount)}: {NonZeroPixelCount}, " + $"{nameof(BoundingRectangle)}: {BoundingRectangle}, " + $"{nameof(FirstPixelPosition)}: {FirstPixelPosition}, " + $"{nameof(LastPixelPosition)}: {LastPixelPosition}, " + $"{nameof(IsBottomLayer)}: {IsBottomLayer}, " + $"{nameof(IsNormalLayer)}: {IsNormalLayer}, " + $"{nameof(LayerHeight)}: {LayerHeight}mm, " + $"{nameof(PositionZ)}: {PositionZ}mm, " + $"{nameof(LightOffDelay)}: {LightOffDelay}s, " + $"{nameof(WaitTimeBeforeCure)}: {WaitTimeBeforeCure}s, " + $"{nameof(ExposureTime)}: {ExposureTime}s, " + $"{nameof(WaitTimeAfterCure)}: {WaitTimeAfterCure}s, " + $"{nameof(LiftHeight)}: {LiftHeight}mm, " + $"{nameof(LiftSpeed)}: {LiftSpeed}mm/mim, " + $"{nameof(LiftHeight2)}: {LiftHeight2}mm, " + $"{nameof(LiftSpeed2)}: {LiftSpeed2}mm/mim, " + $"{nameof(WaitTimeAfterLift)}: {WaitTimeAfterLift}s, " + $"{nameof(RetractHeight)}: {RetractHeight}mm, " + $"{nameof(RetractSpeed)}: {RetractSpeed}mm/mim, " + $"{nameof(RetractHeight2)}: {RetractHeight2}mm, " + $"{nameof(RetractSpeed2)}: {RetractSpeed2}mm/mim, " + $"{nameof(LightPWM)}: {LightPWM}, " + $"{nameof(IsModified)}: {IsModified}, " + $"{nameof(IsUsingGlobalParameters)}: {IsUsingGlobalParameters}"; } #endregion #region Methods /// /// Reset all parameters to the default values from the global parameters /// public void ResetParameters() { _lightOffDelay = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLightOffDelay, SlicerFile.LightOffDelay); _waitTimeBeforeCure = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomWaitTimeBeforeCure, SlicerFile.WaitTimeBeforeCure); _exposureTime = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomExposureTime, SlicerFile.ExposureTime); _waitTimeAfterCure = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomWaitTimeAfterCure, SlicerFile.WaitTimeAfterCure); _liftHeight = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLiftHeight, SlicerFile.LiftHeight); _liftSpeed = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLiftSpeed, SlicerFile.LiftSpeed); _liftHeight2 = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLiftHeight2, SlicerFile.LiftHeight2); _liftSpeed2 = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLiftSpeed2, SlicerFile.LiftSpeed2); _waitTimeAfterLift = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomWaitTimeAfterLift, SlicerFile.WaitTimeAfterLift); _retractSpeed = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomRetractSpeed, SlicerFile.RetractSpeed); _retractHeight2 = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomRetractHeight2, SlicerFile.RetractHeight2); _retractSpeed2 = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomRetractSpeed2, SlicerFile.RetractSpeed2); _lightPWM = SlicerFile.GetBottomOrNormalValue(this, SlicerFile.BottomLightPWM, SlicerFile.LightPWM); } /// /// Gets the layer area (XY) in mm^2 /// Pixel size * number of pixels /// public float GetArea() => SlicerFile.PixelArea * _nonZeroPixelCount; /// /// Gets the layer area (XY) in mm^2 /// Pixel size * number of pixels /// public float GetArea(byte roundToDigits) => (float)Math.Round(GetArea(), roundToDigits); /// /// Gets the layer volume (XYZ) in mm^3 /// Pixel size * number of pixels * layer height /// public float GetVolume() => GetArea() * LayerHeight; /// /// Gets the layer volume (XYZ) in mm^3 /// Pixel size * number of pixels * layer height /// public float GetVolume(byte roundToDigits) => (float)Math.Round(GetArea() * LayerHeight, roundToDigits); public float CalculateMotorMovementTime(float extraTime = 0) { return OperationCalculator.LightOffDelayC.CalculateSeconds(this, extraTime); } public float CalculateLightOffDelay(float extraTime = 0) { if (SlicerFile is null) return OperationCalculator.LightOffDelayC.CalculateSeconds(this, extraTime); return SlicerFile.SupportsGCode ? extraTime : OperationCalculator.LightOffDelayC.CalculateSeconds(this, extraTime); } public void SetLightOffDelay(float extraTime = 0) { LightOffDelay = CalculateLightOffDelay(extraTime); } /// /// Gets the wait time before cure, if not available calculate it from light off delay /// /// public float GetWaitTimeBeforeCure() { if (SlicerFile.CanUseLayerWaitTimeBeforeCure) { return WaitTimeBeforeCure; } if (SlicerFile.CanUseLayerLightOffDelay) { return (float)Math.Max(0, Math.Round(LightOffDelay - CalculateLightOffDelay(), 2)); } return 0; } /// /// Attempt to set wait time before cure if supported, otherwise fallback to light-off delay /// /// The time to set /// When true and time is zero, it will calculate light-off delay without extra time, otherwise false to set light-off delay to 0 when time is 0 public void SetWaitTimeBeforeCureOrLightOffDelay(float time = 0, bool zeroLightOffDelayCalculateBase = false) { if (SlicerFile.CanUseLayerWaitTimeBeforeCure) { LightOffDelay = 0; WaitTimeBeforeCure = time; } else if(SlicerFile.CanUseLayerLightOffDelay) { if (time == 0 && !zeroLightOffDelayCalculateBase) { LightOffDelay = 0; return; } SetLightOffDelay(time); } } /// /// Zero all 'wait times / delays' for this layer /// public void SetNoDelays() { LightOffDelay = 0; WaitTimeBeforeCure = 0; WaitTimeAfterCure = 0; WaitTimeAfterLift = 0; } public string FormatFileName(string prepend = "", byte padDigits = 0, IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") { var index = Index; if (layerIndexStartNumber == IndexStartNumber.One) { index++; } return $"{prepend}{index.ToString().PadLeft(padDigits, '0')}{appendExt}"; } public string FormatFileName(byte padDigits, IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") => FormatFileName(string.Empty, padDigits, layerIndexStartNumber, appendExt); public string FormatFileNameWithLayerDigits(string prepend = "", IndexStartNumber layerIndexStartNumber = default, string appendExt = ".png") => FormatFileName(prepend, SlicerFile.LayerDigits, layerIndexStartNumber, appendExt); public Rectangle GetBoundingRectangle(Mat? mat = null, bool reCalculate = false) { if (_nonZeroPixelCount > 0 && !reCalculate) { return BoundingRectangle; } bool needDispose = false; if (mat is null) { if (_compressedBytes is null || _compressedBytes.Length == 0) { NonZeroPixelCount = 0; return Rectangle.Empty; } mat = LayerMat; needDispose = true; } //NonZeroPixelCount = (uint)CvInvoke.CountNonZero(mat); //BoundingRectangle = _nonZeroPixelCount > 0 ? CvInvoke.BoundingRectangle(mat) : Rectangle.Empty; BoundingRectangle = CvInvoke.BoundingRectangle(mat); if (_boundingRectangle.IsEmpty) { NonZeroPixelCount = 0; } else { var roiMat = mat.Roi(_boundingRectangle); NonZeroPixelCount = (uint)CvInvoke.CountNonZero(roiMat); // Compute first and last pixel var span = roiMat.GetDataByteSpan(); var yOffset = mat.GetRealStep() * BoundingRectangle.Y; for (var i = 0; i < span.Length; i++) { if (span[i] == 0) continue; var xOffset = BoundingRectangle.X + i; FirstPixelIndex = (uint) (yOffset + xOffset); FirstPixelPosition = new Point(xOffset, BoundingRectangle.Y); break; } for (var i = span.Length - 1; i >= 0; i--) { if (span[i] == 0) continue; LastPixelIndex = (uint) (yOffset + BoundingRectangle.X + i); LastPixelPosition = new Point(BoundingRectangle.Right - (span.Length - i), BoundingRectangle.Bottom - 1); break; } roiMat.DisposeIfSubMatrix(); } /* // Test mat.SetByte((int)FirstPixelIndex, 255); mat.SetByte(FirstPixelPosition, 255); mat.SetByte((int)LastPixelIndex, 255); mat.SetByte(LastPixelPosition, 255); mat.Save("D:\\test.png"); */ if (needDispose) mat.Dispose(); return BoundingRectangle; } public Rectangle GetBoundingRectangle(int marginX, int marginY) { var rect = BoundingRectangle; if (marginX == 0 && marginY == 0) return rect; rect.Inflate(marginX / 2, marginY / 2); SlicerFile.SanitizeBoundingRectangle(ref rect); return rect; } public Rectangle GetBoundingRectangle(int margin) => GetBoundingRectangle(margin, margin); public Rectangle GetBoundingRectangle(Size margin) => GetBoundingRectangle(margin.Width, margin.Height); public bool SetValueFromPrintParameterModifier(FileFormat.PrintParameterModifier modifier, decimal value) { if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.PositionZ)) { PositionZ = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.LightOffDelay)) { LightOffDelay = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.WaitTimeBeforeCure)) { WaitTimeBeforeCure = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.ExposureTime)) { ExposureTime = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.WaitTimeAfterCure)) { WaitTimeAfterCure = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.LiftHeight)) { LiftHeight = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.LiftSpeed)) { LiftSpeed = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.LiftHeight2)) { LiftHeight2 = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.LiftSpeed2)) { LiftSpeed2 = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.WaitTimeAfterLift)) { WaitTimeAfterLift = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.RetractSpeed)) { RetractSpeed = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.RetractHeight2)) { RetractHeight2 = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.RetractSpeed2)) { RetractSpeed2 = (float)value; return true; } if (ReferenceEquals(modifier, FileFormat.PrintParameterModifier.LightPWM)) { LightPWM = (byte)value; return true; } return false; } public byte SetValuesFromPrintParametersModifiers(FileFormat.PrintParameterModifier[]? modifiers) { if (modifiers is null) return 0; byte changed = 0; foreach (var modifier in modifiers) { if (!modifier.HasChanged) continue; SetValueFromPrintParameterModifier(modifier, modifier.NewValue); changed++; } return changed; } /* /// /// Gets all islands start pixel location for this layer /// https://www.geeksforgeeks.org/find-number-of-islands/ /// /// holding all islands coordinates public List GetIssues(uint requiredPixelsToSupportIsland = 5) { if (requiredPixelsToSupportIsland == 0) requiredPixelsToSupportIsland = 1; // These arrays are used to // get row and column numbers // of 8 neighbors of a given cell List result = new(); List pixels = new(); var mat = LayerMat; var bytes = mat.GetDataSpan(); var previousLayerImage = PreviousLayer()?.LayerMat; var previousBytes = previousLayerImage?.GetBytes(); // Make a bool array to // mark visited cells. // Initially all cells // are unvisited bool[,] visited = new bool[mat.Width, mat.Height]; // Initialize count as 0 and // traverse through the all // cells of given matrix //uint count = 0; // Island checker sbyte[] rowNbr = { -1, -1, -1, 0, 0, 1, 1, 1 }; sbyte[] colNbr = { -1, 0, 1, -1, 1, -1, 0, 1 }; const uint minPixel = 10; const uint minPixelForSupportIsland = 200; int pixelIndex; uint islandSupportingPixels; if (Index > 0) { for (int y = 0; y < mat.Height; y++) { for (int x = 0; x < mat.Width; x++) { pixelIndex = y * mat.Width + x; if (bytes[pixelIndex] > minPixel && !visited[x, y]) { // If a cell with value 1 is not // visited yet, then new island // found, Visit all cells in this // island and increment island count pixels.Clear(); pixels.Add(new Point(x, y)); islandSupportingPixels = previousBytes[pixelIndex] >= minPixelForSupportIsland ? 1u : 0; int minX = x; int maxX = x; int minY = y; int maxY = y; int x2; int y2; Queue queue = new(); queue.Enqueue(new Point(x, y)); // Mark this cell as visited visited[x, y] = true; while (queue.Count > 0) { var point = queue.Dequeue(); y2 = point.Y; x2 = point.X; for (byte k = 0; k < 8; k++) { //if (isSafe(y2 + rowNbr[k], x2 + colNbr[k])) var tempy2 = y2 + rowNbr[k]; var tempx2 = x2 + colNbr[k]; pixelIndex = tempy2 * mat.Width + tempx2; if (tempy2 >= 0 && tempy2 < mat.Height && tempx2 >= 0 && tempx2 < mat.Width && bytes[pixelIndex] >= minPixel && !visited[tempx2, tempy2]) { visited[tempx2, tempy2] = true; point = new Point(tempx2, tempy2); pixels.Add(point); queue.Enqueue(point); minX = Math.Min(minX, tempx2); maxX = Math.Max(maxX, tempx2); minY = Math.Min(minY, tempy2); maxY = Math.Max(maxY, tempy2); islandSupportingPixels += previousBytes[pixelIndex] >= minPixelForSupportIsland ? 1u : 0; } } } //count++; if (islandSupportingPixels >= requiredPixelsToSupportIsland) continue; // Not a island, bounding is strong if (islandSupportingPixels > 0 && pixels.Count < requiredPixelsToSupportIsland && islandSupportingPixels >= Math.Max(1, pixels.Count / 2)) continue; // Not a island result.Add(new LayerIssue(this, LayerIssue.IssueType.Island, pixels.ToArray(), new Rectangle(minX, minY, maxX - minX, maxY - minY))); } } } } pixels.Clear(); // TouchingBounds Checker for (int x = 0; x < mat.Width; x++) // Check Top and Bottom bounds { if (bytes[x] >= 200) // Top { pixels.Add(new Point(x, 0)); } if (bytes[mat.Width * mat.Height - mat.Width + x] >= 200) // Bottom { pixels.Add(new Point(x, mat.Height - 1)); } } for (int y = 0; y < mat.Height; y++) // Check Left and Right bounds { if (bytes[y * mat.Width] >= 200) // Left { pixels.Add(new Point(0, y)); } if (bytes[y * mat.Width + mat.Width - 1] >= 200) // Right { pixels.Add(new Point(mat.Width - 1, y)); } } if (pixels.Count > 0) { result.Add(new LayerIssue(this, LayerIssue.IssueType.TouchingBound, pixels.ToArray())); } pixels.Clear(); return result; }*/ /// /// Copy all parameters from this layer to an target layer /// /// public void CopyParametersTo(Layer layer) { CopyWaitTimesTo(layer); CopyExposureTo(layer); CopyLiftTo(layer); } /// /// Copy all exposure parameters from this layer to an target layer /// /// public void CopyExposureTo(Layer layer) { layer.ExposureTime = _exposureTime; layer.LightPWM = _lightPWM; } /// /// Copy all wait parameters from this layer to an target layer /// /// public void CopyWaitTimesTo(Layer layer) { layer.LightOffDelay = _lightOffDelay; layer.WaitTimeBeforeCure = _waitTimeBeforeCure; layer.WaitTimeAfterCure = _waitTimeAfterCure; layer.WaitTimeAfterLift = _waitTimeAfterLift; } /// /// Copy all lift parameters from this layer to an target layer /// /// public void CopyLiftTo(Layer layer) { layer.LiftHeight = _liftHeight; layer.LiftHeight2 = _liftHeight2; layer.LiftSpeed = _liftSpeed; layer.LiftSpeed2 = _liftSpeed2; layer.RetractHeight2 = _retractHeight2; layer.RetractSpeed = _retractSpeed; layer.RetractSpeed2 = _retractSpeed2; } /// /// Copy the image and related parameters from this layer to an target layer /// /// public void CopyImageTo(Layer layer) { if (!HaveImage) return; layer.ResolutionX = _resolutionX; layer.ResolutionY = _resolutionY; layer.CompressedBytes = _compressedBytes?.ToArray(); layer._contours = _contours?.Clone(); layer.BoundingRectangle = _boundingRectangle; layer.NonZeroPixelCount = _nonZeroPixelCount; layer.FirstPixelIndex = _firstPixelIndex; layer.FirstPixelPosition = _firstPixelPosition; layer.LastPixelIndex = _lastPixelIndex; layer.LastPixelPosition = _lastPixelPosition; } public Layer Clone() { var layer = (Layer)MemberwiseClone(); layer._compressedBytes = _compressedBytes?.ToArray(); layer._contours = _contours?.Clone(); //Debug.WriteLine(ReferenceEquals(_compressedBytes, layer.CompressedBytes)); return layer; /*return new (_index, CompressedBytes?.ToArray()!, SlicerFile) { _compressionMethod = _compressionMethod, _positionZ = _positionZ, _lightOffDelay = _lightOffDelay, _waitTimeBeforeCure = _waitTimeBeforeCure, _exposureTime = _exposureTime, _waitTimeAfterCure = _waitTimeAfterCure, _liftHeight = _liftHeight, _liftSpeed = _liftSpeed, _liftHeight2 = _liftHeight2, _liftSpeed2 = _liftSpeed2, _waitTimeAfterLift = _waitTimeAfterLift, _retractSpeed = _retractSpeed, _retractHeight2 = _retractHeight2, _retractSpeed2 = _retractSpeed2, _lightPWM = _lightPWM, _boundingRectangle = _boundingRectangle, _nonZeroPixelCount = _nonZeroPixelCount, _isModified = _isModified, _materialMilliliters = _materialMilliliters, };*/ } public Layer[] Clone(uint times) { var layers = new Layer[times]; for (int i = 0; i < times; i++) { layers[i] = Clone(); } return layers; } #endregion #region Static Methods /// /// Gets the bounding rectangle that is the union of a collection of layers /// /// Layer collection /// public static Rectangle GetBoundingRectangleUnion(params Layer[] layers) { var rect = Rectangle.Empty; foreach (var layer in layers) { if(layer.BoundingRectangle.IsEmpty) continue; rect = rect.IsEmpty ? layer.BoundingRectangle : Rectangle.Union(rect, layer.BoundingRectangle); } return rect; } public static float RoundHeight(float height) => (float) Math.Round(height, HeightPrecision); public static double RoundHeight(double height) => Math.Round(height, HeightPrecision); public static decimal RoundHeight(decimal height) => Math.Round(height, HeightPrecision); public static string ShowHeight(float height) => string.Format($"{{0:F{HeightPrecision}}}", height); public static string ShowHeight(double height) => string.Format($"{{0:F{HeightPrecision}}}", height); public static string ShowHeight(decimal height) => string.Format($"{{0:F{HeightPrecision}}}", height); public static Layer[] CloneLayers(Layer[] layers) { var clonedLayers = new Layer[layers.Length]; for (uint layerIndex = 0; layerIndex < layers.Length; layerIndex++) { clonedLayers[layerIndex] = layers[layerIndex].Clone(); } return clonedLayers; } public static byte[] CompressMat(Mat mat, LayerCompressionCodec codec) { switch (codec) { case LayerCompressionCodec.Png: return mat.GetPngByes(); case LayerCompressionCodec.Lz4: { var span = mat.GetDataByteSpan(); var target = new byte[LZ4Codec.MaximumOutputSize(span.Length)]; var encodedLength = LZ4Codec.Encode(span, target.AsSpan()); Array.Resize(ref target, encodedLength); return target; } case LayerCompressionCodec.GZip: { return CompressionExtensions.GZipCompressToBytes(mat.GetBytes(), CompressionLevel.Fastest); /*using var compressedStream = new MemoryStream(); using var matStream = mat.GetUnmanagedMemoryStream(FileAccess.Read); using var gzipStream = new GZipStream(compressedStream, CompressionLevel.Fastest); matStream.CopyTo(gzipStream); return compressedStream.ToArray();*/ } case LayerCompressionCodec.Deflate: { return CompressionExtensions.DeflateCompressToBytes(mat.GetBytes(), CompressionLevel.Fastest); /*using var compressedStream = new MemoryStream(); using var deflateStream = new DeflateStream(compressedStream, CompressionLevel.Fastest); deflateStream.Write(mat.GetDataByteSpan()); return compressedStream.ToArray();*/ } /*case LayerCompressionMethod.None: return mat.GetBytes();*/ default: throw new ArgumentOutOfRangeException(nameof(codec)); } } #endregion }