Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Documentation~/EXPERIMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@
4. Документировать результат здесь ДО мержа.
5. Если ломает — реверт. Не компенсировать другим фиксом.

## Эксперимент 2026-08-06 — Линейная фильтрация overlap-ячеек

- **Проблема:** попарная проверка всех face в каждой ячейке сетки имела худший случай
`O(gridCells * faces²)` и позволяла патологическому UV-мешу надолго блокировать Editor.
- **Изменение:** число face, имеющих общую вершину с текущим face, вычисляется линейно через
счётчики вершин, пар и троек (формула включений-исключений). Наличие остальных face означает overlap.
- **Сохранённый контракт:** соседние face с общей вершиной игнорируются, а не смежные face в общей
ячейке по-прежнему помечаются как overlap.
- **Проверка:** добавлены EditMode-тесты для обоих случаев; сложные модели необходимо проверить
локально в Unity Editor по стандартному протоколу.

## Что НЕ работает (уроки из 5 отклонённых PR и ~10 ревертов)

- **Affine UV0→UV2 mapping** → экстраполяция за пределы шелов → overlap (PR #48, reverted)
Expand Down
114 changes: 84 additions & 30 deletions Editor/SpatialPartitioner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,12 @@ public static ShellPartitionResult[] PartitionShells(
continue;
}

// Step 1: Build face adjacency (needed for both overlap detection and flood-fill)
var faceVerts = BuildFaceVertexSets(shell.faceIndices, triangles);
// Step 1: Build face adjacency for flood-fill
var adjacency = BuildFaceAdjacency(shell.faceIndices, triangles);

// Step 2: Overlap detection — only flag faces sharing a grid cell
// with a NON-ADJACENT face (no shared vertex)
var overlappingFaces = DetectOverlap(shell, uv0, triangles, faceVerts);
var overlappingFaces = DetectOverlap(shell, uv0, triangles);
r.hasOverlap = overlappingFaces.Count > 0;

if (!r.hasOverlap)
Expand Down Expand Up @@ -218,40 +217,46 @@ public static int[] GetPartitionFaces(
return faces.ToArray();
}

// ════════════════════════════════════════════════════════════
// Per-face vertex set (for fast adjacency check in overlap detection)
// ════════════════════════════════════════════════════════════

static Dictionary<int, HashSet<int>> BuildFaceVertexSets(List<int> faceIndices, int[] triangles)
readonly struct FaceVertexKey : System.IEquatable<FaceVertexKey>
{
var result = new Dictionary<int, HashSet<int>>(faceIndices.Count);
foreach (int f in faceIndices)
public readonly int a;
public readonly int b;
public readonly int c;

public FaceVertexKey(int v0, int v1, int v2)
{
var set = new HashSet<int>();
set.Add(triangles[f * 3]);
set.Add(triangles[f * 3 + 1]);
set.Add(triangles[f * 3 + 2]);
result[f] = set;
if (v0 > v1) { int t = v0; v0 = v1; v1 = t; }
if (v1 > v2) { int t = v1; v1 = v2; v2 = t; }
if (v0 > v1) { int t = v0; v0 = v1; v1 = t; }
a = v0;
b = v1;
c = v2;
}
return result;
}

static bool FacesShareVertex(Dictionary<int, HashSet<int>> faceVerts, int fA, int fB)
{
if (!faceVerts.TryGetValue(fA, out var setA)) return false;
if (!faceVerts.TryGetValue(fB, out var setB)) return false;
foreach (int v in setA)
if (setB.Contains(v)) return true;
return false;
public bool Equals(FaceVertexKey other)
{
return a == other.a && b == other.b && c == other.c;
}

public override bool Equals(object obj) => obj is FaceVertexKey other && Equals(other);

public override int GetHashCode()
{
unchecked
{
int hash = a;
hash = (hash * 397) ^ b;
return (hash * 397) ^ c;
}
}
}

// ════════════════════════════════════════════════════════════
// Overlap detection: grid rasterization + vertex-sharing filter
// ════════════════════════════════════════════════════════════

static HashSet<int> DetectOverlap(
UvShell shell, Vector2[] uv0, int[] triangles,
Dictionary<int, HashSet<int>> faceVerts)
UvShell shell, Vector2[] uv0, int[] triangles)
{
var overlapping = new HashSet<int>();

Expand Down Expand Up @@ -294,27 +299,76 @@ static HashSet<int> DetectOverlap(
}
}

// Count incidences instead of comparing every pair in a cell. For a
// face, inclusion-exclusion gives the number of cell faces sharing
// at least one of its vertices. Any remaining face is non-adjacent.
// This keeps detection linear in face-cell memberships even when a
// crafted mesh makes every face cover every grid cell.
var vertexCounts = new Dictionary<int, int>();
var pairCounts = new Dictionary<long, int>();
var tripleCounts = new Dictionary<FaceVertexKey, int>();

