diff options
author | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-02-21 06:38:37 +0300 |
---|---|---|
committer | Tiago Conceição <Tiago_caza@hotmail.com> | 2021-02-21 06:38:37 +0300 |
commit | 4ec98cfab14f4e2c8cd1ba246c85e75ea67d1934 (patch) | |
tree | e051c435005e44a6ce4ed053b31b4fe2644f42cd | |
parent | 9dffbd77b957c95c475ea35ad20d4410fa0f2378 (diff) |
v2.5.0v2.5.0
* (Add) Help - Material manager (F10): Allow to manage material stock and costs with statistic over time
* (Add) File - I printed this file (CTRL + P): Allow to select a material and consume resin from stock and print time from the loaded file
-rw-r--r-- | CHANGELOG.md | 5 | ||||
-rw-r--r-- | UVtools.Core/Managers/MaterialManager.cs | 271 | ||||
-rw-r--r-- | UVtools.Core/Objects/Material.cs | 99 | ||||
-rw-r--r-- | UVtools.Core/Operations/OperationIPrintedThisFile.cs | 138 | ||||
-rw-r--r-- | UVtools.Core/UVtools.Core.csproj | 4 | ||||
-rw-r--r-- | UVtools.WPF/App.axaml.cs | 4 | ||||
-rw-r--r-- | UVtools.WPF/Assets/Icons/flask-16x16.png | bin | 0 -> 162 bytes | |||
-rw-r--r-- | UVtools.WPF/Controls/Tools/ToolIPrintedThisFileControl.axaml | 53 | ||||
-rw-r--r-- | UVtools.WPF/Controls/Tools/ToolIPrintedThisFileControl.axaml.cs | 21 | ||||
-rw-r--r-- | UVtools.WPF/MainWindow.axaml | 21 | ||||
-rw-r--r-- | UVtools.WPF/MainWindow.axaml.cs | 17 | ||||
-rw-r--r-- | UVtools.WPF/UVtools.WPF.csproj | 7 | ||||
-rw-r--r-- | UVtools.WPF/Windows/MaterialManagerWindow.axaml | 254 | ||||
-rw-r--r-- | UVtools.WPF/Windows/MaterialManagerWindow.axaml.cs | 97 | ||||
-rw-r--r-- | UVtools.WPF/Windows/PrusaSlicerManagerWindow.axaml (renamed from UVtools.WPF/Windows/PrusaSlicerManager.axaml) | 2 | ||||
-rw-r--r-- | UVtools.WPF/Windows/PrusaSlicerManagerWindow.axaml.cs (renamed from UVtools.WPF/Windows/PrusaSlicerManager.axaml.cs) | 4 |
16 files changed, 967 insertions, 30 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index f9999cb..38ceaba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## 21/02/2021 - v2.5.0 + +* (Add) Help - Material manager (F10): Allow to manage material stock and costs with statistic over time +* (Add) File - I printed this file (CTRL + P): Allow to select a material and consume resin from stock and print time from the loaded file + ## 19/02/2021 - v2.4.9 * **(Fix) PhotonWorkshop files: (#149)** diff --git a/UVtools.Core/Managers/MaterialManager.cs b/UVtools.Core/Managers/MaterialManager.cs new file mode 100644 index 0000000..bd0446b --- /dev/null +++ b/UVtools.Core/Managers/MaterialManager.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Xml.Serialization; +using UVtools.Core.Objects; + +namespace UVtools.Core.Managers +{ + public class MaterialManager : BindableBase, IList<Material> + { + #region Settings + + public static string FilePath; + #endregion + + #region Singleton + + private static Lazy<MaterialManager> _instanceHolder = + new(() => new MaterialManager()); + + /// <summary> + /// Instance of <see cref="UserSettings"/> (singleton) + /// </summary> + public static MaterialManager Instance => _instanceHolder.Value; + + //public static List<Operation> Operations => _instance.Operations; + #endregion + + #region Members + + private ObservableCollection<Material> _materials = new(); + + #endregion + + #region Properties + + public ObservableCollection<Material> Materials + { + get => _materials; + set => RaiseAndSetIfChanged(ref _materials, value); + } + + /// <summary> + /// Gets the total number of bottles in stock + /// </summary> + public int BottlesInStock => this.Sum(material => material.BottlesInStock); + + /// <summary> + /// Gets the total number of bottles ever owned + /// </summary> + public int OwnedBottles => this.Sum(material => material.OwnedBottles); + + /// <summary> + /// Gets the total of consumed volume in milliliters + /// </summary> + public decimal ConsumedVolume => this.Sum(material => material.ConsumedVolume); + + /// <summary> + /// Gets the total of consumed volume in liters + /// </summary> + public decimal ConsumedVolumeLiters => this.Sum(material => material.ConsumedVolumeLiters); + + /// <summary> + /// Gets the total volume in stock in milliliters + /// </summary> + public decimal VolumeInStock => this.Sum(material => material.VolumeInStock); + + /// <summary> + /// Gets the total volume in stock in liters + /// </summary> + public decimal VolumeInStockLiters => VolumeInStock / 1000; + + /// <summary> + /// Gets the total costs + /// </summary> + public decimal TotalCost => this.Sum(material => material.TotalCost); + + /// <summary> + /// Gets the total print time in hours + /// </summary> + public double PrintTime => this.Sum(material => material.PrintTime); + + /// <summary> + /// Gets the total print time + /// </summary> + public TimeSpan PrintTimeSpan => TimeSpan.FromHours(PrintTime); + + #endregion + + #region Constructor + private MaterialManager() + { + } + #endregion + + #region Methods + public Material this[int index] + { + get => _materials[index]; + set => _materials[index] = value; + } + + public Material this[uint index] + { + get => _materials[(int) index]; + set => _materials[(int) index] = value; + } + + public Material this[Material material] + { + get + { + var index = IndexOf(material); + return index < 0 ? this[index] : null; + } + set + { + var index = IndexOf(material); + if(index >= 0) this[index] = value; + } + } + + public IEnumerator<Material> GetEnumerator() => _materials.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public void Add(Material item) + { + _materials.Add(item); + RaisePropertiesChanged(); + } + + public void Add(Material item, bool save) + { + Add(item); + if (save) Save(); + } + + public void Clear() + { + _materials.Clear(); + RaisePropertiesChanged(); + } + + public void Clear(bool save) + { + Clear(); + if(save) Save(); + } + + + public bool Contains(Material item) => _materials.Contains(item); + + public void CopyTo(Material[] array, int arrayIndex) + { + _materials.CopyTo(array, arrayIndex); + RaisePropertiesChanged(); + } + + public bool Remove(Material item) + { + if (_materials.Remove(item)) + { + RaisePropertiesChanged(); + return true; + } + + return false; + } + + public void Remove(Material item, bool save) + { + Remove(item); + if (save) Save(); + } + + public int Count => _materials.Count; + public bool IsReadOnly => false; + public int IndexOf(Material item) => _materials.IndexOf(item); + + public void Insert(int index, Material item) + { + _materials.Insert(index, item); + RaisePropertiesChanged(); + } + + public void Insert(int index, Material item, bool save) + { + Insert(index, item); + if (save) Save(); + } + + public void RemoveAt(int index) + { + _materials.RemoveAt(index); + RaisePropertiesChanged(); + } + + public void RemoveAt(int index, bool save) + { + RemoveAt(index); + if (save) Save(); + } + + public void RaisePropertiesChanged() + { + RaisePropertyChanged(nameof(BottlesInStock)); + RaisePropertyChanged(nameof(OwnedBottles)); + RaisePropertyChanged(nameof(ConsumedVolume)); + RaisePropertyChanged(nameof(ConsumedVolumeLiters)); + RaisePropertyChanged(nameof(VolumeInStock)); + RaisePropertyChanged(nameof(VolumeInStockLiters)); + RaisePropertyChanged(nameof(TotalCost)); + RaisePropertyChanged(nameof(PrintTime)); + RaisePropertyChanged(nameof(PrintTimeSpan)); + RaisePropertyChanged(nameof(Count)); + } + + /// <summary> + /// Load settings from file + /// </summary> + public static void Load() + { + if (string.IsNullOrWhiteSpace(FilePath) || !File.Exists(FilePath)) + { + return; + } + + var serializer = new XmlSerializer(Instance.GetType()); + try + { + using var myXmlReader = new StreamReader(FilePath); + var instance = (MaterialManager)serializer.Deserialize(myXmlReader); + _instanceHolder = new Lazy<MaterialManager>(() => instance); + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + } + } + + /// <summary> + /// Save settings to file + /// </summary> + public static void Save() + { + var serializer = new XmlSerializer(Instance.GetType()); + try + { + using var myXmlWriter = new StreamWriter(FilePath); + serializer.Serialize(myXmlWriter, Instance); + } + catch (Exception e) + { + Debug.WriteLine(e.ToString()); + } + } + + #endregion + + public void SortByName() + { + var materials = _materials.ToList(); + materials.Sort((material, material1) => string.Compare(material.Name, material1.Name, StringComparison.Ordinal)); + Materials = new ObservableCollection<Material>(materials); + } + } +}
\ No newline at end of file diff --git a/UVtools.Core/Objects/Material.cs b/UVtools.Core/Objects/Material.cs index fed7392..84f39fa 100644 --- a/UVtools.Core/Objects/Material.cs +++ b/UVtools.Core/Objects/Material.cs @@ -13,17 +13,17 @@ namespace UVtools.Core.Objects /// <summary> /// Represents a material to feed in the printer /// </summary> - public class Material : BindableBase + public class Material : BindableBase, ICloneable { #region Members private string _name; - private decimal _bottleVolume = 1000; + private uint _bottleVolume = 1000; private decimal _density = 1; private decimal _bottleCost = 30; private int _bottlesInStock = 1; private decimal _bottleRemainingVolume = 1000; - private decimal _totalConsumedVolume; - private double _totalPrintTime; + private decimal _consumedVolume; + private double _printTime; #endregion @@ -37,14 +37,16 @@ namespace UVtools.Core.Objects /// <summary> /// Gets or sets the bottle volume in milliliters /// </summary> - public decimal BottleVolume + public uint BottleVolume { get => _bottleVolume; set { if(!RaiseAndSetIfChanged(ref _bottleVolume, value)) return; RaisePropertyChanged(nameof(BottleWeight)); - RaisePropertyChanged(nameof(TotalConsumedBottles)); + RaisePropertyChanged(nameof(ConsumedBottles)); + RaisePropertyChanged(nameof(TotalCost)); + RaisePropertyChanged(nameof(VolumeInStock)); } } @@ -72,16 +74,28 @@ namespace UVtools.Core.Objects public decimal BottleCost { get => _bottleCost; - set => RaiseAndSetIfChanged(ref _bottleCost, value); + set + { + if(!RaiseAndSetIfChanged(ref _bottleCost, value)) return; + RaisePropertyChanged(nameof(TotalCost)); + } } + public decimal TotalCost => OwnedBottles * _bottleCost; + /// <summary> /// Gets or sets the number of bottles in stock /// </summary> public int BottlesInStock { get => _bottlesInStock; - set => RaiseAndSetIfChanged(ref _bottlesInStock, value); + set + { + if(!RaiseAndSetIfChanged(ref _bottlesInStock, value)) return; + RaisePropertyChanged(nameof(OwnedBottles)); + RaisePropertyChanged(nameof(TotalCost)); + RaisePropertyChanged(nameof(VolumeInStock)); + } } /// <summary> @@ -89,47 +103,75 @@ namespace UVtools.Core.Objects /// </summary> public decimal BottleRemainingVolume { - get => _bottleRemainingVolume; - set => RaiseAndSetIfChanged(ref _bottleRemainingVolume, value); + get => Math.Round(_bottleRemainingVolume, 2); + set + { + if(!RaiseAndSetIfChanged(ref _bottleRemainingVolume, value)) return; + RaisePropertyChanged(nameof(VolumeInStock)); + } } /// <summary> + /// Gets the total available volume in stock in milliliters + /// </summary> + public decimal VolumeInStock => _bottlesInStock * _bottleVolume - (_bottleVolume - _bottleRemainingVolume); + + /// <summary> /// Gets the number of consumed bottles /// </summary> - public uint TotalConsumedBottles => (uint) Math.Floor(_totalConsumedVolume / _bottleVolume); + public uint ConsumedBottles => (uint) Math.Floor(_consumedVolume / _bottleVolume); + + /// <summary> + /// Gets the total number of owned bottles + /// </summary> + public int OwnedBottles => (int) (_bottlesInStock + ConsumedBottles); /// <summary> /// Gets or sets the total number of consumed volume in milliliters /// </summary> - public decimal TotalConsumedVolume + public decimal ConsumedVolume { - get => _totalConsumedVolume; - set => RaiseAndSetIfChanged(ref _totalConsumedVolume, value); + get => _consumedVolume; + set + { + if(!RaiseAndSetIfChanged(ref _consumedVolume, value)) return; + RaisePropertyChanged(nameof(ConsumedVolumeLiters)); + } } /// <summary> + /// Gets total number of consumed volume in liters + /// </summary> + public decimal ConsumedVolumeLiters => ConsumedVolume / 1000; + + /// <summary> /// Gets or sets the total print time using with material in hours /// </summary> - public double TotalPrintTime + public double PrintTime { - get => _totalPrintTime; - set => RaiseAndSetIfChanged(ref _totalPrintTime, value); + get => _printTime; + set + { + if(!RaiseAndSetIfChanged(ref _printTime, value)) return; + RaisePropertyChanged(nameof(PrintTimeSpan)); + } } - public TimeSpan TotalPrintTimeSpan => TimeSpan.FromHours(_totalPrintTime); + public TimeSpan PrintTimeSpan => TimeSpan.FromHours(_printTime); #endregion #region Constructors public Material() { } - public Material(string name, decimal bottleVolume = 1000, decimal density = 1, decimal bottleCost = 30, int bottlesInStock = 1) + public Material(string name, uint bottleVolume = 1000, decimal density = 1, decimal bottleCost = 30, int bottlesInStock = 1) { _name = name; _bottleVolume = bottleVolume; _density = density; _bottleCost = bottleCost; _bottlesInStock = bottlesInStock; + _bottleRemainingVolume = bottleVolume; } #endregion @@ -155,7 +197,17 @@ namespace UVtools.Core.Objects public override string ToString() { - return $"{nameof(Name)}: {Name}, {nameof(BottleVolume)}: {BottleVolume}ml, {nameof(BottleWeight)}: {BottleWeight}g, {nameof(Density)}: {Density}g/ml, {nameof(BottleCost)}: {BottleCost}, {nameof(BottlesInStock)}: {BottlesInStock}, {nameof(BottleRemainingVolume)}: {BottleRemainingVolume}ml, {nameof(TotalConsumedBottles)}: {TotalConsumedBottles}, {nameof(TotalConsumedVolume)}: {TotalConsumedVolume}ml, {nameof(TotalPrintTime)}: {TotalPrintTime:F4}h"; + return $"{_name} ({_bottleRemainingVolume}/{VolumeInStock}ml)"; + } + + public object Clone() + { + return MemberwiseClone(); + } + + public Material CloneMaterial() + { + return (Material)Clone(); } #endregion @@ -200,8 +252,9 @@ namespace UVtools.Core.Objects } BottlesInStock -= consumedBottles; + ConsumedVolume += volume; - AddPrintTime(printSeconds); + AddPrintTimeSeconds(printSeconds); return _bottlesInStock > 0; } @@ -210,10 +263,10 @@ namespace UVtools.Core.Objects /// Add print time with this material /// </summary> /// <param name="seconds">Seconds to add</param> - public void AddPrintTime(double seconds) + public void AddPrintTimeSeconds(double seconds) { if (seconds <= 0) return; - TotalPrintTime += seconds / 60 / 60; + PrintTime += seconds / 60 / 60; } #endregion } diff --git a/UVtools.Core/Operations/OperationIPrintedThisFile.cs b/UVtools.Core/Operations/OperationIPrintedThisFile.cs new file mode 100644 index 0000000..851c4d1 --- /dev/null +++ b/UVtools.Core/Operations/OperationIPrintedThisFile.cs @@ -0,0 +1,138 @@ +/* + * 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.Text; +using UVtools.Core.FileFormats; +using UVtools.Core.Managers; +using UVtools.Core.Objects; + +namespace UVtools.Core.Operations +{ + [Serializable] + public class OperationIPrintedThisFile : Operation + { + #region Members + private decimal _volume; + private float _printTime; + private Material _materialItem; + + #endregion + + #region Overrides + + public override Enumerations.LayerRangeSelection StartLayerRangeSelection => Enumerations.LayerRangeSelection.None; + + public override bool CanROI => false; + public override bool CanHaveProfiles => false; + public override string ButtonOkText => "Consume"; + + public override string Title => "I printed this file"; + public override string Description => "Select a material and consume resin from stock and print time."; + + public override string ConfirmationText => + $"consume {_volume}ml and {PrintTimeHours}h on:\n{_materialItem} ?"; + + public override string ProgressTitle => + $"Consuming"; + + public override string ProgressAction => "Consumed"; + + public override StringTag Validate(params object[] parameters) + { + var sb = new StringBuilder(); + + if (_materialItem is null) + { + sb.AppendLine("You must select an material."); + } + if (_volume <= 0) + { + sb.AppendLine("Volume must be higher than 0ml."); + } + if (_printTime <= 0) + { + sb.AppendLine("Print time must be higher than 0s."); + } + + return new StringTag(sb.ToString()); + } + + public override string ToString() + { + var result = $"{_volume}ml {PrintTimeHours}h"; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + } + #endregion + + #region Properties + + public Material MaterialItem + { + get => _materialItem; + set => RaiseAndSetIfChanged(ref _materialItem, value); + } + + public decimal Volume + { + get => _volume; + set => RaiseAndSetIfChanged(ref _volume, value); + } + + public float PrintTime + { + get => _printTime; + set + { + if(!RaiseAndSetIfChanged(ref _printTime, value)) return; + RaisePropertyChanged(nameof(PrintTimeHours)); + } + } + + public float PrintTimeHours => _printTime / 60 / 60; + + public MaterialManager Manager => MaterialManager.Instance; + + #endregion + + #region Constructor + + public OperationIPrintedThisFile() { } + + public OperationIPrintedThisFile(FileFormat slicerFile) : base(slicerFile) { } + + public override void InitWithSlicerFile() + { + base.InitWithSlicerFile(); + _volume = (decimal) SlicerFile.MaterialMilliliters; + _printTime = SlicerFile.PrintTime; + MaterialManager.Load(); + } + + #endregion + + #region Methods + + protected override bool ExecuteInternally(OperationProgress progress) + { + if (MaterialItem is null) return !progress.Token.IsCancellationRequested; + + MaterialItem.Consume(_volume, _printTime); + + MaterialManager.Save(); + return !progress.Token.IsCancellationRequested; + } + + #endregion + + #region Equality + + #endregion + } +} diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index 37302e0..9c4fcf6 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -10,7 +10,7 @@ <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <PackageProjectUrl>https://github.com/sn4k3/UVtools</PackageProjectUrl> <Description>MSLA/DLP, file analysis, calibration, repair, conversion and manipulation</Description> - <Version>2.4.9</Version> + <Version>2.5.0</Version> <Copyright>Copyright © 2020 PTRTECH</Copyright> <PackageIcon>UVtools.png</PackageIcon> <Platforms>AnyCPU;x64</Platforms> @@ -44,7 +44,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="BinarySerializer" Version="8.5.3" /> + <PackageReference Include="BinarySerializer" Version="8.6.0" /> <PackageReference Include="Emgu.CV" Version="4.5.1.4349" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="System.Memory" Version="4.5.4" /> diff --git a/UVtools.WPF/App.axaml.cs b/UVtools.WPF/App.axaml.cs index 219a7db..7a454a1 100644 --- a/UVtools.WPF/App.axaml.cs +++ b/UVtools.WPF/App.axaml.cs @@ -22,6 +22,7 @@ using Avalonia.ThemeManager; using Emgu.CV; using UVtools.Core; using UVtools.Core.FileFormats; +using UVtools.Core.Managers; using UVtools.WPF.Extensions; using UVtools.WPF.Structures; @@ -52,6 +53,9 @@ namespace UVtools.WPF OperationProfiles.Load(); + MaterialManager.FilePath = Path.Combine(UserSettings.SettingsFolder, "materials.xml"); + MaterialManager.Load(); + /*ThemeSelector = ThemeSelector.Create(Path.Combine(ApplicationPath, "Assets", "Themes")); ThemeSelector.LoadSelectedTheme(Path.Combine(UserSettings.SettingsFolder, "selected.theme")); if (ThemeSelector.SelectedTheme.Name == "UVtoolsDark" || ThemeSelector.SelectedTheme.Name == "Light") diff --git a/UVtools.WPF/Assets/Icons/flask-16x16.png b/UVtools.WPF/Assets/Icons/flask-16x16.png Binary files differnew file mode 100644 index 0000000..4cd3c0a --- /dev/null +++ b/UVtools.WPF/Assets/Icons/flask-16x16.png diff --git a/UVtools.WPF/Controls/Tools/ToolIPrintedThisFileControl.axaml b/UVtools.WPF/Controls/Tools/ToolIPrintedThisFileControl.axaml new file mode 100644 index 0000000..7d2e04f --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolIPrintedThisFileControl.axaml @@ -0,0 +1,53 @@ +<UserControl xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" + x:Class="UVtools.WPF.Controls.Tools.ToolIPrintedThisFileControl"> + + <Grid RowDefinitions="Auto,10,Auto,10,Auto" + ColumnDefinitions="Auto,10,Auto"> + <TextBlock Grid.Row="0" Grid.Column="0" + VerticalAlignment="Center" + ToolTip.Tip="The material you want to consume from (remaining in current bottle / total in stock)" + Text="Material:"/> + <ComboBox Grid.Row="0" Grid.Column="2" + HorizontalAlignment="Stretch" + MinWidth="600" + Items="{Binding Operation.Manager}" + SelectedItem="{Binding Operation.MaterialItem}"/> + + <TextBlock Grid.Row="2" Grid.Column="0" + VerticalAlignment="Center" + Text="Volume:"/> + + <StackPanel Grid.Row="2" Grid.Column="2" + Orientation="Horizontal" Spacing="5"> + <NumericUpDown Minimum="1" + Maximum="1000000" + Increment="1" + MinWidth="200" + Value="{Binding Operation.Volume}"/> + <TextBlock VerticalAlignment="Center" Text="ml"/> + + </StackPanel> + + + <TextBlock Grid.Row="4" Grid.Column="0" + VerticalAlignment="Center" + Text="Print time:"/> + + <StackPanel Grid.Row="4" Grid.Column="2" + Orientation="Horizontal" Spacing="5"> + <NumericUpDown Minimum="1" + Maximum="100000000" + Increment="1" + MinWidth="200" + FormatString="F2" + Value="{Binding Operation.PrintTime}"/> + <TextBlock VerticalAlignment="Center" + Text="{Binding Operation.PrintTimeHours, StringFormat=s / {0:N4}h}"/> + </StackPanel> + </Grid> + +</UserControl> diff --git a/UVtools.WPF/Controls/Tools/ToolIPrintedThisFileControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolIPrintedThisFileControl.axaml.cs new file mode 100644 index 0000000..91ed2f1 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolIPrintedThisFileControl.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia.Markup.Xaml; +using UVtools.Core.Operations; + +namespace UVtools.WPF.Controls.Tools +{ + public class ToolIPrintedThisFileControl : ToolControl + { + public OperationIPrintedThisFile Operation => BaseOperation as OperationIPrintedThisFile; + + public ToolIPrintedThisFileControl() + { + InitializeComponent(); + BaseOperation = new OperationIPrintedThisFile(SlicerFile); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/UVtools.WPF/MainWindow.axaml b/UVtools.WPF/MainWindow.axaml index 93d132f..2229ca7 100644 --- a/UVtools.WPF/MainWindow.axaml +++ b/UVtools.WPF/MainWindow.axaml @@ -72,6 +72,15 @@ </MenuItem> <Separator/> + + <MenuItem + Header="I _printed this file" HotKey="Ctrl+P" InputGesture="Ctrl+P" + IsEnabled="{Binding IsFileLoaded}" + Command="{Binding IPrintedThisFile}"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\flask-16x16.png"/> + </MenuItem.Icon> + </MenuItem> <MenuItem Name="MainMenu.File.Extract" @@ -183,12 +192,24 @@ <Separator/> <MenuItem + Header="_Material manager" + HotKey="F10" + InputGesture="F10" + Command="{Binding MenuHelpMaterialManagerClicked}"> + <MenuItem.Icon> + <Image Source="\Assets\Icons\flask-16x16.png"/> + </MenuItem.Icon> + </MenuItem> + + <MenuItem Header="_Install profiles into PrusaSlicer" Command="{Binding MenuHelpInstallProfilesClicked}"> <MenuItem.Icon> <Image Source="\Assets\Icons\CNCMachine-16x16.png"/> </MenuItem.Icon> </MenuItem> + + <Separator/> <MenuItem Header="_Open settings folder" diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 90882ee..8553b3b 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -805,6 +805,11 @@ namespace UVtools.WPF App.StartProcess(UserSettings.SettingsFolder); } + private async void MenuHelpMaterialManagerClicked() + { + await new MaterialManagerWindow().ShowDialog(this); + } + public async void MenuHelpInstallProfilesClicked() { var PEFolder = App.GetPrusaSlicerDirectory(); @@ -818,7 +823,7 @@ namespace UVtools.WPF "Unable to detect PrusaSlicer") == ButtonResult.Yes) App.OpenBrowser("https://www.prusa3d.com/prusaslicer/"); return; } - await new PrusaSlicerManager().ShowDialog(this); + await new PrusaSlicerManagerWindow().ShowDialog(this); } public async void MenuNewVersionClicked() @@ -1149,6 +1154,8 @@ namespace UVtools.WPF ProgressWindow = new ProgressWindow(title); } + + private async void ConvertToOnTapped(object? sender, RoutedEventArgs e) { if (sender is not MenuItem item) return; @@ -1295,6 +1302,11 @@ namespace UVtools.WPF return task; } + public async void IPrintedThisFile() + { + await ShowRunOperation(typeof(OperationIPrintedThisFile)); + } + public async void ExtractFile() { if (!IsFileLoaded) return; @@ -1413,6 +1425,9 @@ namespace UVtools.WPF CanSave = true; return true; + case OperationIPrintedThisFile operation: + operation.Execute(); + return true; case OperationRepairLayers operation: if (Issues is null) { diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 65e1c9a..2a353c6 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -12,7 +12,7 @@ <PackageLicenseFile>LICENSE</PackageLicenseFile> <RepositoryUrl>https://github.com/sn4k3/UVtools</RepositoryUrl> <RepositoryType>Git</RepositoryType> - <Version>2.4.9</Version> + <Version>2.5.0</Version> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> @@ -75,4 +75,9 @@ </AvaloniaResource> <AvaloniaResource Include="Assets\Icons\*" /> </ItemGroup> + <ItemGroup> + <Compile Update="Windows\PrusaSlicerManagerWindow.axaml.cs"> + <DependentUpon>PrusaSlicerManagerWindow.axaml</DependentUpon> + </Compile> + </ItemGroup> </Project> diff --git a/UVtools.WPF/Windows/MaterialManagerWindow.axaml b/UVtools.WPF/Windows/MaterialManagerWindow.axaml new file mode 100644 index 0000000..52908af --- /dev/null +++ b/UVtools.WPF/Windows/MaterialManagerWindow.axaml @@ -0,0 +1,254 @@ +<controls:WindowEx xmlns="https://github.com/avaloniaui" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:controls="clr-namespace:UVtools.WPF.Controls" + mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="450" + x:Class="UVtools.WPF.Windows.MaterialManagerWindow" + Title="Material manager" + MinWidth="900" + MinHeight="600" + WindowStartupLocation="CenterOwner" + Icon="/Assets/Icons/UVtools.ico"> + + <Grid RowDefinitions="Auto,Auto,Auto,*"> + <Border Grid.Row="0" Padding="5"> + <Expander IsExpanded="True"> + <Expander.Header> + <TextBlock Text="Global statistics" + FontWeight="Bold" + Cursor="Hand"/> + </Expander.Header> + + <StackPanel> + <!--<Button VerticalAlignment="Center" + Command="{Binding RefreshStatistics}"> + <StackPanel Orientation="Horizontal" Spacing="10"> + <Image Source="/Assets/Icons/refresh-16x16.png"/> + <TextBlock Text="Refresh statistics"/> + </StackPanel> + </Button> + !--> + + <WrapPanel Orientation="Horizontal"> + <StackPanel Orientation="Horizontal"> + <TextBlock Text="Bottles in stock:" VerticalAlignment="Center" FontWeight="Bold"/> + <TextBox Classes="TransparentReadOnly" + Text="{Binding Manager.BottlesInStock, StringFormat=\{0:N0\}}"/> + </StackPanel> + + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock Text="Owned bottles:" VerticalAlignment="Center" FontWeight="Bold"/> + <TextBox Classes="TransparentReadOnly" + Text="{Binding Manager.OwnedBottles, StringFormat=\{0:N0\}}"/> + </StackPanel> + + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock Text="Consumed volume:" VerticalAlignment="Center" FontWeight="Bold"/> + <TextBox Classes="TransparentReadOnly" + Text="{Binding Manager.ConsumedVolumeLiters, StringFormat=\{0:N4\}}"/> + <TextBlock Text="liters" VerticalAlignment="Center"/> + </StackPanel> + + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock Text="Volume in stock:" VerticalAlignment="Center" FontWeight="Bold"/> + <TextBox Classes="TransparentReadOnly" + Text="{Binding Manager.VolumeInStockLiters, StringFormat=\{0:N4\}}"/> + <TextBlock Text="liters" VerticalAlignment="Center"/> + </StackPanel> + + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock Text="Spent:" VerticalAlignment="Center" FontWeight="Bold"/> + <TextBox Classes="TransparentReadOnly" + Text="{Binding Manager.TotalCost, StringFormat=\{0:N2\}}"/> + </StackPanel> + + <StackPanel Margin="10,0,0,0" Orientation="Horizontal"> + <TextBlock Text="Print time:" VerticalAlignment="Center" FontWeight="Bold"/> + <TextBox Classes="TransparentReadOnly" + Text="{Binding Manager.PrintTimeSpan.TotalDays, StringFormat=\{0:N4\}}"/> + <TextBlock Text="days" VerticalAlignment="Center"/> + </StackPanel> + </WrapPanel> + + </StackPanel> + + </Expander> + </Border> + + + <Border Grid.Row="1" BorderBrush="Black" BorderThickness="1" Padding="5"> + <Expander IsExpanded="False"> + <Expander.Header> + <TextBlock Text="Add new material" + FontWeight="Bold" + Cursor="Hand"/> + </Expander.Header> + + <Grid + RowDefinitions="Auto,10,Auto,10,Auto,10,Auto" + ColumnDefinitions="Auto,10,150,5,Auto,20,Auto,10,Auto,5,Auto,20,Auto,10,150,5,Auto,20,Auto,10,Auto,5,Auto"> + + <TextBlock Grid.Row="0" Grid.Column="0" + VerticalAlignment="Center" + Text="Name:"/> + + <TextBox Grid.Row="0" Grid.Column="2" Grid.ColumnSpan="17" + HorizontalAlignment="Stretch" + Watermark="A descriptive material name, eg: Epax Hard Grey" + Text="{Binding Material.Name}"/> + + <Button + Grid.Row="0" Grid.Column="20" Grid.ColumnSpan="3" + VerticalContentAlignment="Center" + HorizontalContentAlignment="Stretch" + VerticalAlignment="Stretch" + Command="{Binding AddNewMaterial}"> + <StackPanel Orientation="Horizontal" Spacing="10"> + <Image Source="/Assets/Icons/plus-16x16.png"/> + <TextBlock + VerticalAlignment="Center" + Text="Add new material"/> + </StackPanel> + </Button> + + <TextBlock Grid.Row="2" Grid.Column="0" + VerticalAlignment="Center" + ToolTip.Tip="Bottle material volume" + Text="Volume:"/> + + <NumericUpDown Grid.Row="2" Grid.Column="2" + Minimum="100" + Maximum="100000" + Increment="100" + Value="{Binding Material.BottleVolume}" + /> + <TextBlock Grid.Row="2" Grid.Column="4" + VerticalAlignment="Center" + Text="ml"/> + + <TextBlock Grid.Row="2" Grid.Column="6" + VerticalAlignment="Center" + ToolTip.Tip="Used to calculate weight and cost" + Text="Density:"/> + + <NumericUpDown Grid.Row="2" Grid.Column="8" + Minimum="0.1" + Maximum="10" + Increment="0.1" + Value="{Binding Material.Density}"/> + <TextBlock Grid.Row="2" Grid.Column="10" + VerticalAlignment="Center" + Text="g/ml"/> + + <TextBlock Grid.Row="2" Grid.Column="12" + VerticalAlignment="Center" + ToolTip.Tip="One bottle unit cost" + Text="Cost:"/> + + <NumericUpDown Grid.Row="2" Grid.Column="14" + Minimum="1" + Maximum="100000" + Increment="1" + Value="{Binding Material.BottleCost}"/> + <TextBlock Grid.Row="2" Grid.Column="16" + VerticalAlignment="Center" + Text=""/> + + <TextBlock Grid.Row="2" Grid.Column="18" + VerticalAlignment="Center" + ToolTip.Tip="Number of bottles in stock" + Text="Stock:"/> + + <NumericUpDown Grid.Row="2" Grid.Column="20" + Minimum="1" + Maximum="100000" + Increment="1" + Value="{Binding Material.BottlesInStock}"/> + <TextBlock Grid.Row="2" Grid.Column="22" + VerticalAlignment="Center" + Text="units"/> + </Grid> + </Expander> + </Border> + + <TextBlock Grid.Row="2" + VerticalAlignment="Center" FontWeight="Bold" + Margin="10,0,0,0" + Text="{Binding Manager.Count, StringFormat=Materials: {0}}"/> + + <StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Spacing="1"> + + <Button VerticalAlignment="Center" + IsEnabled="{Binding #MaterialsTable.SelectedItem, Converter={x:Static ObjectConverters.IsNotNull}}" + Command="{Binding RemoveSelectedMaterials}"> + <StackPanel Orientation="Horizontal" Spacing="10"> + <Image Source="/Assets/Icons/trash-16x16.png"/> + <TextBlock Text="Remove selected materials"/> + </StackPanel> + </Button> + + <Button VerticalAlignment="Center" + IsEnabled="{Binding Manager.Count}" + Command="{Binding ClearMaterials}"> + <StackPanel Orientation="Horizontal" Spacing="10"> + <Image Source="/Assets/Icons/delete-16x16.png"/> + <TextBlock Text="{Binding Manager.Count, StringFormat=Clear {0} materials}"/> + </StackPanel> + </Button> + + </StackPanel> + + + + <DataGrid Grid.Row="3" + Name="MaterialsTable" + CanUserReorderColumns="True" + CanUserResizeColumns="True" + CanUserSortColumns="True" + GridLinesVisibility="Horizontal" + ClipboardCopyMode="IncludeHeader" + VerticalAlignment="Stretch" + Items="{Binding Manager.Materials}"> + <DataGrid.Columns> + <DataGridTextColumn Header="Name" + Binding="{Binding Name}" + Width="Auto" /> + <DataGridTextColumn Header="Volume (ml)" + Binding="{Binding BottleVolume}" + Width="Auto" /> + <!-- + <DataGridTextColumn Header="Density (g/ml)" + Binding="{Binding Density}" + Width="Auto" />!--> + <DataGridTextColumn Header="Cost" + Binding="{Binding BottleCost}" + Width="Auto" /> + <DataGridTextColumn Header="In stock" + Binding="{Binding BottlesInStock}" + Width="Auto" /> + <DataGridTextColumn Header="Consumed bottles" + IsReadOnly="True" + Binding="{Binding ConsumedBottles, StringFormat=\{0:N0\}}" + Width="Auto" /> + <DataGridTextColumn Header="Spent" + IsReadOnly="True" + Binding="{Binding TotalCost, StringFormat=\{0:N2\}}" + Width="Auto" /> + <DataGridTextColumn Header="Remaining volume (ml)" + Binding="{Binding BottleRemainingVolume}" + Width="Auto" /> + <DataGridTextColumn Header="Consumed volume (l)" + IsReadOnly="True" + Binding="{Binding ConsumedVolumeLiters, StringFormat=\{0:N3\}}" + Width="Auto" /> + <DataGridTextColumn Header="Print time (days)" + IsReadOnly="True" + Binding="{Binding PrintTimeSpan.TotalDays, StringFormat=\{0:N4\}}" + Width="Auto" /> + </DataGrid.Columns> + + </DataGrid> + </Grid> + +</controls:WindowEx> diff --git a/UVtools.WPF/Windows/MaterialManagerWindow.axaml.cs b/UVtools.WPF/Windows/MaterialManagerWindow.axaml.cs new file mode 100644 index 0000000..a519d34 --- /dev/null +++ b/UVtools.WPF/Windows/MaterialManagerWindow.axaml.cs @@ -0,0 +1,97 @@ +using System; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using DynamicData; +using MessageBox.Avalonia.Enums; +using UVtools.Core.Managers; +using UVtools.Core.Objects; +using UVtools.WPF.Controls; +using UVtools.WPF.Extensions; + +namespace UVtools.WPF.Windows +{ + public class MaterialManagerWindow : WindowEx + { + private Material _material = new(); + private readonly DataGrid _grid; + public MaterialManager Manager => MaterialManager.Instance; + + public Material Material + { + get => _material; + set => RaiseAndSetIfChanged(ref _material, value); + } + + public MaterialManagerWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + + _grid = this.FindControl<DataGrid>("MaterialsTable"); + + MaterialManager.Load(); // Reload + + DataContext = this; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + protected override void OnClosed(EventArgs e) + { + base.OnClosed(e); + MaterialManager.Save(); // Apply changes + } + + public void RefreshStatistics() + { + Manager.RaisePropertiesChanged(); + } + + public async void AddNewMaterial() + { + if (string.IsNullOrWhiteSpace(Material.Name)) + { + await this.MessageBoxError("Material name can't be empty"); + return; + } + + if (Manager.Contains(Material)) + { + await this.MessageBoxError("A material with same name already exists."); + return; + } + + Material.BottleRemainingVolume = Material.BottleVolume; + + if (await this.MessageBoxQuestion("Are you sure you want to add the following material:\n" + + $"{Material}") != ButtonResult.Yes) return; + + Manager.Add(Material); + Manager.SortByName(); + MaterialManager.Save(); + Material = new(); + } + + public async void RemoveSelectedMaterials() + { + if (_grid.SelectedItems.Count <= 0) return; + if (await this.MessageBoxQuestion($"Are you sure you want to remove {_grid.SelectedItems.Count} materials?") != ButtonResult.Yes) return; + Manager.RemoveMany(_grid.SelectedItems.Cast<Material>()); + MaterialManager.Save(); + } + + public async void ClearMaterials() + { + if (Manager.Count == 0) return; + if (await this.MessageBoxQuestion($"Are you sure you want to clear {Manager.Count} materials?") != ButtonResult.Yes) return; + Manager.Clear(true); + } + } +} diff --git a/UVtools.WPF/Windows/PrusaSlicerManager.axaml b/UVtools.WPF/Windows/PrusaSlicerManagerWindow.axaml index e9d6b1c..72184b1 100644 --- a/UVtools.WPF/Windows/PrusaSlicerManager.axaml +++ b/UVtools.WPF/Windows/PrusaSlicerManagerWindow.axaml @@ -4,7 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:UVtools.WPF.Controls" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="700" - x:Class="UVtools.WPF.Windows.PrusaSlicerManager" + x:Class="UVtools.WPF.Windows.PrusaSlicerManagerWindow" Title="Install profiles into PrusaSlicer" Width="900" MinWidth="900" diff --git a/UVtools.WPF/Windows/PrusaSlicerManager.axaml.cs b/UVtools.WPF/Windows/PrusaSlicerManagerWindow.axaml.cs index ae24b3c..4b30c9c 100644 --- a/UVtools.WPF/Windows/PrusaSlicerManager.axaml.cs +++ b/UVtools.WPF/Windows/PrusaSlicerManagerWindow.axaml.cs @@ -10,11 +10,11 @@ using UVtools.WPF.Structures; namespace UVtools.WPF.Windows { - public class PrusaSlicerManager : WindowEx + public class PrusaSlicerManagerWindow : WindowEx { public PEProfileFolder[] Profiles { get;} - public PrusaSlicerManager() + public PrusaSlicerManagerWindow() { InitializeComponent(); Profiles = new[] |