Skip to content
Merged
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
32 changes: 32 additions & 0 deletions src/NwcIO/Model/Sectioning/NwcMeshClipper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ public static NwcMesh Clip(NwcMesh mesh, IReadOnlyList<NwcSectionPlane> planes)
return mesh;
}

// The planes are in world space and the vertices in the mesh's local space: cut with the planes mapped into
// local space, so the result keeps its vertices and Transform
if (mesh.Transform is not null)
{
double[] m = mesh.Transform.ToMatrix4x4();
activePlanes = activePlanes.Select(p => ToLocalSpace(p, m)).ToList();
}

var currentVerts = new List<Vector3d>(mesh.Vertices);
var currentIndices = new List<int>(mesh.Indices);

Expand Down Expand Up @@ -57,6 +65,30 @@ public static NwcMesh Clip(NwcMesh mesh, NwcSectionBox sectionBox)
return Clip(mesh, sectionBox.ToSectionPlanes());
}

/// <summary>
/// Maps a world-space plane n·x = d (n normalized, as <see cref="ClipMeshByPlane"/> reads it) into the local space
/// of a mesh whose world position is x = M·l + t, with <paramref name="m"/> laid out as in
/// <see cref="NwcMesh.GetWorldVertices"/>: (Mᵀn)·l = d − n·t, then normalized.
/// </summary>
private static NwcSectionPlane ToLocalSpace(NwcSectionPlane plane, double[] m)
{
var n = plane.Normal.Normalize();
var localNormal = new Vector3d(
m[0] * n.X + m[1] * n.Y + m[2] * n.Z,
m[4] * n.X + m[5] * n.Y + m[6] * n.Z,
m[8] * n.X + m[9] * n.Y + m[10] * n.Z);
double localDistance = plane.Distance - (n.X * m[12] + n.Y * m[13] + n.Z * m[14]);

double length = localNormal.Length;
if (!(length > 1e-12) || !double.IsFinite(length) || !double.IsFinite(localDistance))
{
// Like GetWorldVertices, which keeps the local vertices for a transform it can't apply
return plane;
}

return new NwcSectionPlane(plane.PlaneIndex, localNormal / length, localDistance / length, plane.IsEnabled, plane.Alignment);
}

private static void ClipMeshByPlane(
List<Vector3d> inVerts,
List<int> inIndices,
Expand Down
43 changes: 43 additions & 0 deletions tests/NwcIO.Tests/SectioningTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,49 @@ public void NwcMeshClipper_Clip_DisabledPlane_LeavesMeshUnchanged()
Assert.Equal(mesh.Indices.Count, clipped.Indices.Count);
}

[Fact]
public void NwcMeshClipper_Clip_TranslatedMesh_CutsInWorldSpace()
{
var mesh = new NwcMesh
{
Id = 1,
Vertices = [new(0, 0, 0), new(1, 0, 0), new(0, 1, 0)],
Indices = [0, 1, 2],
Transform = new NwcTransform { Id = 1, Translation = new Vector3d(0, 0, 100) },
};

var above = NwcMeshClipper.Clip(mesh, [NwcSectionPlane.CreateBottom(50)]);
var below = NwcMeshClipper.Clip(mesh, [NwcSectionPlane.CreateTop(50)]);

Assert.Equal(1, above.TriangleCount);
Assert.Same(mesh.Transform, above.Transform);
Assert.All(above.GetWorldVertices(), v => Assert.Equal(100, v.Z, 9));
Assert.Equal(0, below.TriangleCount);
}

[Fact]
public void NwcMeshClipper_Clip_RotatedMesh_CutsInWorldSpace()
{
// A quarter turn about Z maps local X onto world Y: the triangle spans world Y 2..4
double s = Math.Sqrt(0.5);
var mesh = new NwcMesh
{
Id = 1,
Vertices = [new(2, 0, 0), new(4, 0, 0), new(4, 0, 1)],
Indices = [0, 1, 2],
Transform = new NwcTransform { Id = 1, Translation = new Vector3d(10, 0, 0), Rotation = new Quaterniond(0, 0, s, s) },
};

var clipped = NwcMeshClipper.Clip(mesh, [NwcSectionPlane.CreateFront(3)]);

Assert.True(clipped.TriangleCount > 0);
var world = clipped.GetWorldVertices();
Assert.All(world, v => Assert.True(v.Y >= 3 - 1e-9, $"Y = {v.Y}"));
Assert.Contains(world, v => Math.Abs(v.Y - 4) < 1e-9);
Assert.Contains(world, v => Math.Abs(v.Y - 3) < 1e-9);
Assert.All(world, v => Assert.Equal(10, v.X, 9));
}

[Theory]
[InlineData(0, true)]
[InlineData(1, true)]
Expand Down
Loading