Merge pull request #329 from tslater2006/master

Add Voxelization support to UVTools.Core
This commit is contained in:
Tiago Conceição
2021-10-27 16:29:47 +01:00
committed by GitHub
4 changed files with 842 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
namespace UVtools.Core.MeshFormats
{
public abstract class MeshFile
{
public string FilePath;
public MeshFile(string filePath)
{
FilePath = filePath;
}
public abstract void Create();
public abstract void WriteTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 normal);
public abstract void Close();
}
}
+101
View File
@@ -0,0 +1,101 @@
using System;
using System.IO;
using System.Numerics;
using System.Text;
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)
{
outputFormat = format;
objectName = name ?? "UVTools STL Object";
}
public override void Create()
{
stream = File.Create(FilePath);
if (outputFormat == STLFormat.ASCII)
{
asciiWriter = new StreamWriter(stream);
asciiWriter.WriteLine($"solid \"{objectName}\"");
}
else
{
byte[] header = new byte[80];
byte[] headerText = UTF8Encoding.UTF8.GetBytes("STL File Generated by UVTools");
Array.Copy(headerText, header, headerText.Length);
stream.Write(header);
stream.Position += 4;
}
}
public override void WriteTriangle(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 normal)
{
if (outputFormat == STLFormat.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
{
triangleCount++;
stream.Write(BitConverter.GetBytes(normal.X));
stream.Write(BitConverter.GetBytes(normal.Y));
stream.Write(BitConverter.GetBytes(normal.Z));
stream.Write(BitConverter.GetBytes(p1.X));
stream.Write(BitConverter.GetBytes(p1.Y));
stream.Write(BitConverter.GetBytes(p1.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]);
}
}
public override void Close()
{
if (asciiWriter is not null)
{
asciiWriter.WriteLine($"endsolid \"{objectName}\"");
asciiWriter.Flush();
asciiWriter.Close();
} else
{
stream.Position = 80;
stream.Write(BitConverter.GetBytes(triangleCount));
stream.Flush();
stream.Close();
}
}
}
}
+1
View File
@@ -51,6 +51,7 @@
<PackageReference Include="AnimatedGif" Version="1.0.5" />
<PackageReference Include="BinarySerializer" Version="8.6.0" />
<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" />
<PackageReference Include="morelinq" Version="3.3.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
+719
View File
@@ -0,0 +1,719 @@
using Emgu.CV;
using Emgu.CV.CvEnum;
using KdTree;
using KdTree.Math;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using UVtools.Core.Extensions;
using UVtools.Core.FileFormats;
using UVtools.Core.MeshFormats;
using UVtools.Core.Operations;
namespace UVtools.Core.Voxel
{
public class Voxelizer
{
class UVFace
{
public FaceOrientation Type;
public uint LayerIndex;
public Rectangle FaceRect;
/* Doubly linked list of UVFaces, used during Stage 3, collapsing the faces vertically.
* instead of modifying properties and having to remove items from lists, we keep all faces
* and just link parents and children together.
* During STL triangle generation, we only draw the 'roots' (faces with no parent) and we count
* the chain of children for how "high" the face should be. */
public UVFace Parent = null;
public UVFace Child = null;
/* This is used to make a linked list of faces, instead of generating a list which requires resize/reallocation/copies.
* Particularly useful when you have a model that consists of 49 million visible faces...*/
public UVFace FlatListNext = null;
}
[Flags]
enum FaceOrientation : short
{
None = 0,
Top = 1,
Bottom = 2,
Left = 4,
Right = 8,
Front = 16,
Back = 32
}
unsafe FaceOrientation GetOpenFaces(Mat layer, int x, int y, Mat layerBelow = null, Mat layerAbove = null)
{
var layerSpan = layer.GetBytePointer();
FaceOrientation foundFaces = FaceOrientation.None;
if (layerSpan[layer.GetPixelPos(x, y)] == 0)
{
return foundFaces;
}
if (layerBelow is null)
{
foundFaces |= FaceOrientation.Bottom;
}
else
{
var belowSpan = layerBelow.GetBytePointer();
if (belowSpan[layerBelow.GetPixelPos(x, y)] == 0)
{
foundFaces |= FaceOrientation.Bottom;
}
}
if (layerAbove is null)
{
foundFaces |= FaceOrientation.Top;
}
else
{
var aboveSpan = layerAbove.GetBytePointer();
if (aboveSpan[layerAbove.GetPixelPos(x, y)] == 0)
{
foundFaces |= FaceOrientation.Top;
}
}
if (x == 0 || layerSpan[layer.GetPixelPos(x - 1, y)] == 0)
{
foundFaces |= FaceOrientation.Left;
}
if (x == layer.Width - 1 || layerSpan[layer.GetPixelPos(x + 1, y)] == 0)
{
foundFaces |= FaceOrientation.Right;
}
if (y == 0 || layerSpan[layer.GetPixelPos(x, y - 1)] == 0)
{
foundFaces |= FaceOrientation.Front;
}
if (y == layer.Height - 1 || layerSpan[layer.GetPixelPos(x, y + 1)] == 0)
{
foundFaces |= FaceOrientation.Back;
}
return foundFaces;
}
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);
var onlyContours = curLayer.NewBlank();
CvInvoke.DrawContours(onlyContours, contours, -1, EmguExtensions.WhiteColor, 1);
bool needAboveDispose = layerAbove is null;
bool needBelowDispose = layerBelow is null;
layerAbove ??= curLayer.NewBlank();
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();
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();
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();
CvInvoke.BitwiseOr(onlyContours, voxelLayer, voxelLayer);
CvInvoke.BitwiseOr(upperXor, voxelLayer, voxelLayer);
CvInvoke.BitwiseOr(lowerXor, voxelLayer, voxelLayer);
/* dispoose of the layerAbove/layerBelow if they were allocated here */
if (needAboveDispose)
{
layerAbove.Dispose();
}
if (needBelowDispose)
{
layerBelow.Dispose();
}
onlyContours.Dispose();
return voxelLayer;
}
public enum VoxelQuality : int
{
ACCURATE = 1,
AVERAGE = 2,
QUICK = 3,
DIRTY = 6,
MINECRAFT = 8
}
public unsafe void CreateVoxelMesh(MeshFile mesh, 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
* 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) */
if (layerStop == 0)
{
layerStop = file.LayerCount;
}
uint layerCount = layerStop - layerStart;
float xWidth = (float)(layerManager.SlicerFile.Xppmm > 0 ? 1 / layerManager.SlicerFile.Xppmm : 0.035) * (int)quality;
float yWidth = (float)(layerManager.SlicerFile.Yppmm > 0 ? 1 / layerManager.SlicerFile.Yppmm : 0.035) * (int)quality;
var zHeight = layerManager.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 RoiLayer = layerManager[layerStart].LayerMat.Roi(layerManager.SlicerFile.BoundingRectangle);
Mat aboveLayer = RoiLayer.Clone(); /* clone and then dispose of the ROI mat, not effecient but keeps the GetPixelPos working and clean */
if ((int)quality > 1)
{
Mat resize = new Mat();
CvInvoke.Resize(aboveLayer, resize, new Size(), 1.0 / (int)quality, 1.0 / (int)quality, Inter.Area);
aboveLayer.Dispose();
aboveLayer = resize;
}
RoiLayer.Dispose();
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. */
FaceOrientation[] facesToCheck = new FaceOrientation[] { FaceOrientation.Front, FaceOrientation.Back, FaceOrientation.Left, FaceOrientation.Right, FaceOrientation.Top, FaceOrientation.Bottom };
/* Init of other objects that will be used in subsequent stages */
UVFace[] rootFaces = new UVFace[layerCount];
uint[] layerFaceCounts = new uint[layerCount];
KdTree<float, UVFace>[] layerTrees = new KdTree<float, UVFace>[layerCount];
progress.Reset();
progress.Title = "Generating faces from layers...";
progress.ItemCount = layerCount;
/* Begin Stage 1, identifying all faces that are visible from outside the model */
for (uint layerIndex = 0; layerIndex < layerCount; layerIndex++)
{
UVFace currentFaceItem = null;
/* Should contain a list of all found faces on this layer, keyed by the face orientation */
Dictionary<FaceOrientation, List<Point>> foundFaces = new Dictionary<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 < layerCount - 1)
{
RoiLayer = layerManager[layerIndex + layerStart + 1].LayerMat.Roi(layerManager.SlicerFile.BoundingRectangle);
aboveLayer = RoiLayer.Clone();
if ((int)quality > 1)
{
Mat resize = new Mat();
CvInvoke.Resize(aboveLayer, resize, new Size(), 1.0 / (int)quality, 1.0 / (int)quality, Inter.Area);
aboveLayer.Dispose();
aboveLayer = resize;
}
RoiLayer.Dispose();
//CvInvoke.Threshold(aboveLayer, aboveLayer, 1, 255, ThresholdType.Binary);
}
else
{
aboveLayer = null;
}
/* get image of pixels to do neighbor checks on */
var voxelLayer = BuildVoxelLayerImage(curLayer, aboveLayer, belowLayer);
var voxelBytes = 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 */
Dictionary<FaceOrientation, List<Point>> threadDict = new Dictionary<FaceOrientation, List<Point>>();
for (var x = 0; x < curLayer.Width; x++)
{
if (voxelBytes[voxelLayer.GetPixelPos(x, y)] == 0) continue;
var faces = GetOpenFaces(curLayer, x, y, belowLayer, aboveLayer);
if (faces != FaceOrientation.None)
{
foreach (var face in facesToCheck)
{
if (faces.HasFlag(face))
{
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 == FaceOrientation.Front || faceType == FaceOrientation.Back || faceType == FaceOrientation.Top || faceType == 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[layerIndex]++;
if (currentFaceItem is null)
{
rootFaces[layerIndex] = new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) };
currentFaceItem = rootFaces[layerIndex];
}
else
{
currentFaceItem.FlatListNext = new UVFace() { LayerIndex = layerIndex, 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[layerIndex]++;
if (currentFaceItem is null)
{
rootFaces[layerIndex] = new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) };
currentFaceItem = rootFaces[layerIndex];
}
else
{
currentFaceItem.FlatListNext = new UVFace() { LayerIndex = layerIndex, 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[layerIndex]++;
if (currentFaceItem is null)
{
rootFaces[layerIndex] = new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) };
currentFaceItem = rootFaces[layerIndex];
}
else
{
currentFaceItem.FlatListNext = new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curX - startX + 1, 1) };
currentFaceItem = currentFaceItem.FlatListNext;
}
}
if (faceType == FaceOrientation.Left || faceType == 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[layerIndex]++;
if (currentFaceItem is null)
{
rootFaces[layerIndex] = new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) };
currentFaceItem = rootFaces[layerIndex];
}
else
{
currentFaceItem.FlatListNext = new UVFace() { LayerIndex = layerIndex, 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[layerIndex]++;
if (currentFaceItem is null)
{
rootFaces[layerIndex] = new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) };
currentFaceItem = rootFaces[layerIndex];
}
else
{
currentFaceItem.FlatListNext = new UVFace() { LayerIndex = layerIndex, 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[layerIndex]++;
if (currentFaceItem is null)
{
rootFaces[layerIndex] = new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) };
currentFaceItem = rootFaces[layerIndex];
}
else
{
currentFaceItem.FlatListNext = new UVFace() { LayerIndex = layerIndex, Type = faceType, FaceRect = new Rectangle(startX, startY, curY - startY + 1, 1) };
currentFaceItem = currentFaceItem.FlatListNext;
}
}
}
progress.LockAndIncrement();
if (progress.Token.IsCancellationRequested)
{
Cleanup();
return;
}
}
progress.Reset();
progress.Title = "Building KD Trees";
progress.ItemCount = layerCount;
/* 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, layerCount, 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, 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[] { (float)currentFaceItem.Type, currentFaceItem.FaceRect.X, currentFaceItem.FaceRect.Y }, currentFaceItem);
currentFaceItem = currentFaceItem.FlatListNext;
}
layerTrees[layerIndex].Add(new float[] { (float)currentFaceItem.Type, currentFaceItem.FaceRect.X, currentFaceItem.FaceRect.Y }, currentFaceItem);
progress.LockAndIncrement();
});
if (progress.Token.IsCancellationRequested)
{
Cleanup();
return;
}
progress.Reset();
progress.Title = "Collapsing faces...";
progress.ItemCount = layerCount;
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, layerCount, idx =>
{
if (progress.Token.IsCancellationRequested)
{
return;
}
/* if no faces on this layer... skip.... needed for empty layers */
if (layerTrees[idx] is null) return;
/* check each point in the current layers tree */
foreach (var point in layerTrees[idx])
{
/* 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, UVFace> treeBelow = null;
if (point.Value.Type == FaceOrientation.Top || point.Value.Type == FaceOrientation.Bottom)
{
if (point.Value.Type == FaceOrientation.Top)
{
pointBelow = new float[] { point.Point[0], point.Point[1], point.Point[2] - 1 };
} else
{
pointBelow = new float[] { point.Point[0], point.Point[1], point.Point[2] - 1 };
}
treeBelow = layerTrees[idx];
}
else
{
pointBelow = new float[] { point.Point[0], point.Point[1], point.Point[2] };
if (idx > 0)
{
treeBelow = layerTrees[idx - 1];
}
}
if (treeBelow == null) continue;
var faceBelow = treeBelow.FindValueAt(pointBelow);
if (faceBelow is not null)
{
/* 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;
}
progress.Reset();
progress.Title = "Generating STL...";
progress.ItemCount = (uint)layerCount;
mesh.Create();
/* 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 MakeFacetsForUVFace(p.Value, xWidth, yWidth, zHeight, layerStart))
{
/* save to STL 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)
{
mesh.Close();
Cleanup();
return;
}
progress.LockAndIncrement();
}
mesh.Close();
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;
System.GC.Collect();
}
}
/* 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)
{
/* 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);
/* count the "height" of this face, which is == to itself + number of children in its doubly linked list chain */
var height = 1;
UVFace child = face;
while (child.Child is not null)
{
height++;
child = child.Child;
}
face.LayerIndex += layerStart;
if (face.Type == FaceOrientation.Front)
{
var lowerLeft = new Vector3(face.FaceRect.X * xSize, face.FaceRect.Y * ySize, face.LayerIndex * layerHeight);
var lowerRight = new Vector3((face.FaceRect.X + face.FaceRect.Width) * xSize, face.FaceRect.Y * ySize, face.LayerIndex * layerHeight);
var upperLeft = new Vector3(lowerLeft.X, lowerLeft.Y, lowerLeft.Z + ((height) * layerHeight));
var upperRight = new Vector3(lowerRight.X, lowerRight.Y, lowerRight.Z + ((height) * layerHeight));
yield return (lowerLeft, lowerRight, upperRight, FrontNormal);
yield return (upperRight, upperLeft, lowerLeft, FrontNormal);
}
else if (face.Type == FaceOrientation.Back)
{
var lowerRight = new Vector3(face.FaceRect.X * xSize, face.FaceRect.Y * ySize + ySize, face.LayerIndex * layerHeight);
var lowerLeft = new Vector3((face.FaceRect.X + face.FaceRect.Width) * xSize, face.FaceRect.Y * ySize + ySize, face.LayerIndex * layerHeight);
var upperLeft = new Vector3(lowerLeft.X, lowerLeft.Y, lowerLeft.Z + ((height) * layerHeight));
var upperRight = new Vector3(lowerRight.X, lowerRight.Y, lowerRight.Z + ((height) * layerHeight));
yield return (lowerLeft, lowerRight, upperRight, BackNormal);
yield return (upperRight, upperLeft, lowerLeft, BackNormal);
}
else if (face.Type == FaceOrientation.Left)
{
var lowerLeft = new Vector3(face.FaceRect.X * xSize, (face.FaceRect.Y + face.FaceRect.Width) * ySize, face.LayerIndex * layerHeight);
var lowerRight = new Vector3(face.FaceRect.X * xSize, (face.FaceRect.Y) * ySize, face.LayerIndex * layerHeight);
var upperLeft = new Vector3(lowerLeft.X, lowerLeft.Y, lowerLeft.Z + ((height) * layerHeight));
var upperRight = new Vector3(lowerRight.X, lowerRight.Y, lowerRight.Z + ((height) * layerHeight));
yield return (lowerLeft, lowerRight, upperRight, LeftNormal);
yield return (upperRight, upperLeft, lowerLeft, LeftNormal);
}
else if (face.Type == FaceOrientation.Right)
{
var lowerRight = new Vector3(face.FaceRect.X * xSize + xSize, (face.FaceRect.Y + face.FaceRect.Width) * ySize, face.LayerIndex * layerHeight);
var lowerLeft = new Vector3(face.FaceRect.X * xSize + xSize, (face.FaceRect.Y) * ySize, face.LayerIndex * layerHeight);
var upperLeft = new Vector3(lowerLeft.X, lowerLeft.Y, lowerLeft.Z + ((height) * layerHeight));
var upperRight = new Vector3(lowerRight.X, lowerRight.Y, lowerRight.Z + ((height) * layerHeight));
yield return (lowerLeft, lowerRight, upperRight, RightNormal);
yield return (upperRight, upperLeft, lowerLeft, RightNormal);
}
else if (face.Type == FaceOrientation.Top)
{
var upperLeft = new Vector3(face.FaceRect.X * xSize, face.FaceRect.Y * ySize, face.LayerIndex * layerHeight + layerHeight);
var upperRight = new Vector3(face.FaceRect.X * xSize, (face.FaceRect.Y + height) * ySize, face.LayerIndex * layerHeight + layerHeight);
var lowerLeft = new Vector3(upperLeft.X + (face.FaceRect.Width * xSize), upperLeft.Y, face.LayerIndex * layerHeight + layerHeight);
var lowerRight = new Vector3(upperRight.X + (face.FaceRect.Width * xSize), upperRight.Y, face.LayerIndex * layerHeight + layerHeight);
yield return (lowerLeft, lowerRight, upperRight, TopNormal);
yield return (upperRight, upperLeft, lowerLeft, TopNormal);
}
else if (face.Type == FaceOrientation.Bottom)
{
var upperRight = new Vector3(face.FaceRect.X * xSize, face.FaceRect.Y * ySize, face.LayerIndex * layerHeight);
var upperLeft = new Vector3(face.FaceRect.X * xSize, (face.FaceRect.Y + height) * ySize, face.LayerIndex * layerHeight);
var lowerLeft = new Vector3(upperLeft.X + (face.FaceRect.Width * xSize), upperLeft.Y, face.LayerIndex * layerHeight);
var lowerRight = new Vector3(upperRight.X + (face.FaceRect.Width * xSize), upperRight.Y, face.LayerIndex * layerHeight);
yield return (lowerLeft, lowerRight, upperRight, BottomNormal);
yield return (upperRight, upperLeft, lowerLeft, BottomNormal);
}
else
{
yield break;
}
}
}
}