A helper class used by AutomaticWardrobeFit that finds open fabric edges from the finest LOD and spatially bins them. It welds vertex positions, finds boundary edges (edges belonging to a single triangle), stores those edges in 3D grid cells with padding, and exposes InteriorWeight(point) that returns a smooth weight based on distance to the nearest boundary edge.
using System;
namespace CharacterEditor;
public static partial class AutomaticWardrobeFit
{
/// <summary>Open fabric edges from the finest LOD, welded across UV/material seams.</summary>
sealed class FabricBoundary
{
const float Padding = .6f;
readonly record struct Edge(Vector3 A, Vector3 B);
readonly Dictionary<(int, int, int), List<Edge>> cells = new();
static (int, int, int) Cell(Vector3 p) => ((int)MathF.Floor(p.x), (int)MathF.Floor(p.y), (int)MathF.Floor(p.z));
public FabricBoundary(Vector3[] points, int[] triangles)
{
var welded = new Dictionary<(int, int, int), int>();
var positions = new List<Vector3>();
int Weld(int index)
{
var p = points[index];
var key = ((int)MathF.Round(p.x * 500), (int)MathF.Round(p.y * 500), (int)MathF.Round(p.z * 500));
if (welded.TryGetValue(key, out int id)) return id;
welded[key] = positions.Count;
positions.Add(p);
return positions.Count - 1;
}
var faces = new HashSet<(int, int, int)>();
var counts = new Dictionary<(int, int), int>();
for (int t = 0; t < triangles.Length; t += 3)
{
int a = Weld(triangles[t]), b = Weld(triangles[t + 1]), c = Weld(triangles[t + 2]);
if (a > b) (a, b) = (b, a);
if (b > c) (b, c) = (c, b);
if (a > b) (a, b) = (b, a);
if (a == b || b == c || !faces.Add((a, b, c))) continue;
foreach (var edge in new[] { (a, b), (b, c), (a, c) })
counts[edge] = counts.GetValueOrDefault(edge) + 1;
}
foreach (var pair in counts.Where(p => p.Value == 1))
{
var edge = new Edge(positions[pair.Key.Item1], positions[pair.Key.Item2]);
var min = Cell(Vector3.Min(edge.A, edge.B) - Vector3.One * Padding);
var max = Cell(Vector3.Max(edge.A, edge.B) + Vector3.One * Padding);
for (int x = min.Item1; x <= max.Item1; x++)
for (int y = min.Item2; y <= max.Item2; y++)
for (int z = min.Item3; z <= max.Item3; z++)
{
var key = (x, y, z);
if (!cells.TryGetValue(key, out var list)) cells[key] = list = new();
list.Add(edge);
}
}
}
public float InteriorWeight(Vector3 point)
{
if (!cells.TryGetValue(Cell(point), out var edges)) return 1;
float distanceSquared = Padding * Padding;
foreach (var edge in edges)
{
var direction = edge.B - edge.A;
float fraction = Math.Clamp(Vector3.Dot(point - edge.A, direction) / MathF.Max(direction.LengthSquared, 1e-10f), 0, 1);
distanceSquared = MathF.Min(distanceSquared, (point - edge.A - direction * fraction).LengthSquared);
}
return Smooth(.2f, Padding, MathF.Sqrt(distanceSquared));
}
}
}