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.
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 );
}