foreach (var kv in cellFaces)
{
var list = kv.Value;
if (list.Count < 2) continue;

vertexCounts.Clear();
pairCounts.Clear();
tripleCounts.Clear();

for (int i = 0; i < list.Count; i++)
{
int f = list[i];
int i0 = triangles[f * 3], i1 = triangles[f * 3 + 1], i2 = triangles[f * 3 + 2];
IncrementCount(vertexCounts, i0);
if (i1 != i0) IncrementCount(vertexCounts, i1);
if (i2 != i0 && i2 != i1) IncrementCount(vertexCounts, i2);

if (i0 != i1) IncrementCount(pairCounts, VertexPairKey(i0, i1));
if (i0 != i2) IncrementCount(pairCounts, VertexPairKey(i0, i2));
if (i1 != i2) IncrementCount(pairCounts, VertexPairKey(i1, i2));
if (i0 != i1 && i0 != i2 && i1 != i2)
IncrementCount(tripleCounts, new FaceVertexKey(i0, i1, i2));
}

for (int i = 0; i < list.Count; i++)
{
for (int j = i + 1; j < list.Count; j++)
int f = list[i];
int i0 = triangles[f * 3], i1 = triangles[f * 3 + 1], i2 = triangles[f * 3 + 2];
int sharedCount = vertexCounts[i0];

if (i1 != i0)
sharedCount += vertexCounts[i1] - pairCounts[VertexPairKey(i0, i1)];
if (i2 != i0 && i2 != i1)
{
if (!FacesShareVertex(faceVerts, list[i], list[j]))
sharedCount += vertexCounts[i2] - pairCounts[VertexPairKey(i0, i2)];
if (i1 != i0)
{
overlapping.Add(list[i]);
overlapping.Add(list[j]);
sharedCount -= pairCounts[VertexPairKey(i1, i2)];
sharedCount += tripleCounts[new FaceVertexKey(i0, i1, i2)];
}
}

if (sharedCount < list.Count)
overlapping.Add(f);
}
}

return overlapping;
}

static long VertexPairKey(int v0, int v1)
{
return v0 < v1 ? ((long)v0 << 32) | (uint)v1 : ((long)v1 << 32) | (uint)v0;
}

static void IncrementCount<TKey>(Dictionary<TKey, int> counts, TKey key)
{
counts.TryGetValue(key, out int count);
counts[key] = count + 1;
}

// ════════════════════════════════════════════════════════════
// Face adjacency graph
// ════════════════════════════════════════════════════════════
Expand Down
74 changes: 74 additions & 0 deletions Tests/Editor/SpatialPartitionerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;

namespace SashaRX.UnityMeshLab.Tests
{
public class SpatialPartitionerTests
{
[Test]
public void PartitionShells_FacesSharingGridCellsAndVertex_DoNotOverlap()
{
var uv = BuildFullBoundsUvs(4);
var triangles = new[]
{
0, 1, 2,
0, 3, 4,
0, 5, 6,
0, 7, 8
};

var result = PartitionSingleShell(uv, triangles);

Assert.IsFalse(result.hasOverlap,
"Faces that only meet through a shared vertex must not be treated as UV overlap");
}

[Test]
public void PartitionShells_NonAdjacentFaceInSharedGridCells_DetectsOverlap()
{
var uv = BuildFullBoundsUvs(4);
var triangles = new[]
{
0, 1, 2,
0, 3, 4,
0, 5, 6,
9, 7, 8
};

var result = PartitionSingleShell(uv, triangles);

Assert.IsTrue(result.hasOverlap,
"A face sharing grid cells but no vertex must be treated as UV overlap");
}

static Vector2[] BuildFullBoundsUvs(int faceCount)
{
var uv = new Vector2[faceCount * 2 + 2];
uv[0] = Vector2.zero;
for (int i = 0; i < faceCount; i++)
{
uv[i * 2 + 1] = Vector2.right;
uv[i * 2 + 2] = Vector2.up;
}
uv[9] = Vector2.zero;
return uv;
}

static SpatialPartitioner.ShellPartitionResult PartitionSingleShell(
Vector2[] uv, int[] triangles)
{
var shell = new UvShell
{
shellId = 0,
boundsMin = Vector2.zero,
boundsMax = Vector2.one,
faceIndices = new List<int> { 0, 1, 2, 3 }
};
var vertices = new Vector3[uv.Length];

return SpatialPartitioner.PartitionShells(
new List<UvShell> { shell }, uv, triangles, vertices)[0];
}
}
}
3 changes: 3 additions & 0 deletions Tests/Editor/SpatialPartitionerTests.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading