Editor/Tool/ArchWorkPlane.cs

Editor tool code that maps a camera ray to architecture editing coordinates. Defines ArchCursor and ArchWorkPlane, computes intersections with planes, terrain and roofs, snaps to grid and walls, and returns cursor info for placements.

File Access
using System;
using Sandbox;

namespace Sunless.Architecture;

// Which face of the storey the cursor came to rest on. Looking DOWN you are working the floor, looking UP
// you are working the ceiling over you - that is the whole rule, and it needs no trace: the ray's own
// direction says which of the two planes you are on the near side of.
public enum ArchCursorFace
{
	Floor,
	Ceiling
}

public readonly struct ArchCursor
{
	public bool Found { get; init; }
	public Vector3 World { get; init; }
	public Vector2 Plan { get; init; }
	public ArchCursorFace Face { get; init; }
	// Where the ray actually landed, before the grid took it - the only honest answer for a tool
	// the author has turned snapping off on. Snapping still happens in exactly one place.
	public Vector2 Free { get; init; }
	// What Plan came to rest on instead of the grid. Free is untouched by it - a snap is still a snap, so an
	// author who turned snapping off gets the point the ray landed on and nothing else.
	public ArchSnapKind Snap { get; init; }
	public ArchWall SnappedTo { get; init; }
	public float Height { get; init; }
	public ArchViewAxis Axis { get; init; }
	public bool OnGround { get; init; }
	// The deck it came to rest on, so a placement can file itself on the roof it was drawn against.
	public ArchRoofPart OnDeck { get; init; }

	public bool InElevation => Axis is ArchViewAxis.Front or ArchViewAxis.Side;
}

