/* * 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 System; using System.Drawing; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml.Serialization; using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Layers; using UVtools.Core.Objects; namespace UVtools.Core.Operations; public abstract class Operation : BindableBase, IDisposable { #region Constants public const string NotSupportedMessage = "The current printer and/or file format is not compatible with this tool."; public string NotSupportedTitle => $"{Title} - Unable to run"; #endregion #region Enums public enum OperationImportFrom : byte { None, Profile, Session, Undo, } #endregion #region Members private FileFormat _slicerFile = null!; private Rectangle _originalBoundingRectangle; private OperationImportFrom _importedFrom = OperationImportFrom.None; private Rectangle _roi = Rectangle.Empty; private Point[][]? _maskPoints; private uint _layerIndexEnd; private uint _layerIndexStart; private string? _profileName; private bool _profileIsDefault; private LayerRangeSelection _layerRangeSelection = LayerRangeSelection.All; public const byte ClassNameLength = 9; #endregion #region Properties /// /// Gets or sets from where this option got loaded/imported /// [XmlIgnore] public OperationImportFrom ImportedFrom { get => _importedFrom; set => RaiseAndSetIfChanged(ref _importedFrom, value); } /// /// Gets or sets the parent /// [XmlIgnore] public FileFormat SlicerFile { get => _slicerFile; set { if(!RaiseAndSetIfChanged(ref _slicerFile, value)) return; OriginalBoundingRectangle = _slicerFile.BoundingRectangle; InitWithSlicerFile(); } } /// /// Gets the bounding rectangle of the model, preserved from any change during and after execution /// [XmlIgnore] public Rectangle OriginalBoundingRectangle { get => _originalBoundingRectangle; private set => RaiseAndSetIfChanged(ref _originalBoundingRectangle, value); } /// /// Gets or sets any object which is not used internally /// [XmlIgnore] public object? Tag { get; set; } /// /// Gets the ID name of this operation, this comes from class name with "Operation" removed /// public string Id => GetType().Name.Remove(0, ClassNameLength); /// /// Gets the starting layer selection /// public virtual LayerRangeSelection StartLayerRangeSelection => LayerRangeSelection.All; /// /// Gets the last used layer range selection, returns none if custom /// public LayerRangeSelection LayerRangeSelection { get => _layerRangeSelection; set => RaiseAndSetIfChanged(ref _layerRangeSelection, value); } /// /// Gets a string representing the layer range, used with profiles /// public virtual string LayerRangeString { get { if (LayerRangeSelection == LayerRangeSelection.None) { return $" [Layers: {LayerIndexStart}-{LayerIndexEnd}]"; } return $" [Layers: {LayerRangeSelection}]"; } } /// /// Gets if the LayerIndexEnd selector is enabled /// public virtual bool LayerIndexEndEnabled => true; /// /// Gets if this operation should set layer range to the actual layer index on layer preview /// public virtual bool PassActualLayerIndex => false; /// /// Gets if this operation can run in a file open as partial mode /// public virtual bool CanRunInPartialMode => false; /// /// Gets if this operation can make use of ROI /// public virtual bool CanROI => true; /// /// Gets if this operation can make use maskable areas /// public virtual bool CanMask => CanROI; /// /// Gets if this operation can store profiles /// public virtual bool CanHaveProfiles => true; /// /// Gets if this operation supports cancellation /// public virtual bool CanCancel => true; /// /// Gets the icon class to show on the UI /// public virtual string IconClass => null!; /// /// Gets the title of this operation /// public virtual string Title => Id; /// /// Gets a descriptive text of this operation /// public virtual string Description => Id; /// /// Gets the Ok button text /// public virtual string ButtonOkText => Title; /// /// Gets the confirmation text for the operation /// public virtual string ConfirmationText => $"Are you sure you want to {Id}"; /// /// Gets the progress window title /// public virtual string ProgressTitle => "Processing items"; /// /// Gets the progress action name /// public virtual string ProgressAction => Id; /// /// Gets if this operation have a action text /// public bool HaveAction => !string.IsNullOrEmpty(ProgressAction); /// /// Gets the start layer index where operation will starts in /// public virtual uint LayerIndexStart { get => _layerIndexStart; set { if (SlicerFile is not null) { SlicerFile.SanitizeLayerIndex(ref value); } if (!RaiseAndSetIfChanged(ref _layerIndexStart, value)) return; RaisePropertyChanged(nameof(LayerRangeCount)); RaisePropertyChanged(nameof(LayerRangeHaveBottoms)); } } /// /// Gets the end layer index where operation will ends in /// public virtual uint LayerIndexEnd { get => _layerIndexEnd; set { if (SlicerFile is not null) { SlicerFile.SanitizeLayerIndex(ref value); } if(!RaiseAndSetIfChanged(ref _layerIndexEnd, value)) return; RaisePropertyChanged(nameof(LayerRangeCount)); RaisePropertyChanged(nameof(LayerRangeHaveNormals)); } } /// /// Gets if any bottom layer is included in the selected layer range /// public bool LayerRangeHaveBottoms => LayerIndexStart < (SlicerFile.FirstNormalLayer?.Index ?? 0); /// /// Gets if any normal layer is included in the selected layer range /// public bool LayerRangeHaveNormals => LayerIndexEnd >= (SlicerFile.FirstNormalLayer?.Index ?? 0); /// /// Gets the number of selected layers /// public uint LayerRangeCount => (uint)Math.Max(0, (int)LayerIndexEnd - LayerIndexStart + 1); /// /// Gets the name for this profile /// public string? ProfileName { get => _profileName; set => RaiseAndSetIfChanged(ref _profileName, value); } /// /// Gets if this profile is the default to load /// public bool ProfileIsDefault { get => _profileIsDefault; set => RaiseAndSetIfChanged(ref _profileIsDefault, value); } /// /// Gets or sets an ROI to process this operation /// [XmlIgnore] public Rectangle ROI { get => _roi; set { if (!CanROI) return; RaiseAndSetIfChanged(ref _roi, value); } } /// /// Gets if there is an ROI associated /// public bool HaveROI => !ROI.IsEmpty; /// /// Gets or sets an Mask to process this operation /// [XmlIgnore] public Point[][]? MaskPoints { get => _maskPoints; set { if (!CanMask) return; if(!RaiseAndSetIfChanged(ref _maskPoints, value)) return; //if(HaveMask) ROI = Rectangle.Empty; } } /// /// Gets if there is masks associated /// public bool HaveMask => _maskPoints is not null && _maskPoints.Length > 0; /// /// Gets if there is roi or masks associated /// public bool HaveROIorMask => HaveROI || HaveMask; /// /// Gets if this operation have been executed once /// [XmlIgnore] public bool HaveExecuted { get; private set; } /// /// Gets if this operation have validated at least once /// [XmlIgnore] public bool IsValidated { get; private set; } /// /// Gets or sets an report to show to the user after complete the operation with success /// [XmlIgnore] public string? AfterCompleteReport { get; set; } #endregion #region Constructor protected Operation() { } protected Operation(FileFormat slicerFile) : this() { _slicerFile = slicerFile; OriginalBoundingRectangle = _slicerFile.BoundingRectangle; SelectAllLayers(); InitWithSlicerFile(); } #endregion #region Methods /// /// Gets if the operation can spawn /// public bool CanSpawn => string.IsNullOrWhiteSpace(ValidateSpawn()); /// /// Gets if this operation can spawn under the /// public virtual string? ValidateSpawn() => null; /// /// Gets if this operation can spawn under the /// public bool ValidateSpawn(out string? message) { message = ValidateSpawn(); return string.IsNullOrWhiteSpace(message); } /// /// Validates the operation, return null or empty if validates /// /// public virtual string? ValidateInternally() { if (!ValidateSpawn(out var message)) { return message; } return null; } /// /// Validates the operation /// /// null or empty if validates, otherwise return a string with error message public string? Validate() { IsValidated = true; return ValidateInternally(); } /// /// Gets if the operation is able to execute /// /// public bool CanValidate() { return string.IsNullOrWhiteSpace(Validate()); } /// /// Selects all layers from first to last layer /// public void SelectAllLayers() { LayerIndexStart = 0; LayerIndexEnd = SlicerFile.LastLayerIndex; LayerRangeSelection = LayerRangeSelection.All; } /// /// Selects one layer /// /// Layer index to select public void SelectCurrentLayer(uint layerIndex) { LayerIndexStart = LayerIndexEnd = layerIndex; LayerRangeSelection = LayerRangeSelection.Current; } /// /// Selects all bottom layers /// public void SelectBottomLayers() { LayerIndexStart = 0; LayerIndexEnd = Math.Max(1, SlicerFile.FirstNormalLayer?.Index ?? 1) - 1u; LayerRangeSelection = LayerRangeSelection.Bottom; } /// /// Selects all normal layers /// public void SelectNormalLayers() { LayerIndexStart = SlicerFile.FirstNormalLayer?.Index ?? 0; LayerIndexEnd = SlicerFile.LastLayerIndex; LayerRangeSelection = LayerRangeSelection.Normal; } /// /// Select the first layer (0) /// public void SelectFirstLayer() { LayerIndexStart = LayerIndexEnd = 0; LayerRangeSelection = LayerRangeSelection.First; } /// /// Select the last layer /// public void SelectLastLayer() { LayerIndexStart = LayerIndexEnd = SlicerFile.LastLayerIndex; LayerRangeSelection = LayerRangeSelection.Last; } /// /// Selects from first to a layer index /// /// To layer index to select public void SelectFirstToCurrentLayer(uint currentLayerIndex) { LayerIndexStart = 0; LayerIndexEnd = SlicerFile.SanitizeLayerIndex(currentLayerIndex); } /// /// Selects from a layer index to the last layer /// /// From layer index to select public void SelectCurrentToLastLayer(uint currentLayerIndex) { LayerIndexStart = SlicerFile.SanitizeLayerIndex(currentLayerIndex); LayerIndexEnd = SlicerFile.LastLayerIndex; } /// /// Selects layer given a range type /// /// /// public void SelectLayers(LayerRangeSelection range) { switch (range) { case LayerRangeSelection.None: break; case LayerRangeSelection.All: SelectAllLayers(); break; case LayerRangeSelection.Current: //SelectCurrentLayer(); break; case LayerRangeSelection.Bottom: SelectBottomLayers(); break; case LayerRangeSelection.Normal: SelectNormalLayers(); break; case LayerRangeSelection.First: SelectFirstLayer(); break; case LayerRangeSelection.Last: SelectLastLayer(); break; default: throw new NotImplementedException(); } } /// /// Called to init the object when changes /// public virtual void InitWithSlicerFile() { } /// /// Clears the ROI and set to empty /// public void ClearROI() { ROI = Rectangle.Empty; } /// /// Clear and /// public void ClearROIandMasks() { ClearROI(); ClearMasks(); } /// /// Set only if not set already /// /// ROI to set public void SetROIIfEmpty(Rectangle roi) { if (HaveROI) return; ROI = roi; } /// /// Gets the size, but if empty returns the file resolution size instead /// /// public Size GetRoiSizeOrDefault() => GetRoiSizeOrDefault(SlicerFile.Resolution); /// /// Gets the size, but if empty returns size instead /// /// /// public Size GetRoiSizeOrDefault(Mat? src) => src is null ? GetRoiSizeOrDefault() : GetRoiSizeOrDefault(src.Size); /// /// Gets the size, but if empty returns the size from instead /// /// /// public Size GetRoiSizeOrDefault(Rectangle fallbackRectangle) => GetRoiSizeOrDefault(fallbackRectangle.Size); /// /// Gets the size, but if empty returns the instead /// /// /// public Size GetRoiSizeOrDefault(Size fallbackSize) { return HaveROI ? _roi.Size : fallbackSize; } /// /// Gets the size, but if empty returns the model volume bounds size instead /// /// public Size GetRoiSizeOrVolumeSize() => GetRoiSizeOrVolumeSize(_originalBoundingRectangle.Size); /// /// Gets the size, but if empty returns the instead /// /// /// public Size GetRoiSizeOrVolumeSize(Size fallbackSize) { return HaveROI ? _roi.Size : fallbackSize; } /// /// Gets a cropped shared from by the , but if empty return the instead /// /// /// public Mat GetRoiOrDefault(Mat src) { return HaveROI && src.Size != _roi.Size ? src.Roi(_roi) : src; } /// /// Gets a cropped shared from by the , but if empty crop by /// /// /// /// public Mat GetRoiOrDefault(Mat src, Rectangle fallbackRoi) { if (HaveROI && src.Size != _roi.Size) return src.Roi(_roi); if (fallbackRoi.IsEmpty) return src; return src.Size != fallbackRoi.Size ? src.Roi(fallbackRoi) : src; } /// /// Gets a cropped shared by the , but if empty crop by /// /// /// public Mat GetRoiOrVolumeBounds(Mat defaultMat) { return GetRoiOrDefault(defaultMat, _originalBoundingRectangle); } /// /// Gets the , but if empty returns /// /// public Rectangle GetRoiOrVolumeBounds() => HaveROI ? _roi : _originalBoundingRectangle; /// /// Clears all masks /// public void ClearMasks() { MaskPoints = null; } /// /// Sets masks only if they are empty /// /// public void SetMasksIfEmpty(Point[][] points) { if (HaveMask) return; MaskPoints = points; } /// /// Returns a mask given /// /// /// /// public Mat? GetMask(Mat mat, Point offset = default) => GetMask(_maskPoints, mat, offset); public Mat? GetMask(Point[][]? points, Mat mat, Point offset = default) { if (!HaveMask) return null; var mask = mat.CreateMask(points!, offset); return GetRoiOrDefault(mask); } /// /// Apply a mask to a mat /// /// Original untouched mat /// Mat to modify and apply the mask /// Mask public void ApplyMask(Mat original, Mat result, Mat? mask) { if (mask is null) return; var originalRoi = GetRoiOrDefault(original); var resultRoi = result; if (originalRoi.Size != result.Size) // Accept a ROI mat { resultRoi = GetRoiOrDefault(result); } if (mask.Size != resultRoi.Size) // Accept a full size mask { mask = GetRoiOrDefault(mask); } using var tempMat = originalRoi.Clone(); resultRoi.CopyTo(tempMat, mask); tempMat.CopyTo(resultRoi); } /// /// Gets a mask and apply it /// /// Original unmodified image /// Result image which will also be modified public void ApplyMask(Mat original, Mat result) { using var mask = GetMask(original); ApplyMask(original, result, mask); } /// /// Execute the operation internally, to be override by class /// /// /// /// protected virtual bool ExecuteInternally(OperationProgress progress) { throw new NotImplementedException(); } /// /// Execute the operation /// /// /// /// public bool Execute(OperationProgress? progress = null) { if (_slicerFile is null) throw new InvalidOperationException($"{Title} can't execute due the lacking of a file parent."); if (_slicerFile.DecodeType == FileFormat.FileDecodeType.Partial && !CanRunInPartialMode) throw new InvalidOperationException($"The file was open in partial mode and the tool \"{Title}\" is unable to run in this mode.\nPlease reload the file in full mode in order to use this tool."); AfterCompleteReport = null; if (!IsValidated) { var msg = Validate(); if(!string.IsNullOrWhiteSpace(msg)) throw new InvalidOperationException($"{Title} can't execute due some errors:\n{msg}"); } progress ??= new OperationProgress(); progress.Reset(ProgressAction, LayerRangeCount); HaveExecuted = true; var result = ExecuteInternally(progress); progress.PauseOrCancelIfRequested(); return result; } public Task ExecuteAsync(OperationProgress? progress = null) => Task.Run(() => Execute(progress), progress?.Token ?? default); /// /// Execute the operation on a given /// /// /// /// /// public virtual bool Execute(Mat mat, params object[]? arguments) { throw new NotImplementedException(); } public Task ExecuteAsync(Mat mat, params object[]? arguments) => Task.Run(() => Execute(mat, arguments)); /// /// Get the selected layer range in a new array, array index will not match layer index when a range is selected /// /// public Layer[] GetSelectedLayerRange() { return LayerRangeCount == SlicerFile.LayerCount ? SlicerFile.ToArray() : SlicerFile.Where((_, layerIndex) => layerIndex >= _layerIndexStart && layerIndex <= _layerIndexEnd).ToArray(); } /// /// Copy this operation base configuration to another operation. /// Layer range, ROI, Masks /// /// public void CopyConfigurationTo(Operation operation) { operation.LayerIndexStart = LayerIndexStart; operation.LayerIndexEnd = LayerIndexEnd; operation.ROI = ROI; operation.MaskPoints = MaskPoints; } /// /// Serialize class to XML file /// /// /// public void Serialize(string path, bool indent = false) { if(indent) XmlExtensions.SerializeToFile(this, path, XmlExtensions.SettingsIndent); else XmlExtensions.SerializeToFile(this, path); } /// /// Clone object /// /// public virtual Operation Clone() { var operation = MemberwiseClone() as Operation; operation!.SlicerFile = _slicerFile; return operation; } public override string ToString() { if (!string.IsNullOrEmpty(ProfileName)) return ProfileName; var result = $"{Title}: {LayerRangeString}"; return result; } public virtual void Dispose() { /*GC.SuppressFinalize(this);*/ } #endregion #region Static Methods /// /// Deserialize from a XML file /// /// XML file path /// public static Operation? Deserialize(string path) { if (!File.Exists(path)) return null; var fileText = File.ReadAllText(path); var match = Regex.Match(fileText, @"(?:<\/\s*Operation)([a-zA-Z0-9_]+)(?:\s*>)"); if (!match.Success) return null; if (match.Groups.Count < 1) return null; var operationName = match.Groups[1].Value; var baseType = typeof(Operation).FullName; if (string.IsNullOrWhiteSpace(baseType)) return null; var classname = baseType + operationName + ", UVtools.Core"; var type = Type.GetType(classname); if (type is null) return null; return Deserialize(path, type); } /// /// Deserialize from a XML file /// /// XML file path /// /// public static Operation Deserialize(string path, Type type) { var serializer = new XmlSerializer(type); using var stream = File.OpenRead(path); var operation = serializer.Deserialize(stream) as Operation; operation!.ImportedFrom = OperationImportFrom.Session; return operation; } /// /// Deserialize from a XML file /// /// XML file path /// /// public static Operation Deserialize(string path, Operation operation) => Deserialize(path, operation.GetType()); #endregion }