diff --git a/README.md b/README.md index 1bb9916..ca38324 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,11 @@ The UVtools executable allow to set some arguments to do special functions: - **Example 2:** UVtools -e model.zip mymodel - **Example 3:** UVtools --extract model.zip . - **Note:** Nothing happen when providing wrong files/folder, will quit. +- **Export a file to a 3D mesh** + - **Syntax:** UVtools --export-mesh \ [output_mesh_file] + - **Example 1:** UVtools --export-mesh model.zip + - **Example 2:** UVtools --export-mesh model.zip model_exported.stl + - **Note:** Nothing happen when providing wrong files, will quit. # Requirements diff --git a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj index e7ba28e..61d2ff2 100644 --- a/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj +++ b/UVtools.AvaloniaControls/UVtools.AvaloniaControls.csproj @@ -34,7 +34,7 @@ - + diff --git a/UVtools.Core/Extensions/FileStreamExtensions.cs b/UVtools.Core/Extensions/FileStreamExtensions.cs index 8944a48..d287640 100644 --- a/UVtools.Core/Extensions/FileStreamExtensions.cs +++ b/UVtools.Core/Extensions/FileStreamExtensions.cs @@ -106,6 +106,16 @@ namespace UVtools.Core.Extensions return (uint)bytes.Length; } + public static uint WriteLine(this FileStream fs, string text, int offset = 0) + => fs.WriteLine(text, Encoding.UTF8, offset); + + public static uint WriteLine(this FileStream fs, string text, Encoding encoding, int offset = 0) + { + var bytes = encoding.GetBytes(text+Environment.NewLine); + fs.Write(bytes, offset, bytes.Length); + return (uint)bytes.Length; + } + public static uint WriteSerialize(this FileStream fs, object data, int offset = 0) { return Helpers.SerializeWriteFileStream(fs, data, offset); diff --git a/UVtools.Core/MeshFormats/MeshFile.cs b/UVtools.Core/MeshFormats/MeshFile.cs index 1b3fe75..68fc7e3 100644 --- a/UVtools.Core/MeshFormats/MeshFile.cs +++ b/UVtools.Core/MeshFormats/MeshFile.cs @@ -1,21 +1,111 @@ -using System; -using System.Collections.Generic; +/* + * 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 System; +using System.IO; using System.Linq; using System.Numerics; -using System.Text; -using System.Threading.Tasks; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; namespace UVtools.Core.MeshFormats { - public abstract class MeshFile + public abstract class MeshFile : IDisposable { - public string FilePath; - public MeshFile(string filePath) + #region Static + public static readonly FileExtension[] AvailableMeshFiles = + { + STLMeshFile.FileExtension + }; + + public static FileExtension FindFileExtension(string filePath) + { + var ext = Path.GetExtension(filePath); + return AvailableMeshFiles.FirstOrDefault(fileExtension => $".{fileExtension.Extension}" == ext); + } + + public static MeshFile CreateInstance(string filePath, FileMode fileMode, MeshFileFormat fileFormat = MeshFileFormat.BINARY) + { + var fileExtension = FindFileExtension(filePath); + return fileExtension?.FileFormatType.CreateInstance(filePath, fileMode, fileFormat); + } + + #endregion + + #region Enums + + public enum MeshFileFormat : byte + { + ASCII, + BINARY + } + + #endregion + + #region Properties + + /// + /// Gets the file format for this mesh + /// + public MeshFileFormat FileFormat { get; } = MeshFileFormat.BINARY; + + /// + /// Gets the file path of the stream + /// + public string FilePath { get; } + + /// + /// Gets the current file stream + /// + public FileStream MeshStream { get; } + + /// + /// Gets the number of triangles + /// + public uint TriangleCount { get; protected set; } + #endregion + + + #region Constructor + protected MeshFile(string filePath, FileMode fileMode, MeshFileFormat meshFileFormat = MeshFileFormat.BINARY) { FilePath = filePath; + FileFormat = meshFileFormat; + MeshStream = new FileStream(filePath, fileMode); } - public abstract void Create(); + #endregion + + #region Methods + /// + /// Call once before write content to the file, use this to build up the header if any + /// + public virtual void BeginWrite(){} + + /// + /// Writes an triangle to the file + /// + /// + /// + /// + /// public abstract void WriteTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 normal); - public abstract void Close(); + + /// + /// Call once before close the file, use this to build up the footer if any + /// + public virtual void EndWrite(){} + + + /// + public void Dispose() + { + MeshStream?.Dispose(); + } + #endregion } } diff --git a/UVtools.Core/MeshFormats/STLMeshFile.cs b/UVtools.Core/MeshFormats/STLMeshFile.cs index 0aa635c..3e8500d 100644 --- a/UVtools.Core/MeshFormats/STLMeshFile.cs +++ b/UVtools.Core/MeshFormats/STLMeshFile.cs @@ -1,101 +1,112 @@ -using System; +/* + * 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 System; using System.IO; using System.Numerics; using System.Text; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; namespace UVtools.Core.MeshFormats { - public enum STLFormat - { - ASCII, BINARY - } public class STLMeshFile : MeshFile { - STLFormat outputFormat; - string objectName; - FileStream stream; - StreamWriter asciiWriter; - uint triangleCount; - public STLMeshFile(string filePath, STLFormat format, string name = null) : base(filePath) + #region Constants + public const string DefaultObjectName = "UVTools STL Object"; + //public const string DefaultHeader = "STL File Generated by UVTools"; + #endregion + + #region Properties + public static FileExtension FileExtension => new(typeof(STLMeshFile), "stl", "Standard Triangle Language"); + + public string ObjectName { get; } = DefaultObjectName; + #endregion + + #region Constructor + public STLMeshFile(string filePath, FileMode fileMode, MeshFileFormat fileFormat, string name) : base(filePath, fileMode, fileFormat) { - outputFormat = format; - objectName = name ?? "UVTools STL Object"; + ObjectName = name ?? DefaultObjectName; } - public override void Create() + public STLMeshFile(string filePath, FileMode fileMode, MeshFileFormat fileFormat) : this(filePath, fileMode, fileFormat, DefaultObjectName) { } + + public STLMeshFile(string filePath, FileMode fileMode) : this(filePath, fileMode, MeshFileFormat.BINARY, DefaultObjectName) { } + + + #endregion + + #region Methods + public override void BeginWrite() { - stream = File.Create(FilePath); - - if (outputFormat == STLFormat.ASCII) + if (FileFormat == MeshFileFormat.ASCII) { - asciiWriter = new StreamWriter(stream); - - asciiWriter.WriteLine($"solid \"{objectName}\""); + MeshStream.WriteLine($"solid \"{ObjectName}\""); } else { - byte[] header = new byte[80]; - byte[] headerText = UTF8Encoding.UTF8.GetBytes("STL File Generated by UVTools"); + var header = new byte[80]; + var headerText = Encoding.UTF8.GetBytes($"{About.Software} v{About.VersionStr}"); Array.Copy(headerText, header, headerText.Length); - stream.Write(header); - stream.Position += 4; + MeshStream.Write(header); + MeshStream.Seek(4, SeekOrigin.Current); } } public override void WriteTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 normal) { - if (outputFormat == STLFormat.ASCII) + if (FileFormat == MeshFileFormat.ASCII) { - asciiWriter.WriteLine($" facet normal {normal.X} {normal.Y} {normal.Z}"); - asciiWriter.WriteLine(" outer loop"); - - asciiWriter.WriteLine($" vertex {p1.X.ToString("E11")} {p1.Y.ToString("E11")} {p1.Z.ToString("E11")}"); - asciiWriter.WriteLine($" vertex {p2.X.ToString("E11")} {p2.Y.ToString("E11")} {p2.Z.ToString("E11")}"); - asciiWriter.WriteLine($" vertex {p3.X.ToString("E11")} {p3.Y.ToString("E11")} {p3.Z.ToString("E11")}"); - - asciiWriter.WriteLine(" endloop"); - asciiWriter.WriteLine(" endfacet"); - } else + MeshStream.WriteLine($" facet normal {normal.X} {normal.Y} {normal.Z}"); + MeshStream.WriteLine(" outer loop"); + MeshStream.WriteLine($" vertex {p1.X:E11} {p1.Y:E11} {p1.Z:E11}"); + MeshStream.WriteLine($" vertex {p2.X:E11} {p2.Y:E11} {p2.Z:E11}"); + MeshStream.WriteLine($" vertex {p3.X:E11} {p3.Y:E11} {p3.Z:E11}"); + MeshStream.WriteLine(" endloop"); + MeshStream.WriteLine(" endfacet"); + } + else { - triangleCount++; + MeshStream.Write(BitConverter.GetBytes(normal.X)); + MeshStream.Write(BitConverter.GetBytes(normal.Y)); + MeshStream.Write(BitConverter.GetBytes(normal.Z)); - stream.Write(BitConverter.GetBytes(normal.X)); - stream.Write(BitConverter.GetBytes(normal.Y)); - stream.Write(BitConverter.GetBytes(normal.Z)); + MeshStream.Write(BitConverter.GetBytes(p1.X)); + MeshStream.Write(BitConverter.GetBytes(p1.Y)); + MeshStream.Write(BitConverter.GetBytes(p1.Z)); - stream.Write(BitConverter.GetBytes(p1.X)); - stream.Write(BitConverter.GetBytes(p1.Y)); - stream.Write(BitConverter.GetBytes(p1.Z)); + MeshStream.Write(BitConverter.GetBytes(p2.X)); + MeshStream.Write(BitConverter.GetBytes(p2.Y)); + MeshStream.Write(BitConverter.GetBytes(p2.Z)); - stream.Write(BitConverter.GetBytes(p2.X)); - stream.Write(BitConverter.GetBytes(p2.Y)); - stream.Write(BitConverter.GetBytes(p2.Z)); - - stream.Write(BitConverter.GetBytes(p3.X)); - stream.Write(BitConverter.GetBytes(p3.Y)); - stream.Write(BitConverter.GetBytes(p3.Z)); - - stream.Write(new byte[2]); + MeshStream.Write(BitConverter.GetBytes(p3.X)); + MeshStream.Write(BitConverter.GetBytes(p3.Y)); + MeshStream.Write(BitConverter.GetBytes(p3.Z)); + + MeshStream.Write(new byte[2]); } + + TriangleCount++; } - public override void Close() + public override void EndWrite() { - if (asciiWriter is not null) + if (FileFormat == MeshFileFormat.ASCII) { - asciiWriter.WriteLine($"endsolid \"{objectName}\""); - asciiWriter.Flush(); - asciiWriter.Close(); - } else + MeshStream.WriteLine($"endsolid \"{ObjectName}\""); + } + else { - stream.Position = 80; - stream.Write(BitConverter.GetBytes(triangleCount)); - stream.Flush(); - stream.Close(); + MeshStream.Seek(80, SeekOrigin.Begin); + MeshStream.WriteUIntLittleEndian(TriangleCount); } + MeshStream.Flush(); } - - + #endregion } } diff --git a/UVtools.Core/Operations/OperationLayerExportGif.cs b/UVtools.Core/Operations/OperationLayerExportGif.cs index ca14254..039f6d0 100644 --- a/UVtools.Core/Operations/OperationLayerExportGif.cs +++ b/UVtools.Core/Operations/OperationLayerExportGif.cs @@ -185,7 +185,6 @@ namespace UVtools.Core.Operations public OperationLayerExportGif(FileFormat slicerFile) : base(slicerFile) { - _filePath = SlicerFile.FileFullPath + ".gif"; _flipDirection = SlicerFile.DisplayMirror; _skip = TotalLayers switch { @@ -199,6 +198,11 @@ namespace UVtools.Core.Operations }*/ } + public override void InitWithSlicerFile() + { + _filePath = SlicerFile.FileFullPath + ".gif"; + } + #endregion #region Methods diff --git a/UVtools.Core/Operations/OperationLayerExportHeatMap.cs b/UVtools.Core/Operations/OperationLayerExportHeatMap.cs index 0947938..a883389 100644 --- a/UVtools.Core/Operations/OperationLayerExportHeatMap.cs +++ b/UVtools.Core/Operations/OperationLayerExportHeatMap.cs @@ -101,10 +101,14 @@ namespace UVtools.Core.Operations public OperationLayerExportHeatMap(FileFormat slicerFile) : base(slicerFile) { - _filePath = SlicerFile.FileFullPath + ".heatmap.png"; _flipDirection = SlicerFile.DisplayMirror; } + public override void InitWithSlicerFile() + { + _filePath = SlicerFile.FileFullPath + ".heatmap.png"; + } + #endregion #region Methods diff --git a/UVtools.Core/Operations/OperationLayerExportImage.cs b/UVtools.Core/Operations/OperationLayerExportImage.cs index 9e52fb2..fa06e97 100644 --- a/UVtools.Core/Operations/OperationLayerExportImage.cs +++ b/UVtools.Core/Operations/OperationLayerExportImage.cs @@ -158,10 +158,14 @@ namespace UVtools.Core.Operations public OperationLayerExportImage(FileFormat slicerFile) : base(slicerFile) { - _outputFolder = Path.Combine(Path.GetDirectoryName(SlicerFile.FileFullPath) ?? string.Empty, FileFormat.GetFileNameStripExtensions(SlicerFile.FileFullPath)); _flipDirection = SlicerFile.DisplayMirror; } + public override void InitWithSlicerFile() + { + _outputFolder = Path.Combine(Path.GetDirectoryName(SlicerFile.FileFullPath) ?? string.Empty, FileFormat.GetFileNameStripExtensions(SlicerFile.FileFullPath)); + } + #endregion #region Methods diff --git a/UVtools.Core/Operations/OperationLayerExportMesh.cs b/UVtools.Core/Operations/OperationLayerExportMesh.cs new file mode 100644 index 0000000..4afbf21 --- /dev/null +++ b/UVtools.Core/Operations/OperationLayerExportMesh.cs @@ -0,0 +1,636 @@ +/* + * 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 System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Emgu.CV; +using Emgu.CV.CvEnum; +using KdTree; +using KdTree.Math; +using UVtools.Core.Extensions; +using UVtools.Core.FileFormats; +using UVtools.Core.MeshFormats; +using UVtools.Core.Voxel; + +namespace UVtools.Core.Operations +{ + [Serializable] + public sealed class OperationLayerExportMesh : Operation + { + #region MyRegion + public enum ExportMeshQuality : byte + { + Accurate = 1, + Average = 2, + Quick = 3, + + Dirty = 6, + Minecraft = 8 + } + + + #endregion + + + #region Members + private string _filePath; + private ExportMeshQuality _quality = ExportMeshQuality.Accurate; + private Enumerations.RotateDirection _rotateDirection = Enumerations.RotateDirection.None; + private Enumerations.FlipDirection _flipDirection = Enumerations.FlipDirection.None; + #endregion + + #region Overrides + + public override bool CanHaveProfiles => false; + public override string Title => "Export layers to mesh"; + + public override string Description => + "Reconstructs and export a layer range to a 3D mesh via voxelization.\n" + + "Note: Depending on quality and triangle count, this will often render heavy files.\n" + + "This process will not recover your original 3D model as data was already lost when sliced."; + + public override string ConfirmationText => + $"generate a mesh from layers {LayerIndexStart} through {LayerIndexEnd}?"; + + public override string ProgressTitle => + $"Generating a mesh from layers {LayerIndexStart} through {LayerIndexEnd}"; + + public override string ProgressAction => "Packed layers"; + public override string ValidateInternally() + { + var sb = new StringBuilder(); + + if (MeshFile.FindFileExtension(_filePath) is null) + { + sb.AppendLine("The used file extension is invalid."); + } + + return sb.ToString(); + } + + /*public override string ToString() + { + var result = $"[Crop by ROI: {_cropByRoi}]" + + LayerRangeString; + if (!string.IsNullOrEmpty(ProfileName)) result = $"{ProfileName}: {result}"; + return result; + }*/ + + #endregion + + #region Properties + + public string FilePath + { + get => _filePath; + set => RaiseAndSetIfChanged(ref _filePath, value); + } + + public ExportMeshQuality Quality + { + get => _quality; + set => RaiseAndSetIfChanged(ref _quality, value); + } + + public Enumerations.RotateDirection RotateDirection + { + get => _rotateDirection; + set => RaiseAndSetIfChanged(ref _rotateDirection, value); + } + + public Enumerations.FlipDirection FlipDirection + { + get => _flipDirection; + set => RaiseAndSetIfChanged(ref _flipDirection, value); + } + + #endregion + + #region Constructor + + public OperationLayerExportMesh() { } + + public OperationLayerExportMesh(FileFormat slicerFile) : base(slicerFile) + { + _flipDirection = SlicerFile.DisplayMirror; + } + + public override void InitWithSlicerFile() + { + _filePath = Path.Combine(Path.GetDirectoryName(SlicerFile.FileFullPath), $"{SlicerFile.FilenameNoExt}.stl"); + } + + #endregion + + #region Methods + + protected override unsafe bool ExecuteInternally(OperationProgress progress) + { + var fileExtension = MeshFile.FindFileExtension(_filePath); + if (fileExtension is null) return false; + + //using var meshFile = fileExtension.FileFormatType.CreateInstance(_filePath, FileMode.Create); + //new Voxelizer().CreateVoxelMesh(fileExtension.FileFormatType, SlicerFile, _filePath, progress); + + + /* Voxelization has 4 overall stages + * 1.) Generate all visible faces, this is for each pixel we determine which of its faces are visible from outside the model + * 2.) Collapse faces horizontally, this combines faces that are coplanar horizontally into a longer face, this reduces triangles + * 3.) Collapse faces that are coplanar and the same size vertically leveraging KD Trees for fast lookups, O(logn) vs O(n) for a normal list + * 4.) Generate triangles for faces and write out to STL + */ + + /* Basic information for the file, how many layers, how big should each voxel be) */ + var pixelSize = SlicerFile.PixelSize; + float xWidth = (pixelSize.Width > 0 ? pixelSize.Width : 0.035f) * (byte)_quality; + float yWidth = (pixelSize.Height > 0 ? pixelSize.Height : 0.035f) * (byte)_quality; + var zHeight = SlicerFile.LayerHeight; + + + /* For the 1st stage, we maintain up to 3 mats, the current layer, the one below us, and the one above us + * (below will be null when current layer is 0, above will be null when currentlayer is layercount-1) */ + /* We init the aboveLayer to the first layer, in the loop coming up we shift above->current->below, so this effectively inits current layer */ + Mat aboveLayer = null; + using (var mat = SlicerFile[LayerIndexStart].LayerMat) + { + var matRoi = mat.Roi(SlicerFile.BoundingRectangle); + + if (_flipDirection != Enumerations.FlipDirection.None) + { + CvInvoke.Flip(matRoi, matRoi, Enumerations.ToOpenCVFlipType(_flipDirection)); + } + + if (_rotateDirection != Enumerations.RotateDirection.None) + { + CvInvoke.Rotate(matRoi, matRoi, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); + } + + if ((byte)_quality > 1) + { + aboveLayer = new Mat(); + CvInvoke.Resize(matRoi, aboveLayer, Size.Empty, 1.0 / (int)_quality, 1.0 / (int)_quality, Inter.Area); + } + else + { + aboveLayer = matRoi.Clone(); /* clone and then dispose of the ROI mat, not efficient but keeps the GetPixelPos working and clean */ + } + } + + Mat curLayer = null; + Mat belowLayer = null; + + /* List of faces to process, great for debugging if you are haveing issues with a face of particular orientation. */ + var facesToCheck = new[] { Voxelizer.FaceOrientation.Front, Voxelizer.FaceOrientation.Back, Voxelizer.FaceOrientation.Left, Voxelizer.FaceOrientation.Right, Voxelizer.FaceOrientation.Top, Voxelizer.FaceOrientation.Bottom }; + + /* Init of other objects that will be used in subsequent stages */ + var rootFaces = new Voxelizer.UVFace[LayerRangeCount]; + var layerFaceCounts = new uint[LayerRangeCount]; + var layerTrees = new KdTree[LayerRangeCount]; + + progress.Reset("layers", LayerRangeCount); + progress.Title = "Stage 1: Generating faces from layers"; + //progress.ItemCount = LayerRangeCount; + + /* Begin Stage 1, identifying all faces that are visible from outside the model */ + for (uint treeIndex = 0; treeIndex < LayerRangeCount; treeIndex++) + { + var layerIndex = LayerIndexStart + treeIndex; + Voxelizer.UVFace currentFaceItem = null; + + /* Should contain a list of all found faces on this layer, keyed by the face orientation */ + var foundFaces = new Dictionary>(); + + /* move current layer to below */ + belowLayer = curLayer; + + /* move above layer to us */ + curLayer = aboveLayer; + + /* bring in a new aboveLayer if we need to */ + if (layerIndex < LayerIndexEnd) + { + using var mat = SlicerFile[layerIndex + 1].LayerMat; + var matRoi = mat.Roi(SlicerFile.BoundingRectangle); + + if (_flipDirection != Enumerations.FlipDirection.None) + { + CvInvoke.Flip(matRoi, matRoi, Enumerations.ToOpenCVFlipType(_flipDirection)); + } + + if (_rotateDirection != Enumerations.RotateDirection.None) + { + CvInvoke.Rotate(matRoi, matRoi, Enumerations.ToOpenCVRotateFlags(_rotateDirection)); + } + + if ((byte)_quality > 1) + { + CvInvoke.Resize(matRoi, aboveLayer, new Size(), 1.0 / (int)_quality, 1.0 / (int)_quality, Inter.Area); + } + else + { + aboveLayer = matRoi.Clone(); + } + + //CvInvoke.Threshold(aboveLayer, aboveLayer, 1, 255, ThresholdType.Binary); + } + else + { + aboveLayer = null; + } + + /* get image of pixels to do neighbor checks on */ + var voxelLayer = Voxelizer.BuildVoxelLayerImage(curLayer, aboveLayer, belowLayer); + var voxelSpan = voxelLayer.GetBytePointer(); + + /* Seems to be faster to parallel on the Y and not the X */ + Parallel.For(0, curLayer.Height, CoreSettings.ParallelOptions, y => + { + /* Collects all the faces found for this thread, will be combined into the main dictionary later */ + var threadDict = new Dictionary>(); + for (var x = 0; x < curLayer.Width; x++) + { + if (voxelSpan[voxelLayer.GetPixelPos(x, y)] == 0) continue; + + var faces = Voxelizer.GetOpenFaces(curLayer, x, y, belowLayer, aboveLayer); + if (faces == Voxelizer.FaceOrientation.None) continue; + foreach (var face in facesToCheck) + { + if (!faces.HasFlag(face)) continue; + if (!threadDict.ContainsKey(face)) threadDict.Add(face, new()); + threadDict[face].Add(new Point(x, y)); + } + } + + /* merge all found faces to main foundFaces dictionary */ + lock (foundFaces) + { + foreach (var kvp in threadDict) + { + if (!foundFaces.ContainsKey(kvp.Key)) foundFaces.Add(kvp.Key, new()); + lock (foundFaces[kvp.Key]) foundFaces[kvp.Key].AddRange(kvp.Value); + } + } + }); + + /* Begin stage 2, horizontal combining of coplanar faces */ + foreach (var faceType in facesToCheck) + { + if (foundFaces.ContainsKey(faceType) == false || foundFaces[faceType].Count == 0) continue; + + if (faceType + is Voxelizer.FaceOrientation.Front + or Voxelizer.FaceOrientation.Back + or Voxelizer.FaceOrientation.Top + or Voxelizer.FaceOrientation.Bottom) + { + /* sort the faces by coordinate */ + foundFaces[faceType] = foundFaces[faceType].OrderBy(f => f.Y).ThenBy(f => f.X).ToList(); + + var startX = foundFaces[faceType][0].X; + var curX = foundFaces[faceType][0].X; + var startY = foundFaces[faceType][0].Y; + var curY = foundFaces[faceType][0].Y; + + foreach (var f in foundFaces[faceType].Skip(1)) + { + if (f.Y == curY) + { + /* same row...*/ + if (f.X == curX + 1) + { + /* this face is adjecent to the previous, just increase the "width" */ + curX++; + } + else + { + /* This face is disconnected by at least 1 pixel from the chain we've been building */ + /* Create a UVFace for the current chain and reset to this one */ + layerFaceCounts[treeIndex]++; + if (currentFaceItem is null) + { + rootFaces[treeIndex] = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) }; + currentFaceItem = rootFaces[treeIndex]; + } + else + { + currentFaceItem.FlatListNext = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) }; + currentFaceItem = currentFaceItem.FlatListNext; + } + //faceTree.Add(new float[] { (float)faceType, startX, startY, layerIndex }, new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) }); + /* disconnected */ + startX = f.X; + curX = f.X; + + } + } + else + { + /* this face isn't on the same Y row as previous, therefore it is disconnected. */ + /* Create a UVFace for the current chain and reset to this one */ + layerFaceCounts[treeIndex]++; + if (currentFaceItem is null) + { + rootFaces[treeIndex] = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) }; + currentFaceItem = rootFaces[treeIndex]; + } + else + { + currentFaceItem.FlatListNext = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) }; + currentFaceItem = currentFaceItem.FlatListNext; + } + startY = f.Y; + curY = f.Y; + startX = f.X; + curX = f.X; + } + } + /* we've gone through all the faces, add the final chain we've been building */ + /* Create a UVFace for the final chain */ + layerFaceCounts[treeIndex]++; + if (currentFaceItem is null) + { + rootFaces[treeIndex] = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) }; + currentFaceItem = rootFaces[treeIndex]; + } + else + { + currentFaceItem.FlatListNext = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) }; + currentFaceItem = currentFaceItem.FlatListNext; + } + } + + if (faceType is Voxelizer.FaceOrientation.Left or Voxelizer.FaceOrientation.Right) + { + /* sort the faces by coordinate */ + foundFaces[faceType] = foundFaces[faceType].OrderBy(f => f.X).ThenBy(f => f.Y).ToList(); + + var startX = foundFaces[faceType][0].X; + var curX = foundFaces[faceType][0].X; + var startY = foundFaces[faceType][0].Y; + var curY = foundFaces[faceType][0].Y; + foreach (var f in foundFaces[faceType].Skip(1)) + { + if (f.X == curX) + { + /* same column...*/ + if (f.Y == curY + 1) + { + /* this face is adjecent to the previous, just increase the "width" */ + curY++; + } + else + { + /* This face is disconnected by at least 1 pixel from the chain we've been building */ + /* Create a UVFace for the current chain and reset to this one */ + layerFaceCounts[treeIndex]++; + if (currentFaceItem is null) + { + rootFaces[treeIndex] = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) }; + currentFaceItem = rootFaces[treeIndex]; + } + else + { + currentFaceItem.FlatListNext = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) }; + currentFaceItem = currentFaceItem.FlatListNext; + } + startY = f.Y; + curY = f.Y; + } + } + else + { + /* this face is on a different column, cannot be part of the current chain we're building */ + /* Create a UVFace for the current chain and reset to this one */ + layerFaceCounts[treeIndex]++; + if (currentFaceItem is null) + { + rootFaces[treeIndex] = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) }; + currentFaceItem = rootFaces[treeIndex]; + } + else + { + currentFaceItem.FlatListNext = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) }; + currentFaceItem = currentFaceItem.FlatListNext; + } + startY = f.Y; + curY = f.Y; + startX = f.X; + curX = f.X; + } + } + layerFaceCounts[treeIndex]++; + if (currentFaceItem is null) + { + rootFaces[treeIndex] = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) }; + currentFaceItem = rootFaces[treeIndex]; + } + else + { + currentFaceItem.FlatListNext = new Voxelizer.UVFace { LayerIndex = treeIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) }; + currentFaceItem = currentFaceItem.FlatListNext; + } + } + } + + progress.LockAndIncrement(); + + if (progress.Token.IsCancellationRequested) + { + Cleanup(); + return false; + } + + } + + progress.Title = "Stage 2: Building KD Trees"; + progress.ProcessedItems = 0; + + /* We build out a 3 dimensional KD tree for each layer, having 1 big KD tree is prohibitive when you get to millions and millions of faces. */ + Parallel.For(0, LayerRangeCount, layerIndex => + { + if (progress.Token.IsCancellationRequested) return; + + /* Create the KD tree for the layer, in practice there should never be dups, but just in case, set to skip */ + layerTrees[layerIndex] = new KdTree(3, new FloatMath(), AddDuplicateBehavior.Skip); + + /* Walk the linked list of UVFaces, adding them to the tree */ + var currentFaceItem = rootFaces[layerIndex]; + if (currentFaceItem is null) return; + while (currentFaceItem.FlatListNext is not null) + { + layerTrees[layerIndex].Add(new[] { (float)currentFaceItem.Type, currentFaceItem.FaceRect.X, currentFaceItem.FaceRect.Y }, currentFaceItem); + currentFaceItem = currentFaceItem.FlatListNext; + } + layerTrees[layerIndex].Add(new[] { (float)currentFaceItem.Type, currentFaceItem.FaceRect.X, currentFaceItem.FaceRect.Y }, currentFaceItem); + + progress.LockAndIncrement(); + }); + + if (progress.Token.IsCancellationRequested) + { + Cleanup(); + return false; + } + + progress.Title = "Stage 3: Collapsing faces"; + progress.ProcessedItems = 0; + long collapseCount = 0; + + /* Begin Stage 3: Vertical collapse + * Since we don't modify the lists/objects and only connect them via doubly linked list + * we can process each layer independant of the others. + */ + Parallel.For(0, LayerRangeCount, i => + { + if (progress.Token.IsCancellationRequested) + { + return; + } + + /* if no faces on this layer... skip.... needed for empty layers */ + if (layerTrees[i] is null) return; + + /* check each point in the current layers tree */ + foreach (var point in layerTrees[i]) + { + /* if this point already has a parent, skip */ + if (point.Value.Parent is not null) continue; + + + /* deterimine the point below to check. + * For front/back/left/right its the same X/Y point and Z is different, and Z is done basically by looking at the layer tree below us + * For Top/Bottom its a bit different, the Z stays the same (we query our own layer tree) but the Y coordinate is 1 less */ + + float[] pointBelow = null; + KdTree treeBelow = null; + if (point.Value.Type is Voxelizer.FaceOrientation.Top or Voxelizer.FaceOrientation.Bottom) + { + if (point.Value.Type == Voxelizer.FaceOrientation.Top) + { + pointBelow = new[] { point.Point[0], point.Point[1], point.Point[2] - 1 }; + } + else + { + pointBelow = new[] { point.Point[0], point.Point[1], point.Point[2] - 1 }; + } + treeBelow = layerTrees[i]; + } + else + { + pointBelow = new[] { point.Point[0], point.Point[1], point.Point[2] }; + if (i > 0) + { + treeBelow = layerTrees[i - 1]; + } + } + + var faceBelow = treeBelow?.FindValueAt(pointBelow); + if (faceBelow is null) continue; + /* if we find a face below us it has to be the same width too */ + if (point.Value.FaceRect.Width == faceBelow.FaceRect.Width) + { + /* same coordinate, same width, safe to merge together. Do so by doubly linking the items */ + point.Value.Parent = faceBelow; + faceBelow.Child = point.Value; + collapseCount++; + } + } + progress.LockAndIncrement(); + }); + + if (progress.Token.IsCancellationRequested) + { + Cleanup(); + return false; + } + + progress.Title = "Stage 4: Generating STL"; + progress.ProcessedItems = 0; + + + using var mesh = fileExtension.FileFormatType.CreateInstance(_filePath, FileMode.Create); + mesh.BeginWrite(); + + /* Begin Stage 4, generating triangles and saving to STL */ + foreach (var tree in layerTrees) + { + if (tree is null) continue; + + /* only process UVFaces that do not have a parent, these are the "root" faces that couldn't be combined with something above them */ + foreach (var p in tree.Where(p => p.Value.Parent is null)) + { + /* generate the triangles */ + foreach (var f in Voxelizer.MakeFacetsForUVFace(p.Value, xWidth, yWidth, zHeight, LayerIndexStart)) + { + /* write to file */ + mesh.WriteTriangle(f.p1, f.p2, f.p3, f.normal); + } + } + + /* check for cancellation at every layer, and if so, close the STL file properly */ + if (progress.Token.IsCancellationRequested) + { + Cleanup(); + return false; + } + progress.LockAndIncrement(); + } + + void Cleanup() + { + /* dispose of everything */ + for (var x = 0; x < layerTrees.Length; x++) + { + layerTrees[x] = null; + } + + layerTrees = null; + + for (var x = 0; x < rootFaces.Length; x++) + { + if (rootFaces[x] is not null) rootFaces[x].FlatListNext = null; + rootFaces[x] = null; + } + rootFaces = null; + GC.Collect(); + } + + mesh.EndWrite(); + + + return !progress.Token.IsCancellationRequested; + } + + + #endregion + + #region Equality + + private bool Equals(OperationLayerExportMesh other) + { + return _filePath == other._filePath && _rotateDirection == other._rotateDirection && _flipDirection == other._flipDirection; + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is OperationLayerExportMesh other && Equals(other); + } + + public override int GetHashCode() + { + return HashCode.Combine(_filePath, (int)_rotateDirection, (int)_flipDirection); + } + + #endregion + } +} diff --git a/UVtools.Core/Operations/OperationLayerExportSkeleton.cs b/UVtools.Core/Operations/OperationLayerExportSkeleton.cs index 9197c5f..2fe70e4 100644 --- a/UVtools.Core/Operations/OperationLayerExportSkeleton.cs +++ b/UVtools.Core/Operations/OperationLayerExportSkeleton.cs @@ -71,6 +71,9 @@ namespace UVtools.Core.Operations { } public OperationLayerExportSkeleton(FileFormat slicerFile) : base(slicerFile) + { } + + public override void InitWithSlicerFile() { _filePath = SlicerFile.FileFullPath + ".skeleton.png"; } diff --git a/UVtools.Core/Operations/OperationMorph.cs b/UVtools.Core/Operations/OperationMorph.cs index 37ac4f8..dd2fe11 100644 --- a/UVtools.Core/Operations/OperationMorph.cs +++ b/UVtools.Core/Operations/OperationMorph.cs @@ -11,9 +11,7 @@ using System.ComponentModel; using System.Threading.Tasks; using System.Xml.Serialization; using Emgu.CV; -using Emgu.CV.Cuda; using Emgu.CV.CvEnum; -using UVtools.Core.Extensions; using UVtools.Core.FileFormats; using UVtools.Core.Objects; diff --git a/UVtools.Core/UVtools.Core.csproj b/UVtools.Core/UVtools.Core.csproj index bf5c602..8034585 100644 --- a/UVtools.Core/UVtools.Core.csproj +++ b/UVtools.Core/UVtools.Core.csproj @@ -49,7 +49,7 @@ - + diff --git a/UVtools.Core/Voxel/Voxelizer.cs b/UVtools.Core/Voxel/Voxelizer.cs index b8e953a..f930bba 100644 --- a/UVtools.Core/Voxel/Voxelizer.cs +++ b/UVtools.Core/Voxel/Voxelizer.cs @@ -1,10 +1,18 @@ -using Emgu.CV; +/* + * 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 KdTree; using KdTree.Math; using System; using System.Collections.Generic; using System.Drawing; +using System.IO; using System.Linq; using System.Numerics; using System.Threading.Tasks; @@ -17,8 +25,8 @@ namespace UVtools.Core.Voxel { public class Voxelizer { - - class UVFace + + public class UVFace { public FaceOrientation Type; public uint LayerIndex; @@ -38,7 +46,7 @@ namespace UVtools.Core.Voxel } [Flags] - enum FaceOrientation : short + public enum FaceOrientation : short { None = 0, Top = 1, @@ -49,13 +57,13 @@ namespace UVtools.Core.Voxel Back = 32 } - unsafe FaceOrientation GetOpenFaces(Mat layer, int x, int y, Mat layerBelow = null, Mat layerAbove = null) + public static FaceOrientation GetOpenFaces(Mat layer, int x, int y, Mat layerBelow = null, Mat layerAbove = null) { - var layerSpan = layer.GetBytePointer(); + var layerSpan = layer.GetDataByteSpan(); - FaceOrientation foundFaces = FaceOrientation.None; - - if (layerSpan[layer.GetPixelPos(x, y)] == 0) + var foundFaces = FaceOrientation.None; + var pixelPos = layer.GetPixelPos(x, y); + if (layerSpan[pixelPos] == 0) { return foundFaces; } @@ -66,8 +74,8 @@ namespace UVtools.Core.Voxel } else { - var belowSpan = layerBelow.GetBytePointer(); - if (belowSpan[layerBelow.GetPixelPos(x, y)] == 0) + var belowSpan = layerBelow.GetDataByteSpan(); + if (belowSpan[pixelPos] == 0) { foundFaces |= FaceOrientation.Bottom; } @@ -79,19 +87,19 @@ namespace UVtools.Core.Voxel } else { - var aboveSpan = layerAbove.GetBytePointer(); - if (aboveSpan[layerAbove.GetPixelPos(x, y)] == 0) + var aboveSpan = layerAbove.GetDataByteSpan(); + if (aboveSpan[pixelPos] == 0) { foundFaces |= FaceOrientation.Top; } } - if (x == 0 || layerSpan[layer.GetPixelPos(x - 1, y)] == 0) + if (x == 0 || layerSpan[pixelPos-1] == 0) { foundFaces |= FaceOrientation.Left; } - if (x == layer.Width - 1 || layerSpan[layer.GetPixelPos(x + 1, y)] == 0) + if (x == layer.Width - 1 || layerSpan[pixelPos+1] == 0) { foundFaces |= FaceOrientation.Right; } @@ -109,12 +117,12 @@ namespace UVtools.Core.Voxel return foundFaces; } - Mat BuildVoxelLayerImage(Mat curLayer, Mat layerAbove = null, Mat layerBelow = null) + public static Mat BuildVoxelLayerImage(Mat curLayer, Mat layerAbove = null, Mat layerBelow = null) { /* The goal of the VoxelLayerImage is to reduce as much as possible, the number of pixels we need to do 6 direction neighbor checking on */ /* the outer contours of the current layer should always be checked, they by definition should have an exposed face */ - using var contours = curLayer.FindContours(out var heirarchy, RetrType.Tree); + using var contours = curLayer.FindContours(RetrType.Tree); var onlyContours = curLayer.NewBlank(); CvInvoke.DrawContours(onlyContours, contours, -1, EmguExtensions.WhiteColor, 1); @@ -125,15 +133,15 @@ namespace UVtools.Core.Voxel layerBelow ??= curLayer.NewBlank(); /* anything that is in the current layer but is not in the layer above, by definition has an exposed face */ - Mat upperXor = curLayer.NewBlank(); + var upperXor = curLayer.NewBlank(); CvInvoke.BitwiseXor(curLayer, layerAbove, upperXor); /* anything that is in the current layer but is not in the layer below, by definition has an exposed face */ - Mat lowerXor = curLayer.NewBlank(); + var lowerXor = curLayer.NewBlank(); CvInvoke.BitwiseXor(curLayer, layerBelow, lowerXor); /* Or all of these together to get the list of pixels that have exposed face(s) */ - Mat voxelLayer = curLayer.NewBlank(); + var voxelLayer = curLayer.NewBlank(); CvInvoke.BitwiseOr(onlyContours, voxelLayer, voxelLayer); CvInvoke.BitwiseOr(upperXor, voxelLayer, voxelLayer); CvInvoke.BitwiseOr(lowerXor, voxelLayer, voxelLayer); @@ -151,7 +159,7 @@ namespace UVtools.Core.Voxel return voxelLayer; } - public enum VoxelQuality : int + public enum VoxelQuality : byte { ACCURATE = 1, AVERAGE = 2, @@ -161,7 +169,7 @@ namespace UVtools.Core.Voxel MINECRAFT = 8 } - public unsafe void CreateVoxelMesh(MeshFile mesh, FileFormat file, string filePath, OperationProgress progress, VoxelQuality quality = VoxelQuality.ACCURATE, uint layerStart = 0, uint layerStop = 0) + public unsafe void CreateVoxelMesh(Type meshType, FileFormat file, string filePath, OperationProgress progress, VoxelQuality quality = VoxelQuality.ACCURATE, uint layerStart = 0, uint layerStop = 0) { var layerManager = file.LayerManager; /* Voxelization has 4 overall stages @@ -577,9 +585,10 @@ namespace UVtools.Core.Voxel progress.Reset(); progress.Title = "Generating STL..."; - progress.ItemCount = (uint)layerCount; + progress.ItemCount = layerCount; - mesh.Create(); + using var mesh = meshType.CreateInstance(filePath, FileMode.Create); + mesh.BeginWrite(); /* Begin Stage 4, generating triangles and saving to STL */ foreach (var tree in layerTrees) @@ -600,15 +609,12 @@ namespace UVtools.Core.Voxel /* check for cancellation at every layer, and if so, close the STL file properly */ if (progress.Token.IsCancellationRequested) { - mesh.Close(); Cleanup(); return; } progress.LockAndIncrement(); } - - mesh.Close(); - + void Cleanup() { /* dispose of everything */ @@ -628,23 +634,25 @@ namespace UVtools.Core.Voxel rootFaces[x] = null; } rootFaces = null; - System.GC.Collect(); + GC.Collect(); } - + + mesh.EndWrite(); + } /* NOTE: this took a lot, a lot, a lot, of trial and error, just trust that it generates the correct triangles for a given face ;) */ - IEnumerable<(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 normal)> MakeFacetsForUVFace(UVFace face, float xSize, float ySize, float layerHeight, uint layerStart) + public static IEnumerable<(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 normal)> MakeFacetsForUVFace(UVFace face, float xSize, float ySize, float layerHeight, uint layerStart) { /* triangles need "normal" vectors to show which is the outside of the triangle */ /* also, triangle points need to be provided in counter clockwise direction...*/ - Vector3 LeftNormal = new Vector3(-1, 0, 0); - Vector3 RightNormal = new Vector3(1, 0, 0); - Vector3 TopNormal = new Vector3(0, 0, 1); - Vector3 BottomNormal = new Vector3(0, 0, -1); - Vector3 BackNormal = new Vector3(0, 1, 0); - Vector3 FrontNormal = new Vector3(0, -1, 0); + var LeftNormal = new Vector3(-1, 0, 0); + var RightNormal = new Vector3(1, 0, 0); + var TopNormal = new Vector3(0, 0, 1); + var BottomNormal = new Vector3(0, 0, -1); + var BackNormal = new Vector3(0, 1, 0); + var FrontNormal = new Vector3(0, -1, 0); /* count the "height" of this face, which is == to itself + number of children in its doubly linked list chain */ var height = 1; diff --git a/UVtools.ScriptSample/ScriptInsetSample.cs b/UVtools.ScriptSample/ScriptInsetSample.cs index 60d5dec..85b9dd4 100644 --- a/UVtools.ScriptSample/ScriptInsetSample.cs +++ b/UVtools.ScriptSample/ScriptInsetSample.cs @@ -7,16 +7,13 @@ */ using System; -using System.Collections.Generic; using System.Drawing; using System.Text; using System.Threading.Tasks; using UVtools.Core.Scripting; using Emgu.CV; using Emgu.CV.CvEnum; -using Emgu.CV.Util; using UVtools.Core; -using UVtools.Core.Extensions; namespace UVtools.ScriptSample { diff --git a/UVtools.WPF/ConsoleArguments.cs b/UVtools.WPF/ConsoleArguments.cs index 940b22b..cccc72e 100644 --- a/UVtools.WPF/ConsoleArguments.cs +++ b/UVtools.WPF/ConsoleArguments.cs @@ -11,6 +11,8 @@ using System.IO; using System.Linq; using MoreLinq; using UVtools.Core.FileFormats; +using UVtools.Core.MeshFormats; +using UVtools.Core.Operations; namespace UVtools.WPF { @@ -141,6 +143,58 @@ namespace UVtools.WPF return true; } + // Convert to other file + if (args[0] is "--export-mesh") + { + if (args.Length < 2) + { + Console.WriteLine("Invalid syntax: [output_mesh_file]"); + return true; + } + if (!File.Exists(args[1])) + { + Console.WriteLine($"Input file does not exists: {args[1]}"); + return true; + } + + var slicerFile = FileFormat.FindByExtensionOrFilePath(args[1], true); + if (slicerFile is null) + { + Console.WriteLine($"Invalid input file: {args[1]}"); + return true; + } + + var outputFile = Path.Combine(Path.GetDirectoryName(args[1]), $"{Path.GetFileNameWithoutExtension(args[1])}.stl"); + + if (args.Length >= 3 && !string.IsNullOrWhiteSpace(args[2])) + { + outputFile = args[2]; + } + + var outputExtension = MeshFile.FindFileExtension(outputFile); + if (outputExtension is null) + { + Console.WriteLine($"Invalid output file extension: {outputFile}"); + return true; + } + + + Console.WriteLine($"Loading file: {args[1]}"); + slicerFile.Decode(args[1]); + + Console.WriteLine($"Exporting mesh to: {outputFile}"); + var operation = new OperationLayerExportMesh(slicerFile) + { + FilePath = outputFile + }; + operation.Execute(); + Console.WriteLine("Exported"); + + Console.WriteLine("OK"); + + return true; + } + if (args[0] == "--crypt-ctb") { if (!File.Exists(args[1])) diff --git a/UVtools.WPF/Controls/Tools/ToolLayerExportMeshControl.axaml b/UVtools.WPF/Controls/Tools/ToolLayerExportMeshControl.axaml new file mode 100644 index 0000000..642c35f --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerExportMeshControl.axaml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/UVtools.WPF/Controls/Tools/ToolLayerExportMeshControl.axaml.cs b/UVtools.WPF/Controls/Tools/ToolLayerExportMeshControl.axaml.cs new file mode 100644 index 0000000..1edb781 --- /dev/null +++ b/UVtools.WPF/Controls/Tools/ToolLayerExportMeshControl.axaml.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; +using System.IO; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using UVtools.Core.MeshFormats; +using UVtools.Core.Operations; + +namespace UVtools.WPF.Controls.Tools +{ + public partial class ToolLayerExportMeshControl : ToolControl + { + public OperationLayerExportMesh Operation => BaseOperation as OperationLayerExportMesh; + public ToolLayerExportMeshControl() + { + BaseOperation = new OperationLayerExportMesh(SlicerFile); + if (!ValidateSpawn()) return; + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public async void ChooseFilePath() + { + var dialog = new SaveFileDialog + { + Directory = Path.GetDirectoryName(SlicerFile.FileFullPath), + DefaultExtension = ".stl", + InitialFileName = SlicerFile.FilenameNoExt, + Filters = GetFilters(), + }; + var filePath = await dialog.ShowAsync(ParentWindow); + if (string.IsNullOrWhiteSpace(filePath)) return; + Operation.FilePath = filePath; + } + + public List GetFilters() + { + var list = new List(); + foreach (var fileExtension in MeshFile.AvailableMeshFiles) + { + list.Add(new FileDialogFilter + { + Name = fileExtension.Description, + Extensions = new List{fileExtension.Extension} + }); + } + + return list; + } + } +} diff --git a/UVtools.WPF/MainWindow.axaml.cs b/UVtools.WPF/MainWindow.axaml.cs index 2c6ff4f..64306f2 100644 --- a/UVtools.WPF/MainWindow.axaml.cs +++ b/UVtools.WPF/MainWindow.axaml.cs @@ -411,6 +411,14 @@ namespace UVtools.WPF Source = new Bitmap(App.GetAsset("/Assets/Icons/file-image-16x16.png")) } }, + new() + { + Tag = new OperationLayerExportMesh(), + Icon = new Avalonia.Controls.Image + { + Source = new Bitmap(App.GetAsset("/Assets/Icons/cubes-16x16.png")) + } + }, }; #region DataSets diff --git a/UVtools.WPF/UVtools.WPF.csproj b/UVtools.WPF/UVtools.WPF.csproj index 60621e0..f204db3 100644 --- a/UVtools.WPF/UVtools.WPF.csproj +++ b/UVtools.WPF/UVtools.WPF.csproj @@ -34,13 +34,13 @@ 1701;1702; - + - - - + + + - +