Editor/Output/ArchMesh.Extrude.cs

A partial class for ArchMesh that computes extruded mesh rings along a path. It generates per-path "rings" of vertex positions from a 2D cross-section, handling orientation, mitre/stretching at corners, and closed or open paths.

Native Interop
using System;
using System.Collections.Generic;
using HalfEdgeMesh;
using Sandbox;

namespace Sunless.Architecture;

public sealed partial class ArchMesh
{
	// One frame for the whole path - per-ring derivation spun the section on near-vertical segments.
	static List<Vector3[]> Rings( IReadOnlyList<Vector3> path, IReadOnlyList<Vector2> section, float scale, Rotation orientation, bool loop )
	{
		var rings = new List<Vector3[]>();
		var right = Seed( Direction( path, 0, loop ), orientation );

		for ( var index = 0; index < path.Count; index++ )
		{
			var incoming = Direction( path, index - 1, loop );
			var outgoing = Direction( path, index, loop );
			var forward = (incoming + outgoing).Normal;

			if ( forward.IsNearZeroLength )
			{
				forward = outgoing;
			}

			right = (right - forward * Vector3.Dot( right, forward )).Normal;

			if ( right.IsNearZeroLength )
			{
				right = Seed( forward, orientation );
			}

			var up = Vector3.Cross( forward, right ).Normal;

			// The bisector-plane ring stretches across the turn, or the runs leave a notch instead of a mitre.
			var lean = Vector3.Dot( forward, outgoing );
			var axis = Vector3.Cross( incoming, outgoing );
			var stretch = axis.IsNearZeroLength || lean < 0.15f ? Vector3.Zero : Vector3.Cross( axis.Normal, forward ).Normal;
			var mitre = stretch.IsNearZeroLength ? 0f : 1f / lean - 1f;

			var ring = new Vector3[section.Count];

			for ( var point = 0; point < section.Count; point++ )
			{
				var offset = right * (section[point].x * scale) + up * (section[point].y * scale);
				ring[point] = path[index] + offset + stretch * (Vector3.Dot( offset, stretch ) * mitre);
			}

			rings.Add( ring );
		}

		return rings;
	}

	// Wrap on a closed run, clamp on an open one - end rings sit square to their own run.
	static Vector3 Direction( IReadOnlyList<Vector3> path, int index, bool loop )
	{
		var count = path.Count;

		if ( !loop )
		{
			var clamped = Math.Clamp( index, 0, count - 2 );
			return (path[clamped + 1] - path[clamped]).Normal;
		}

		var from = ((index % count) + count) % count;

		return (path[(from + 1) % count] - path[from]).Normal;
	}

	static Vector3 Seed( Vector3 forward, Rotation orientation )
	{
		var reference = orientation.Up;

		if ( MathF.Abs( Vector3.Dot( forward, reference ) ) > 0.99f )
		{
			reference = orientation.Forward;
		}

		return Vector3.Cross( reference, forward ).Normal;
	}
}