A helper class used by AutomaticWardrobeFit that collects open mesh edges (fabric boundaries) from welded vertex positions and spatially buckets them, then computes a weight for a point based on distance to the nearest boundary edge. It welds nearby vertices, finds unique triangle edges (edges used by only one face), stores those edges into cubic cells with padding, and computes an interior weight with a smooth falloff based on distance.
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));
}
}
}