// The one place a ray becomes a plan coordinate; in elevation, Depth is the section a height snaps on.
public readonly struct ArchWorkPlane
{
	public ArchViewAxis Axis { get; init; }
	public float Depth { get; init; }
	public float Level { get; init; }
	// The ceiling over this storey, so a drag made looking up lands on it rather than on the floor under it.
	public float Soffit { get; init; }
	// The height a gesture already under way is working, so the rest of it stays on the surface it began on.
	public float? Standing { get; init; }
	// And which deck that surface IS, so a pinned gesture snaps to what it is standing on and nothing below it.
	public ArchRoofPart StandingDeck { get; init; }
	// Read for the decks this storey carries: a roof is somewhere to place, not just something to look at.
	public ArchPlan Plan { get; init; }
	public ArchKit Kit { get; init; }
	public int Storey { get; init; }
	// How far a point reaches for a standing wall before the grid takes it. 0 leaves the grid alone.
	public float WallReach { get; init; }

	public static ArchWorkPlane For( ArchTool tool )
	{
		var axis = tool.Axis;

		return new ArchWorkPlane
		{
			Axis = axis == ArchViewAxis.Free ? ArchViewAxis.Top : axis,
			Depth = axis is ArchViewAxis.Front or ArchViewAxis.Side ? tool.Depth( axis ) : tool.LevelHeight,
			Level = tool.LevelHeight,
			Soffit = tool.Overhead(),
			Standing = tool.Standing,
			StandingDeck = tool.StandingDeck,
			Plan = tool.Plan,
			Kit = tool.Kit,
			Storey = tool.Level,
			WallReach = tool.SnapsToWalls ? ArchWallSnap.Reach( tool.WallSnapReach ) : 0f
		};
	}

	// The one place a plan point comes to rest. A wall already standing on THIS surface takes the point ahead of
	// the grid, because rounding to the nearest rung is exactly what pulls a cursor off the corner it was aimed
	// at - but only that surface's own walls, or a parapet's end is dragged down onto a room below the deck.
	ArchWallSnapped Settle( ArchGridService grid, Vector2 free, ArchRoofPart deck, out Vector2 snapped )
	{
		var found = ArchWallSnap.Nearest( ArchWallSnap.On( Plan, Storey, deck ), free, WallReach );

		snapped = found.Took ? found.Point : grid.Base( free );

		return found;
	}

	public bool Locate( ArchGridService grid, Scene scene, Ray ray, out ArchCursor cursor )
	{
		cursor = default;

		if ( Axis == ArchViewAxis.Top )
		{
			return Ground( grid, scene, ray, out cursor );
		}

		var normal = Axis == ArchViewAxis.Front ? Vector3.Forward : Vector3.Left;

		if ( !Crosses( ray, normal, Depth, out var hit ) )
		{
			return false;
		}

		var plan = Axis == ArchViewAxis.Front
			? new Vector2( Depth, grid.Base( hit.y ) )
			: new Vector2( grid.Base( hit.x ), Depth );

		var height = grid.Height( hit.z );

		cursor = new ArchCursor
		{
			Found = true,
			World = new Vector3( plan.x, plan.y, height ),
			Plan = plan,
			Free = Axis == ArchViewAxis.Front ? new Vector2( Depth, hit.y ) : new Vector2( hit.x, Depth ),
			Height = height,
			Axis = Axis
		};

		return true;
	}

	// Looking UP is working the ceiling. The floor is behind the camera then, and taking the drag down to it
	// puts a coffer's outline on the slab you are standing on instead of on the plaster you were pointing at.
	bool Overhead( ArchGridService grid, Ray ray, out ArchCursor cursor )
	{
		cursor = default;

		if ( ray.Forward.z <= 0.001f || Soffit <= Level || !Crosses( ray, Vector3.Up, Soffit, out var hit ) )
		{
			return false;
		}

		var free = new Vector2( hit.x, hit.y );
		var snap = Settle( grid, free, null, out var point );

		cursor = new ArchCursor
		{
			Found = true,
			World = new Vector3( point.x, point.y, Soffit ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			Height = Soffit,
			Axis = ArchViewAxis.Top,
			Face = ArchCursorFace.Ceiling
		};

		return true;
	}

	// A gesture already under way works the surface it STARTED on, whatever the camera does next. Re-projecting
	// every frame onto the storey's own plane is why a deck 128 inches up could only be dragged on from directly
	// overhead: from any other angle the ray crosses that plane out past the building, so the drag ran away across
	// the yard behind it. Nothing else is offered while pinned - not the terrain, not the ceiling - because a
	// gesture that changes surface halfway through is a gesture with two starts.
	bool Pinned( ArchGridService grid, Ray ray, float height, out ArchCursor cursor )
	{
		cursor = default;

		if ( !Crosses( ray, Vector3.Up, height, out var hit ) )
		{
			return false;
		}

		var free = new Vector2( hit.x, hit.y );
		var snap = Settle( grid, free, StandingDeck, out var point );

		cursor = new ArchCursor
		{
			Found = true,
			World = new Vector3( point.x, point.y, height ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			Height = height,
			Axis = ArchViewAxis.Top
		};

		return true;
	}

	// A DECK IS SOMEWHERE TO PLACE. Offered as its own candidate so a parapet, a dormer or a chimney can be
	// dragged on from the same three-quarter view everything else is drawn from; its height is the deck's own
	// surface, never a grid step, because that height was settled when the roof was authored.
	bool Deck( ArchGridService grid, Ray ray, out ArchCursor cursor )
	{
		cursor = default;

		if ( Plan is null || Kit is null || !ArchAsks.RoofStruck( Plan, Storey, Kit, ray, out var hit, out var roof ) )
		{
			return false;
		}

		var free = new Vector2( hit.x, hit.y );
		var snap = Settle( grid, free, roof, out var point );

		cursor = new ArchCursor
		{
			Found = true,
			World = new Vector3( point.x, point.y, hit.z ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			Height = hit.z,
			Axis = ArchViewAxis.Top,
			OnDeck = roof
		};

		return true;
	}

	// Nearest wins: a plane alone would put a hilltop road down the far side of it, and on an upper storey the plane has to win.
	bool Ground( ArchGridService grid, Scene scene, Ray ray, out ArchCursor cursor )
	{
		if ( Standing is { } pinned )
		{
			return Pinned( grid, ray, pinned, out cursor );
		}

		if ( Overhead( grid, ray, out cursor ) )
		{
			return true;
		}

		// A deck is offered only to a ray looking DOWN on it: from underneath the same crossing is the soffit,
		// and placing there would put a parapet on the roof you are standing under.
		var onDeck = default( ArchCursor );
		var deck = ray.Forward.z < -0.001f && Deck( grid, ray, out onDeck );

		cursor = default;

		var ground = Vector3.Zero;
		var onPlane = Crosses( ray, Vector3.Up, Level, out var plane );
		var onGround = scene.IsValid() && ArchTerrain.Ground( scene, ray, out ground );

		if ( !onPlane && !onGround )
		{
			return deck && Take( onDeck, out cursor );
		}

		var terrain = onGround && (!onPlane || Reach( ray, ground ) < Reach( ray, plane ));
		var nearest = terrain ? ground : plane;

		if ( deck && Reach( ray, onDeck.World ) < Reach( ray, nearest ) )
		{
			return Take( onDeck, out cursor );
		}

		var free = new Vector2( nearest.x, nearest.y );
		var snap = Settle( grid, free, null, out var point );

		cursor = new ArchCursor
		{
			Found = true,
			World = new Vector3( point.x, point.y, nearest.z ),
			Plan = point,
			Free = free,
			Snap = snap.Kind,
			SnappedTo = snap.Wall,
			Height = Level,
			Axis = ArchViewAxis.Top,
			OnGround = terrain
		};

		return true;
	}

	static bool Take( ArchCursor found, out ArchCursor cursor )
	{
		cursor = found;

		return true;
	}

	static bool Crosses( Ray ray, Vector3 normal, float offset, out Vector3 hit )
	{
		hit = default;

		var facing = Vector3.Dot( ray.Forward, normal );

		if ( MathF.Abs( facing ) < 0.0001f )
		{
			return false;
		}

		var distance = (offset - Vector3.Dot( ray.Position, normal )) / facing;

		if ( distance < 0f )
		{
			return false;
		}

		hit = ray.Position + ray.Forward * distance;

		return true;
	}

	static float Reach( Ray ray, Vector3 hit ) => Vector3.Dot( hit - ray.Position, ray.Forward );
}