mirror of
https://github.com/riegera2412/UVtools.git
synced 2026-07-08 17:42:31 +02:00
Add UI for mesh export
This commit is contained in:
@@ -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 \<input_file\> [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
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.8" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.9" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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. <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.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<MeshFile>(filePath, fileMode, fileFormat);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Enums
|
||||
|
||||
public enum MeshFileFormat : byte
|
||||
{
|
||||
ASCII,
|
||||
BINARY
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file format for this mesh
|
||||
/// </summary>
|
||||
public MeshFileFormat FileFormat { get; } = MeshFileFormat.BINARY;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file path of the stream
|
||||
/// </summary>
|
||||
public string FilePath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current file stream
|
||||
/// </summary>
|
||||
public FileStream MeshStream { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of triangles
|
||||
/// </summary>
|
||||
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
|
||||
/// <summary>
|
||||
/// Call once before write content to the file, use this to build up the header if any
|
||||
/// </summary>
|
||||
public virtual void BeginWrite(){}
|
||||
|
||||
/// <summary>
|
||||
/// Writes an triangle to the file
|
||||
/// </summary>
|
||||
/// <param name="p1"></param>
|
||||
/// <param name="p2"></param>
|
||||
/// <param name="p3"></param>
|
||||
/// <param name="normal"></param>
|
||||
public abstract void WriteTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 normal);
|
||||
public abstract void Close();
|
||||
|
||||
/// <summary>
|
||||
/// Call once before close the file, use this to build up the footer if any
|
||||
/// </summary>
|
||||
public virtual void EndWrite(){}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
MeshStream?.Dispose();
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,112 @@
|
||||
using System;
|
||||
/*
|
||||
* 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.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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
/*
|
||||
* 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.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<MeshFile>(_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<float, Voxelizer.UVFace>[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<Voxelizer.FaceOrientation, List<Point>>();
|
||||
|
||||
/* 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<Voxelizer.FaceOrientation, List<Point>>();
|
||||
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<float, Voxelizer.UVFace>(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<float, Voxelizer.UVFace> 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<MeshFile>(_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
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,9 @@ namespace UVtools.Core.Operations
|
||||
{ }
|
||||
|
||||
public OperationLayerExportSkeleton(FileFormat slicerFile) : base(slicerFile)
|
||||
{ }
|
||||
|
||||
public override void InitWithSlicerFile()
|
||||
{
|
||||
_filePath = SlicerFile.FileFullPath + ".skeleton.png";
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AnimatedGif" Version="1.0.5" />
|
||||
<PackageReference Include="BinarySerializer" Version="8.6.0" />
|
||||
<PackageReference Include="BinarySerializer" Version="8.6.1" />
|
||||
<PackageReference Include="Emgu.CV" Version="4.5.3.4721" />
|
||||
<PackageReference Include="KdTree" Version="1.4.1" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.0.0-6.final" />
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
using Emgu.CV;
|
||||
/*
|
||||
* 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 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<MeshFile>(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;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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: <input_file> [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]))
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<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.ToolLayerExportMeshControl">
|
||||
<StackPanel Spacing="10">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<TextBox
|
||||
Watermark="Output filepath"
|
||||
UseFloatingWatermark="True"
|
||||
VerticalAlignment="Center"
|
||||
IsReadOnly="True"
|
||||
Width="600"
|
||||
Text="{Binding Operation.FilePath}"/>
|
||||
<Button
|
||||
VerticalAlignment="Stretch"
|
||||
Command="{Binding ChooseFilePath}">
|
||||
<Image Source="/Assets/Icons/open-16x16.png"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
<Grid RowDefinitions="Auto,10,Auto,10,Auto"
|
||||
ColumnDefinitions="Auto,10,520">
|
||||
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Mesh quality:"/>
|
||||
<ComboBox Grid.Row="0" Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
Items="{Binding Operation.Quality, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}"
|
||||
SelectedItem="{Binding Operation.Quality, Converter={StaticResource FromValueDescriptionToEnumConverter}}"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Rotate direction:"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
Items="{Binding Operation.RotateDirection, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}"
|
||||
SelectedItem="{Binding Operation.RotateDirection, Converter={StaticResource FromValueDescriptionToEnumConverter}}"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Text="Flip direction:"/>
|
||||
<ComboBox Grid.Row="4" Grid.Column="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
Items="{Binding Operation.FlipDirection, Converter={StaticResource EnumToCollectionConverter}, Mode=OneTime}"
|
||||
SelectedItem="{Binding Operation.FlipDirection, Converter={StaticResource FromValueDescriptionToEnumConverter}}"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@@ -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<FileDialogFilter> GetFilters()
|
||||
{
|
||||
var list = new List<FileDialogFilter>();
|
||||
foreach (var fileExtension in MeshFile.AvailableMeshFiles)
|
||||
{
|
||||
list.Add(new FileDialogFilter
|
||||
{
|
||||
Name = fileExtension.Description,
|
||||
Extensions = new List<string>{fileExtension.Extension}
|
||||
});
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
<NoWarn>1701;1702;</NoWarn>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="0.10.8" />
|
||||
<PackageReference Include="Avalonia" Version="0.10.9" />
|
||||
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2020091801" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.8" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.8" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.8" />
|
||||
<PackageReference Include="Avalonia.Controls.DataGrid" Version="0.10.9" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="0.10.9" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.9" />
|
||||
<PackageReference Include="Emgu.CV.runtime.windows" Version="4.5.3.4721" />
|
||||
<PackageReference Include="MessageBox.Avalonia" Version="1.5.3" />
|
||||
<PackageReference Include="MessageBox.Avalonia" Version="1.5.5" />
|
||||
<PackageReference Include="ThemeEditor.Controls.ColorPicker" Version="0.10.8" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user