using FeMM.Grasshopper.Helpers;
using Grasshopper.Kernel;
using Grasshopper.Kernel.Types;
using Rhino;
using Rhino.Geometry;
using System;
using System.Collections.Generic;
namespace FeMM.Grasshopper.Components.Tools
{
public class MeshCleanComponent : GH_Component
{
/// <summary>
/// Initializes a new instance of the MeshRemap class.
/// </summary>
public MeshCleanComponent()
: base("Mesh Clean", "MC", "Clean a mesh, removing small triangles", CategoryNameConstants.CATEGORY_FEMM, CategoryNameConstants.SUBCATEGORY_TOOLS)
{
}
/// <summary>
/// Registers all the input parameters for this component.
/// </summary>
protected override void RegisterInputParams(GH_InputParamManager pManager)
{
pManager.AddMeshParameter("Mesh", "M", "The mesh to clean", GH_ParamAccess.item);
pManager.AddNumberParameter("Length", "L", "Minimum allowed length for an edge", GH_ParamAccess.item, 1);
pManager.AddNumberParameter("Area", "A", "Minimum allowed area for a face", GH_ParamAccess.item, 0);
pManager.AddNumberParameter("Angle", "R", "Angle in radians to consider a vertex as a corner", GH_ParamAccess.item, 2.5);
}
/// <summary>
/// Registers all the output parameters for this component.
/// </summary>
protected override void RegisterOutputParams(GH_OutputParamManager pManager)
{
pManager.AddGeometryParameter("Cleaned", "C", "The cleaned mesh", GH_ParamAccess.item);
}
/// <summary>
/// This is the method that actually does the work.
/// </summary>
/// <param name="DA">The DA object is used to retrieve from inputs and store in outputs.</param>
protected override void SolveInstance(IGH_DataAccess DA)
{
var mesh = new GH_Mesh();
double len = 0;
double area = 0;
double angle = 0;
if (!DA.GetData(0, ref mesh))
{
return;
}
DA.GetData(1, ref len);
DA.GetData(2, ref area);
DA.GetData(3, ref angle);
var cleaned = CleanMesh(mesh.Value, len, area, angle);
DA.SetData(0, new GH_Mesh(cleaned));
}
public static Mesh CleanMesh(Mesh mesh, double length, double area, double cornerTol = 2.35619)
{
// TODO: Is this a proper cleaning operation?
mesh.Weld(0.5);
mesh.RebuildNormals();
mesh.UnifyNormals();
mesh.Vertices.CombineIdentical(true, true);
mesh.Vertices.CullUnused();
// WeldMap: one set for each vertex containing the vertices that will be merged in one point.
// init with the vertex itself
var weldMap = new HashSet<int>[mesh.Vertices.Count];
for (int i = 0; i < mesh.Vertices.Count; ++i)
{
weldMap[i] = [i];
}
// Search vertices clusters
var tree = new RTree();
for (int i = 0; i < mesh.Vertices.Count; ++i)
{
// Result of the search
var closest = -1;
var distance = double.MaxValue;
// Search operation to find the closest vertex
tree.Search(new Sphere(mesh.Vertices[i], length), new EventHandler<RTreeEventArgs>((sender, e) =>
{
// NOTE: If we have found a cluster, do not exit unless it is a closer cluster
if (closest < 0 || weldMap[closest].Count == 1 || weldMap[e.Id].Count > 1)
{
var dist = mesh.Vertices[i].DistanceTo(mesh.Vertices[e.Id]);
if (dist < distance)
{
distance = dist;
closest = e.Id;
}
}
}));
// No other vertex close enough found in tree. Insert this vertex.
if (closest == -1)
{
tree.Insert(mesh.Vertices[i], i);
continue;
}
// Otherwise add the vertex to the weldmap
//weldMap[i].Union(weldMap[closest]);
foreach (var w in weldMap[closest])
{
weldMap[i].Add(w);
}
weldMap[closest] = weldMap[i];
}
// Result cleaned mesh
var clean = new Mesh();
// NakedStatus: one bool for each vertex that tells if the vertex is on border
var nakedStatus = mesh.GetNakedEdgePointStatus();
// vmap: map from old mesh vertex index, to new mesh vertex index
var vmap = new Dictionary<int, int>();
for (int i = 0; i < mesh.Vertices.Count; ++i)
{
// Vertex already added (so it must be part of a cluster)
if (vmap.ContainsKey(i))
{
continue;
}
// Find corner points and border points
var cornerList = new List<int>();
var nakedList = new List<int>();
foreach (var w in weldMap[i])
{
// Not a border point (and not a corner)
if (!nakedStatus[w])
{
continue;
}
// It is a border point
nakedList.Add(w);
// Check if it is a corner point
// find all connections of this vertex
var connPoints = new List<Point3d>();
var connIndices = mesh.Vertices.GetConnectedVertices(w);
// Keep only connection with other border points
for (int c = 0; c < connIndices.Length; ++c)
{
var connIdx = connIndices[c];
if (connIdx != w && nakedStatus[connIdx])
{
connPoints.Add(mesh.Vertices[connIdx]);
}
}
// A corner must have more than one connection
if (connPoints.Count < 2)
{
continue;
}
var vertPt = mesh.Vertices[w];
double angle = 0;
// TODO: should be better if using naked edges
// We don't know wich edge is the border:
// Compare angles from each pair of connected points and choose the largest one
for (int k = 0; k < connPoints.Count - 1; ++k)
{
for (int kk = k + 1; kk < connPoints.Count; ++kk)
{
var vecA = new Vector3d(connPoints[k] - vertPt);
var vecB = new Vector3d(connPoints[kk] - vertPt);
var newAngle = Vector3d.VectorAngle(vecA, vecB);
if (newAngle == RhinoMath.UnsetValue)
{
// TODO: It is probably a duplicated point, can be better handled
newAngle = 0;
continue;
}
if (newAngle > angle)
{
angle = newAngle;
}
}
}
// If the angle is set and lesser than tol it is a corner vertex
if (angle > 0 && angle < cornerTol)
{
cornerList.Add(w);
}
}
// Set positions of new points
var pt = new Point3d(0, 0, 0);
if (cornerList.Count > 0)
{
// If we have one or more corners, use corners position
foreach (var n in cornerList)
{
pt += mesh.Vertices[n];
}
pt = Point3d.Divide(pt, cornerList.Count);
}
else if (nakedList.Count > 0)
{
// If we have one or more borders, use borders position
foreach (var n in nakedList)
{
pt += mesh.Vertices[n];
}
pt = Point3d.Divide(pt, nakedList.Count);
}
else
{
// Otherwise average all the points
foreach (var n in weldMap[i])
{
pt += mesh.Vertices[n];
}
pt = Point3d.Divide(pt, weldMap[i].Count);
}
// Add the vertex to new mesh
var newIndex = clean.Vertices.Count;
clean.Vertices.Add(pt);
// Map the new vertex
foreach (var n in weldMap[i])
{
vmap.Add(n, newIndex);
}
}
// Add the faces to new mesh
for (int i = 0; i < mesh.Faces.Count; ++i)
{
var face = mesh.Faces[i];
// List of the old face indices
var vertices = new int[4] { vmap[face.A], vmap[face.B], vmap[face.C], vmap[face.D] };
var uniques = new HashSet<int>();
var uniquesLoop = new List<int>();
// Find the uniquely mapped indices, keep indices order
for (int j = 0; j < vertices.Length; ++j)
{
if (!uniques.Contains(vertices[j]))
{
uniques.Add(vertices[j]);
uniquesLoop.Add(vertices[j]);
}
}
// Add new face to the mesh
if (uniquesLoop.Count == 3)
{
clean.Faces.AddFace(uniquesLoop[0], uniquesLoop[1], uniquesLoop[2]);
}
if (uniquesLoop.Count == 4)
{
clean.Faces.AddFace(uniquesLoop[0], uniquesLoop[1], uniquesLoop[2], uniquesLoop[3]);
}
}
// Clean areas
if (area > 0)
{
clean.CollapseFacesByArea(area, 0);
}
clean.RebuildNormals();
clean.UnifyNormals();
return clean;
}
public override GH_Exposure Exposure => GH_Exposure.tertiary;
/// <summary>
/// Provides an Icon for the component.
/// </summary>
protected override System.Drawing.Bitmap Icon => Properties.Resources.CleanMeshIcon;
/// <summary>
/// Gets the unique ID for this component. Do not change this ID after release.
/// </summary>
public override Guid ComponentGuid => new("f77f438b-00d4-46dd-b428-bc3ec195cfd2");
}
}