Editor/Tool/ArchWorkPlane.cs
using System;
using Sandbox;
namespace Sunless.Architecture;
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; }
// Pre-snap ray hit
public Vector2 Free { get; init; }
public ArchSnapKind Snap { get; init; }
public ArchWall SnappedTo { get; init; }
public Vector2 SnapHeading { get; init; }
// Snapped to a surface ABOVE this storey — align head, not base
public bool SnapAbove { get; init; }
public IReadOnlyList<Vector2> Surface { get; init; }
public float Height { get; init; }
public ArchViewAxis Axis { get; init; }
public bool OnGround { get; init; }
public ArchRoofPart OnDeck { get; init; }
public ArchPlatformPart OnPlatform { get; init; }
public bool InElevation => Axis is ArchViewAxis.Front or ArchViewAxis.Side;
}
public readonly struct ArchWorkPlane {
public ArchViewAxis Axis { get; init; }
public float Depth { get; init; }
public float Level { get; init; }
public float Soffit { get; init; }
// Locks the work plane to the surface the gesture began on
public float? Standing { get; init; }
public ArchRoofPart StandingDeck { get; init; }
public ArchPlan Plan { get; init; }
public ArchKit Kit { get; init; }
public int Storey { get; init; }
public ArchSnapReach 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.SnapReach()
};
}
// Walls snap ahead of grid — decks use only their own walls, ground adds storey edges
ArchWallSnapped Settle( ArchGridService grid, Vector2 free, ArchRoofPart deck, out Vector2 snapped ) {
var walls = ArchWallSnap.On( Plan, Storey, deck );
return deck is not null
? Settled( grid, free, ArchWallSnap.Nearest( walls, free, WallReach ), out snapped )
: Settled( grid, free, ArchWallSnap.Nearest( walls, ArchStoreyEdges.On( Plan, Kit, Storey ), free, WallReach ), out snapped );
}
ArchWallSnapped Settled( ArchGridService grid, Vector2 free, IEnumerable<ArchSnapLoop> offered, out Vector2 snapped ) {
return Settled( grid, free, ArchWallSnap.Nearest( offered, free, WallReach ), out snapped );
}
// Face snaps: flush across the wall, grid-snapped along it
ArchWallSnapped Settled( ArchGridService grid, Vector2 free, ArchWallSnapped found, out Vector2 snapped ) {
if ( !found.Took ) {
snapped = grid.Base( free );
return found;
}
if ( found.Kind != ArchSnapKind.Face || found.Heading.Length < 0.001f ) {
snapped = found.Point;
return found;
}
// Clamped to the snap line so it can't overshoot the wall's end
var travelled = Math.Clamp( Vector2.Dot( grid.Base( free ) - found.Start, found.Heading ), 0f, found.Length );
snapped = found.Start + found.Heading * travelled;
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;
}
bool Overhead( ArchGridService grid, Ray ray, out ArchCursor cursor ) {
cursor = default;
if ( ray.Forward.z <= 0.001f ) {
return false;
}
// Only count the ceiling plane where the storey actually stands
var ceiling = Soffit > Level
&& Crosses( ray, Vector3.Up, Soffit, out var flat )
&& ArchSoffit.Ceilinged( Plan, Storey, new Vector2( flat.x, flat.y ) )
? flat
: default( Vector3? );
var surface = default( List<Vector2> );
if ( ArchSoffit.Struck( Plan, Kit, Storey, ray, out var struck, out _, out var outline )
&& (ceiling is not { } standing || Reach( ray, struck ) < Reach( ray, standing )) ) {
ceiling = struck;
surface = outline;
}
if ( ceiling is not { } hit ) {
return false;
}
var free = new Vector2( hit.x, hit.y );
// Soffit outline snaps first, storey edges behind it
var offered = ArchStoreyEdges.On( Plan, Kit, Storey );
if ( surface is not null ) {
offered.Insert( 0, new ArchSnapLoop( surface, true ) );
}
var snap = Settled( grid, free, offered, 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,
SnapHeading = snap.Heading,
SnapAbove = snap.Above,
Surface = surface,
Height = hit.z,
Axis = ArchViewAxis.Top,
Face = ArchCursorFace.Ceiling
};
return true;
}
// Locks to the surface the gesture started on
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,
SnapHeading = snap.Heading,
SnapAbove = snap.Above,
Height = height,
Axis = ArchViewAxis.Top
};
return true;
}
bool Deck( ArchGridService grid, Ray ray, out ArchCursor cursor ) {
cursor = default;
if ( Plan is null || Kit is null ) {
return false;
}
var roofed = Roof( grid, ray, out var onRoof );
var paved = Platform( grid, ray, out var onPlatform );
if ( roofed && (!paved || Reach( ray, onRoof.World ) <= Reach( ray, onPlatform.World )) ) {
return Take( onRoof, out cursor );
}
return paved && Take( onPlatform, out cursor );
}
bool Roof( ArchGridService grid, Ray ray, out ArchCursor cursor ) {
cursor = default;
if ( !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 );
// Height is in building-local space; lift converts to scene space for the cursor
var lift = Plan.HostOf( roof ) is ArchBuilding on ? ArchAsks.Lift( Plan, on, Kit ) : 0f;
cursor = new ArchCursor {
Found = true,
World = new Vector3( point.x, point.y, hit.z + lift ),
Plan = point,
Free = free,
Snap = snap.Kind,
SnappedTo = snap.Wall,
SnapHeading = snap.Heading,
SnapAbove = snap.Above,
Height = hit.z,
Axis = ArchViewAxis.Top,
OnDeck = roof
};
return true;
}
bool Platform( ArchGridService grid, Ray ray, out ArchCursor cursor ) {
cursor = default;
if ( !ArchPlatformDeck.Struck( Plan, Kit, Storey, ray, out var struck ) ) {
return false;
}
var free = new Vector2( struck.World.x, struck.World.y );
var surface = struck.Platform.Outline();
var offered = ArchStoreyEdges.On( Plan, Kit, Storey );
offered.Insert( 0, new ArchSnapLoop( surface, false ) );
// Platform outline + storey walls — both snap, unlike soffits where walls are below
var snap = Settled( grid, free,
ArchWallSnap.Nearest( ArchWallSnap.On( Plan, Storey, null ), offered, free, WallReach ), out var point );
var height = ArchPlatformDeck.Seated( Plan, struck.On, Kit, struck.Platform, point );
cursor = new ArchCursor {
Found = true,
World = struck.World.WithX( point.x ).WithY( point.y ),
Plan = point,
Free = free,
Snap = snap.Kind,
SnappedTo = snap.Wall,
SnapHeading = snap.Heading,
SnapAbove = snap.Above,
Surface = surface,
Height = height,
Axis = ArchViewAxis.Top,
OnPlatform = struck.Platform
};
return true;
}
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;
}
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,
SnapHeading = snap.Heading,
SnapAbove = snap.Above,
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 );
}