Editor/Effigy/MeshClip.cs
using System;
using System.Collections.Generic;
namespace Effigy;
/// <summary>
/// Keep the half of a mesh on one side of a plane, clipping faces that straddle it.
///
/// THIS IS A VIEW, NOT A FEATURE. Shell builds interiors nobody can look at; a cutaway hides the
/// near half while looking, without writing anything into the history. Faces that cross the plane
/// are clipped in place so the cut reads as a hard line rather than as missing polygons. There is
/// no cap — an open cut is the point of a section view, and capping would pretend the kernel had
/// booleaned a solid it has not.
/// </summary>
public static class MeshClip
{
/// <summary>
/// Everything on the positive side of the plane through <paramref name="origin"/> with
/// <paramref name="normal"/>, including the plane itself. Vertices on the negative side are
/// dropped; edges that cross emit a new vertex at the intersection.
/// </summary>
public static PolyMesh Keep( PolyMesh mesh, Vec3 origin, Vec3 normal, float tolerance = 1e-5f )
{
if ( mesh is null )
throw new ArgumentNullException( nameof( mesh ) );
var n = normal.Normal;
if ( n.LengthSquared < 0.5f )
return mesh.Clone();
var result = new PolyMesh();
foreach ( var face in mesh.Faces )
{
if ( face.Count < 3 )
continue;
var clipped = ClipFace( mesh, face, origin, n, tolerance );
if ( clipped.Count < 3 )
continue;
var indices = new int[clipped.Count];
var uvs = new Vec2[clipped.Count];
for ( var i = 0; i < clipped.Count; i++ )
{
indices[i] = result.AddVertex( clipped[i].Position );
uvs[i] = clipped[i].Uv;
}
result.AddFace( indices, uvs, face.Material );
}
return result;
}
readonly struct ClipVert
{
public readonly Vec3 Position;
public readonly Vec2 Uv;
public ClipVert( Vec3 position, Vec2 uv )
{
Position = position;
Uv = uv;
}
}
static List<ClipVert> ClipFace( PolyMesh mesh, Face face, Vec3 origin, Vec3 normal, float tolerance )
{
var polygon = new List<ClipVert>( face.Count );
for ( var i = 0; i < face.Count; i++ )
{
var uv = face.UVs is not null && i < face.UVs.Length ? face.UVs[i] : default;
polygon.Add( new ClipVert( mesh.Positions[face.Indices[i]], uv ) );
}
// Sutherland–Hodgman against one plane: walk each edge, emit the intersection when the
// edge crosses, and keep the vertex when it sits on the kept side.
var output = new List<ClipVert>( polygon.Count + 2 );
for ( var i = 0; i < polygon.Count; i++ )
{
var current = polygon[i];
var previous = polygon[( i + polygon.Count - 1 ) % polygon.Count];
var currentDist = Vec3.Dot( current.Position - origin, normal );
var previousDist = Vec3.Dot( previous.Position - origin, normal );
var currentIn = currentDist >= -tolerance;
var previousIn = previousDist >= -tolerance;
if ( currentIn )
{
if ( !previousIn )
output.Add( Intersect( previous, current, previousDist, currentDist ) );
output.Add( current );
}
else if ( previousIn )
{
output.Add( Intersect( previous, current, previousDist, currentDist ) );
}
}
return output;
}
static ClipVert Intersect( ClipVert a, ClipVert b, float da, float db )
{
var denom = da - db;
// Both on the plane, or an edge so close the numbers vanish: pick B rather than divide.
var t = MathF.Abs( denom ) < 1e-12f ? 1f : da / denom;
t = Math.Clamp( t, 0f, 1f );
return new ClipVert( a.Position + ( b.Position - a.Position ) * t, a.Uv + ( b.Uv - a.Uv ) * t );
}
}