Code/CharacterKit/AutomaticWardrobeFit.Boundary.cs

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.

Native Interop
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));
		}
	}
}