Editor tool code for architecture editing. It caches results of expensive shape and geometry queries (outlines, road lookup, elevation cuts, barrier/retaining shapes) so repeated queries within an edit/frame return the same resolved object; provides helpers to find road parts under a point and to get building extents and sections.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// What the plan resolves to, held between frames: resolved once per EDIT, because every answer walks the whole curve.
public partial class ArchTool
{
static readonly List<Vector2> NoOutline = new();
readonly Dictionary<(Type Type, int Id), object> resolved = new();
ArchElevationCut section;
Vector2 roadUnderAt;
ArchRoadPart roadUnder;
bool roadUnderTaken;
// The ceiling plane the cursor may land on. Held between frames like every other resolve: the cursor is
// asked several times a frame and this walks the room's cuts to find any recess in it.
float overhead;
bool overheadTaken;
// Every edit funnels here, so nothing resolved before one survives it; a handle drag edits per frame without committing.
public void Revise()
{
resolved.Clear();
section = null;
roadUnderTaken = false;
overheadTaken = false;
cursorTaken = false;
projectedLayers = null;
// Every face of the build has just been remade, so the probed ones are somebody else's geometry.
ForgetBuiltFaces();
}
public T Resolved<T>( int id, Func<T> resolve )
{
if ( id == 0 )
{
return resolve();
}
var key = (typeof( T ), id);
if ( resolved.TryGetValue( key, out var held ) )
{
return (T)held;
}
var found = resolve();
resolved[key] = found;
return found;
}
// The cached loop itself, not a copy - reshape a room via ArchFloorGen.
public List<Vector2> Outline( ArchRoom room )
{
if ( room is null )
{
return NoOutline;
}
return Resolved( room.Id, () => ArchFloorGen.Footprint( room ) );
}
// Nearest scans a whole dense walk, and the overlay asks every frame.
public ArchRoadPart RoadUnder( Vector2 point )
{
if ( roadUnderTaken && roadUnderAt == point )
{
return roadUnder;
}
roadUnderTaken = true;
roadUnderAt = point;
return roadUnder = Plan.Roads().Count == 0 ? null : RoadAt( point, out _ );
}
// Only ever ONE, and keyed on depth as well as building: scrubbing the section changes it without the plan changing, so Revise never fires.
public ArchElevationCut Section( ArchBuilding building )
{
var axis = Axis;
var depth = Depth( axis );
if ( section is not null && section.Matches( building, axis, depth ) )
{
return section;
}
return section = ArchElevation.Cut( Plan, Kit, building, axis, depth );
}
// Framing and section defaults both ask per frame.
public BBox Extent( ArchBuilding building )
{
var id = building?.Id ?? 0;
return Resolved( id, () => ArchLocate.Find( this, id != 0 ? id.ToString() : "plan" ).Frame );
}
public ArchCurve Curve( ArchFencePart fence ) => Resolved( fence.Id, fence.Curve );
public ArchBarrierShape Barrier( ArchFencePart fence )
=> Resolved( fence.Id, () => ArchBarrierShape.Resolve( Curve( fence ), fence.Barrier, 0f, 0f, Kit ) );
// A whole terrain walk per bay, so the overlay asking every frame has to be held like every other resolve.
public ArchRetainingShape Retaining( ArchFencePart fence )
=> Resolved( fence.Id, () => ArchRetainingShape.Resolve( Barrier( fence ), Kit, new ArchGround( Scene ) ) );
// Four subtools and the overlay used to walk every centreline per frame for this.
public ArchRoadPart RoadAt( Vector2 point, out ArchFrame frame ) => RoadAt( point, road => road.Reach(), out frame );
public ArchRoadPart RoadAt( Vector2 point, Func<ArchRoadPart, float> reach, out ArchFrame frame )
{
ArchRoadPart best = null;
var nearest = float.MaxValue;
frame = default;
foreach ( var road in Plan.Roads() )
{
if ( !Resolved( road.Id, road.Curve ).Nearest( point, out var station, out var gap ) )
{
continue;
}
if ( gap > reach( road ) || gap >= nearest )
{
continue;
}
nearest = gap;
best = road;
frame = station;
}
return best;
}
}