Editor/Tool/ArchTool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
public abstract partial class ArchTool : EditorTool {
const float SettleDelay = 0.25f;
public static ArchTool Active { get; private set; }
public ArchPlan Plan { get; private set; } = new();
public ArchKit Kit { get; private set; } = new();
public List<ArchArchetype> Archetypes { get; private set; } = new();
public List<ArchPillarType> PillarTypes { get; private set; } = new();
public int ActiveBuildingId { get; set; }
public int ActiveRoomId { get; set; }
public int Level { get; set; }
public string ActivePreset { get; set; } = "door";
public string ActiveWindow { get; set; } = "sash_4over4";
public ArchSelection Picked { get; set; }
public int Revision { get; private set; }
public bool HideRoofs { get; set; }
public bool HideWalls { get; set; }
public bool HideFloors { get; set; }
public bool HideFoundation { get; set; }
public bool HideTrim { get; set; }
public bool OnlyCurrentLevel { get; set; }
public bool ShowBaked { get; set; } = true;
public float StoreyHeight => ArchBuild.StoreyHeight( ActiveBuilding(), EnsureKit() );
public float LevelHeight => FloorOf( Level );
// Memoised per edit — cursor queries it several times a frame.
public float Overhead() {
if ( overheadTaken ) {
return overhead;
}
overheadTaken = true;
overhead = LevelHeight + EnsureKit().WallHeight - ArchFloorGen.CeilingDepth( EnsureKit() );
if ( ActiveRoom() is { } room && Plan?.OwnerOf( room ) is { } building ) {
ArchFootprint.Bounds( ArchFloorGen.Footprint( room ), out var min, out var max );
overhead = ArchFloorGen.Overhead( Plan, building, room, EnsureKit(), (min + max) * 0.5f );
}
return overhead;
}
public ArchGridService Grid { get; } = new();
ArchLayerTree projectedLayers;
public ArchLayerTree LayerTree {
get {
var tree = projectedLayers ??= ArchLayerTree.Project( Plan );
if ( InsertionTarget is { } target && tree.Find( target.ItemId ) is null ) {
InsertionTarget = null;
}
return tree;
}
}
public ArchLayerRef? InsertionTarget { get; set; }
public ArchLayerRef? SelectedLayer => Picked?.Item is { } item ? LayerTree.Find( item )?.Ref : null;
readonly List<ArchDraft> drafts = new();
public IReadOnlyList<ArchDraft> Drafts => drafts;
public ArchDraft BeginPlacement( ArchKind kind, ArchLayerRef? parent, object payload, string name = null, object owner = null ) {
var draft = new ArchDraft( kind, parent, name ?? $"{ArchKindsAsked.Label( kind )} (draft)", payload, owner );
drafts.Add( draft );
return draft;
}
void SweepDrafts() => drafts.RemoveAll( draft => draft.Owner is not null && !ReferenceEquals( draft.Owner, CurrentTool ) );
public void FinishPlacement( ArchDraft draft, int itemId ) {
draft.Finish( itemId );
drafts.Remove( draft );
FileWithItsGroup( draft, itemId );
Affect( itemId );
}
void FileWithItsGroup( ArchDraft draft, int itemId ) {
if ( itemId == 0 || !ArchKindsAsked.RootsAtPlan( draft.Kind ) ) {
return;
}
if ( ArchLayerGroups.Holding( Plan, itemId ) is not null ) {
return;
}
var wanted = ArchLayerGroups.Find( Plan, draft.Parent?.ItemId ?? 0 )
?? ArchLayerGroups.Find( Plan, EditedGroupId );
ArchLayerGroups.Join( Plan, wanted, itemId );
}
public void CancelPlacement( ArchDraft draft ) {
draft.Cancel();
drafts.Remove( draft );
}
public void ToggleLayerEnabled( ArchLayerRef layer ) {
if ( LayerTree.Find( layer.ItemId ) is not { } node || node.Payload is null ) {
return;
}
var enabled = !node.Enabled;
SetLayerEnabled( layer, enabled );
Commit( enabled ? $"Enable {node.Name}" : $"Disable {node.Name}" );
}
// No Commit — caller batches and commits once.
public void SetLayerEnabled( ArchLayerRef layer, bool enabled ) {
if ( LayerTree.Find( layer.ItemId ) is not { } node || node.Payload is null ) {
return;
}
LayerTree.Record( Plan, node ).Enabled = enabled;
}
public void DeleteLayers( IReadOnlyList<ArchLayerNode> nodes ) {
var wanted = nodes?.Where( node => node?.Payload is not null ).ToList();
if ( wanted is not { Count: > 0 } ) {
return;
}
var scopes = new List<int>();
foreach ( var node in wanted ) {
var layer = node.Ref?.ItemId ?? 0;
scopes.Add( ArchLayerGroups.Holding( Plan, layer )?.Id ?? layer );
new ArchSelection { Item = node.Payload, Room = node.Room, Building = node.Building }.RemoveFrom( Plan );
}
Picked = null;
Affect( scopes );
Touch( wanted.Count > 1 ? $"Delete {wanted.Count} layers" : "Delete" );
}
public ArchKit EnsureKit() {
return Kit ??= new ArchKit();
}
public float FloorOf( int level ) => ArchBuild.FloorOf( ActiveBuilding(), EnsureKit(), level );
string committed;
string pending;
RealTimeSince touched;
int standing;
Ray cursorRay;
ArchCursor cursorFound;
int cursorStamp;
ArchViewAxis cursorAxis;
float cursorDepth;
float cursorStanding;
ArchSnapReach cursorReach;
bool cursorTaken;
public float? Standing { get; private set; }
public ArchRoofPart StandingDeck { get; private set; }
public void StandOn( float height, ArchRoofPart deck = null ) {
Standing = height;
StandingDeck = deck;
}
public void StepOff() {
Standing = null;
StandingDeck = null;
}
public bool SnapsToWalls { get; set; } = true;
public float WallSnapReach { get; set; }
public ArchSnapReach SnapReach() {
if ( !SnapsToWalls ) {
return ArchSnapReach.None;
}
return (CurrentTool as ArchSubtool)?.SnapReach( WallSnapReach ) ?? ArchWallSnap.Reach( WallSnapReach );
}
public override void OnEnabled() {
Active = this;
AllowGameObjectSelection = true;
Plan = ArchStorage.LoadPlan( Scene );
Kit = ArchStorage.LoadKit( Plan.KitName ) ?? new ArchKit();
Archetypes = ArchStorage.LoadArchetypes();
PillarTypes = ArchAsks.PillarTypes();
Revise();
committed = ArchStorage.Snapshot( Plan );
ArchDockPanel.Open();
ArchLayersDockPanel.Open();
ArchLayerInspectorPanel.Open();
}
public override Widget CreateToolbarWidget() => new ArchToolbar( this );
public override void OnDisabled() {
Save();
if ( Active == this ) {
Active = null;
}
}
public override void OnUpdate() {
AllowGameObjectSelection = CurrentTool is not ArchSelectSubtool;
SweepDrafts();
TurnPicked();
Settle();
SettlePreview();
SyncDeletions();
DrawPlan();
}
void TurnPicked() {
if ( !ArchKeys.Pressed( this, KeyCode.R ) || CurrentTool is ArchSubtool subtool && subtool.Turns() ) {
return;
}
if ( Picked?.Item is not ArchBuilding building || !ArchHandles.Pivot( building, out var about ) ) {
return;
}
if ( ArchCarry.Turn( building, about, 90f, Plan ) ) {
Commit( $"Turn {building.Name}" );
}
}
public bool Placing => CurrentTool is ArchSubtool { Placing: true };
public void Touch( string action = "Architecture Edit" ) {
pending = action;
touched = 0f;
Revise();
}
void Settle() {
if ( pending is null || touched < SettleDelay ) {
return;
}
var action = pending;
pending = null;
Commit( action );
}
public void SyncDeletions() {
var root = ArchScene.FindRoot( Scene );
if ( !root.IsValid() ) {
return;
}
var alive = Parts( root );
// An EMPTY root is not a scene somebody deleted parts out of - it is one that has not been written yet, or
// whose whole generated tree went at once. Read as deletions it prunes the entire plan.
if ( alive.Count == 0 ) {
standing = 0;
return;
}
var lost = alive.Count < standing;
standing = alive.Count;
if ( lost ) {
Prune( alive );
}
}
// Porch parts are filed a level deeper than room parts, so the walk is the whole tree, not a fixed depth.
static HashSet<int> Parts( GameObject root ) {
var alive = new HashSet<int>();
foreach ( var node in ArchScene.Descendants( root ) ) {
if ( ArchNames.TrySourceId( node.Name, out var id ) ) {
alive.Add( id );
}
}
return alive;
}
void Prune( IReadOnlySet<int> alive ) {
var removed = Plan.Units.RemoveAll( unit => unit switch {
ArchBuilding building => building.HasContent && !alive.Contains( building.Id ),
ArchRoadPart road => !alive.Contains( road.Id ),
_ => false
} );
// Stairs are plan-wide — a flight can open slabs across buildings.
foreach ( var room in Plan.AllRooms() ) {
removed += room.Stairs.RemoveAll( stair => !alive.Contains( stair.Id ) );
foreach ( var porch in room.Porches ) {
removed += porch.Stairs.RemoveAll( stair => !alive.Contains( stair.Id ) );
}
}
var climbing = Plan.AllRooms()
.SelectMany( room => room.Stairs.Concat( room.Porches.SelectMany( porch => porch.Stairs ) ) )
.Select( stair => stair.Id )
.ToHashSet();
var owners = climbing
.Concat( Plan.AllRooms().Where( room => room.Spans ).Select( room => room.Id ) )
.Concat( Plan.AllCuts().Select( cut => cut.Id ) )
.Concat( Plan.AllWalls().Select( wall => wall.Id ) )
.ToHashSet();
foreach ( var building in Plan.Buildings ) {
removed += building.Roofs.RemoveAll( roof => !alive.Contains( roof.Id ) );
removed += building.Rooms.RemoveAll( room => room.HasContent && !alive.Contains( room.Id ) );
removed += building.Downpipes.RemoveAll( pipe => !alive.Contains( pipe.Id ) );
removed += building.Pipes.RemoveAll( run => !alive.Contains( run.Id ) );
removed += building.Brackets.RemoveAll( bracket => !alive.Contains( bracket.Id ) );
removed += building.Fences.RemoveAll( fence => !alive.Contains( fence.Id ) );
foreach ( var roof in building.Roofs ) {
removed += roof.Walls.RemoveAll( wall => !alive.Contains( wall.Id ) );
}
foreach ( var room in building.Rooms ) {
removed += room.Walls.RemoveAll( wall => !alive.Contains( wall.Id ) );
removed += room.Pillars.RemoveAll( pillar => !alive.Contains( pillar.Id ) );
removed += room.Trims.RemoveAll( trim => !alive.Contains( trim.Id ) );
removed += room.Porches.RemoveAll( porch => !alive.Contains( porch.Id ) );
foreach ( var porch in room.Porches ) {
removed += porch.Pillars.RemoveAll( pillar => !alive.Contains( pillar.Id ) );
removed += porch.Trims.RemoveAll( trim => !alive.Contains( trim.Id ) );
}
removed += room.Approaches.RemoveAll( approach => !alive.Contains( approach.Id ) );
}
removed += building.Cutouts.RemoveAll( cutout => cutout.OwnerId != 0 && !climbing.Contains( cutout.OwnerId ) );
}
foreach ( var wall in Plan.AllWalls() ) {
removed += wall.Openings.RemoveAll( opening => opening.OwnerId != 0 && !owners.Contains( opening.OwnerId ) );
}
if ( removed == 0 ) {
return;
}
Picked = null;
Commit( "Delete Architecture" );
}
public void Save() {
ArchStorage.SavePlan( Scene, Plan );
}
public ArchKit ReloadKit() {
Kit = ArchStorage.LoadKit( Plan.KitName ) ?? new ArchKit();
ArchStyle.InvalidateCache();
Commit();
return Kit;
}
public List<ArchArchetype> ReloadArchetypes() {
Archetypes = ArchStorage.LoadArchetypes();
return Archetypes;
}
public ArchArchetype FindArchetype( string name ) {
return Archetypes.FirstOrDefault( archetype => string.Equals( archetype.Name, name, StringComparison.OrdinalIgnoreCase ) );
}
public static void Register<T>( List<T> into, T entry, Func<T, string> named ) {
if ( into is null || entry is null || named( entry ) is not { Length: > 0 } key ) {
return;
}
into.RemoveAll( existing => string.Equals( named( existing ), key, StringComparison.OrdinalIgnoreCase ) );
into.Add( entry );
into.Sort( ( left, right ) => string.Compare( named( left ), named( right ), StringComparison.OrdinalIgnoreCase ) );
}
public void RegisterArchetype( ArchArchetype type ) => Register( Archetypes, type, entry => entry.Name );
public void RegisterWallPreset( ArchWallPreset preset ) => Register( Kit?.Walls, preset, entry => entry.Name );
public void RegisterOpeningPreset( ArchOpeningPreset preset ) => Register( Kit?.Openings, preset, entry => entry.Name );
public void RegisterCabinetType( ArchCabinetType type ) => Register( Kit?.Cabinets, type, entry => entry.Name );
public List<ArchPillarType> ReloadPillarTypes() {
PillarTypes = ArchAsks.PillarTypes();
return PillarTypes;
}
public ArchPillarType FindPillarType( string name ) {
return PillarTypes.FirstOrDefault( type => string.Equals( type.Name, name, StringComparison.OrdinalIgnoreCase ) );
}
public List<ArchPillarType> OfferedPillars() {
var wanted = FindArchetype( ActiveBuilding()?.Archetype )?.Pillars;
if ( wanted is null || wanted.Count == 0 ) {
return PillarTypes;
}
var offered = wanted
.Select( FindPillarType )
.Where( type => type is not null )
.ToList();
return offered.Count > 0 ? offered : PillarTypes;
}
public ArchPillarType DefaultPillar() => OfferedPillars().FirstOrDefault();
public IEnumerable<ArchOpeningPreset> Offered( Func<ArchOpeningPreset, bool> wanted ) {
var matching = Kit.Openings.Where( wanted ).ToList();
var type = FindArchetype( ActiveBuilding()?.Archetype );
if ( type is null || type.Openings.Count == 0 ) {
return matching;
}
return matching
.OrderBy( preset => type.Openings.FindIndex( name => string.Equals( name, preset.Name, StringComparison.OrdinalIgnoreCase ) ) switch {
< 0 => int.MaxValue,
var rank => rank
} )
.ToList();
}
public bool Recommends( ArchOpeningPreset preset ) {
var type = FindArchetype( ActiveBuilding()?.Archetype );
return type is not null
&& type.Openings.Any( name => string.Equals( name, preset.Name, StringComparison.OrdinalIgnoreCase ) );
}
readonly ArchBuildCache built = new();
public void Regenerate( ArchBuildContext generation = null, IReadOnlySet<int> rebuild = null ) {
EnsureKit();
Revise();
standing = 0;
previewPending = false;
Unbuilt = false;
RealTimeSince spent = 0f;
using var profile = ArchBuildProfile.Begin();
var root = ArchScene.Generate( Scene, Plan, Kit, generation, LayerTree, built, null, rebuild );
previewCost = spent;
previewed = 0f;
LastProfile = profile;
if ( !root.IsValid() ) {
return;
}
using ( ArchBuildProfile.Stage( "Doors" ) ) {
ArchScene.LinkDoublePairs( root );
}
using ( ArchBuildProfile.Stage( "Uv" ) ) {
ArchFaceUvMappings.ApplyStored( this );
}
using ( ArchBuildProfile.Stage( "Visibility" ) ) {
ApplyVisibility( root );
}
}
public ArchBuildProfile LastProfile { get; private set; }
public void RebuildCold() {
Disown();
Commit( "Rebuild Architecture" );
}
// Invalidates the cache so the next rebuild re-emits everything.
public void Disown() => built.Forget();
public ArchBuildSettlement LastBuild => built.Last;
const float PreviewInterval = 0.08f;
// Preview beat scales with actual build cost so heavy plans don't lag the drag.
const float PreviewDuty = 3f;
const float PreviewCeiling = 1.5f;
public static float Beat( float cost ) => MathF.Min( PreviewCeiling, MathF.Max( PreviewInterval, cost * PreviewDuty ) );
RealTimeSince previewed;
bool previewPending;
float previewCost;
public void Preview() {
Revise();
if ( previewed < Beat( previewCost ) ) {
previewPending = true;
return;
}
Regenerate();
}
void SettlePreview() {
if ( previewPending && previewed >= Beat( previewCost ) ) {
Regenerate();
}
}
public void Commit( string action = "Architecture Edit" ) => Commit( action, true );
public void CommitEdited( string action, ArchSelection selection ) => Commit( action, true, null, selection );
public void CommitPlacement( string action, params ArchBuilding[] buildings ) => Commit( action, true, buildings );
// Commits the plan without rebuilding geometry — caller builds once when done.
public void Bank( string action = "Architecture Edit" ) => Commit( action, false );
public bool Unbuilt { get; private set; }
public void Build() {
ArchTerrain.ExcavateBasements( Scene, Plan, EnsureKit() );
Regenerate();
}
void Commit( string action, bool geometry, IReadOnlyList<ArchBuilding> buildings = null, ArchSelection selection = null ) {
EnsureKit();
Plan.DiscardEmptyTargets();
Revise();
IReadOnlyList<ArchBuilding> excavation = null;
IReadOnlySet<int> rebuild = null;
if ( selection is { Item: ArchStairPart stair, Building: { } building } ) {
var reached = Plan.Buildings
.Where( host => host == building || host.Cutouts.Any( cutout => cutout.OwnerId == stair.Id ) )
.ToList();
ArchStairAffector.Resolve( Plan, Kit, building, selection.Room, stair );
excavation = reached
.Concat( Plan.Buildings.Where( host => host.Cutouts.Any( cutout => cutout.OwnerId == stair.Id ) ) )
.Distinct()
.ToList();
rebuild = excavation
.Select( host => host.Id )
.Concat( Plan.AllRooms()
.Where( host => host == selection.Room || host.Walls
.SelectMany( wall => wall.Openings )
.Any( opening => opening.OwnerId == stair.Id ) )
.Select( host => host.Id ) )
.ToHashSet();
} else {
ArchAffectors.Resolve( Plan, Kit );
}
if ( geometry ) {
if ( buildings is { Count: > 0 } ) {
ArchTerrain.Flatten( Scene, Plan, Kit, buildings );
}
if ( excavation is { Count: > 0 } ) {
ArchTerrain.ExcavateBasements( Scene, Plan, Kit, excavation );
} else {
ArchTerrain.ExcavateBasements( Scene, Plan, Kit );
}
}
var generation = new ArchBuildContext( Plan, Kit, Scene ).ResolveConnections();
if ( !ConsumeAffected() && SelectedLayer is { } edited ) {
Affect( edited.ItemId );
}
var before = committed;
var after = ArchStorage.Snapshot( Plan );
committed = after;
Revision++;
Save();
if ( geometry ) {
Regenerate( generation, rebuild );
} else {
Unbuilt = true;
}
if ( before is null || before == after ) {
return;
}
SceneEditorSession.Active?.AddUndo( action, () => Rewind( before ), () => Rewind( after ) );
}
void Rewind( string snapshot ) {
var restored = ArchStorage.Restore( snapshot );
if ( restored is null ) {
return;
}
Plan = restored;
Picked = null;
Revise();
committed = ArchStorage.Snapshot( Plan );
Save();
ArchTerrain.ExcavateBasements( Scene, Plan, EnsureKit() );
Regenerate();
}
public ArchBuilding ActiveBuilding() {
return Plan.FindBuilding( ActiveBuildingId ) ?? Plan.Buildings.FirstOrDefault();
}
public ArchRoom ActiveRoom() {
var building = ActiveBuilding();
return building?.Rooms.FirstOrDefault( room => room.Id == ActiveRoomId ) ?? building?.Rooms.FirstOrDefault();
}
public ArchRoom RoomOnLevel( Vector2 point ) => RoomOnLevel( Plan.AllRooms(), Level, LevelHeight, StoreyHeight, point, ActiveRoom() );
public static ArchRoom RoomOnLevel( IEnumerable<ArchRoom> rooms, int level, float levelHeight, float storeyHeight, Vector2 point, ArchRoom active ) {
var storey = rooms
.Where( room => room.Floor == level && MathF.Abs( room.BaseHeight - levelHeight ) < storeyHeight * 0.5f )
.ToList();
var inside = storey.FirstOrDefault( room => ArchFloorGen.Contains( ArchFloorGen.Footprint( room ), point ) );
if ( inside is not null ) {
return inside;
}
return active is not null && storey.Contains( active ) ? active : storey.FirstOrDefault();
}
public ArchRoom RoomAt( Vector2 point, out ArchBuilding building ) {
var room = Contained( point );
if ( room is null ) {
NearestWall( point, out room, out _, out _ );
}
room ??= ActiveRoom();
building = (room is null ? null : Plan.OwnerOf( room )) ?? ActiveBuilding();
return room;
}
public ArchRoom Contained( Vector2 point ) {
foreach ( var building in Plan.Buildings ) {
if ( Contained( building, point ) is { } room ) {
return room;
}
}
return null;
}
public ArchRoom Contained( ArchBuilding building, Vector2 point ) {
foreach ( var room in building.Rooms ) {
if ( room.Floor == Level && ArchFootprint.Contains( Outline( room ), point ) ) {
return room;
}
}
return null;
}
public void EnsureTarget() {
if ( Plan.Buildings.Count == 0 ) {
Plan.Units.Add( new ArchBuilding { Id = Plan.AllocateId(), Name = "Building" } );
}
var building = ActiveBuilding();
ActiveBuildingId = building.Id;
if ( building.Rooms.Count == 0 ) {
building.Rooms.Add( new ArchRoom { Id = Plan.AllocateId(), Name = "Room", Floor = Level, BaseHeight = LevelHeight } );
}
ActiveRoomId = ActiveRoom()?.Id ?? 0;
}
public ArchRoom EnsureRoom( ArchBuilding building ) {
if ( building is null ) {
return null;
}
if ( building.Rooms.FirstOrDefault( standing => standing.Floor == Level && ArchLayerGate.On( standing ) ) is { } held ) {
return held;
}
var room = new ArchRoom { Id = Plan.AllocateId(), Name = "Room", Floor = Level, BaseHeight = LevelHeight };
building.Rooms.Add( room );
return room;
}
public virtual void BuildTargets( Layout header, Action refresh ) { }
public override void BuildSceneContextMenu( Menu menu, Ray ray, SceneTraceResult? trace ) {
if ( AimPoint( ray, out var point ) ) {
ArchContextActions.Load().Build( new ArchContextActionContext( this, menu, Level, point ) );
}
ApproachMenu( menu, ray );
if ( Under( ray ) is not { Style: RoofStyle.Gable or RoofStyle.Shed or RoofStyle.Sawtooth } roof ) {
return;
}
menu.AddOption( $"Turn {roof.Name} ridge", "rotate_90_degrees_cw", () => {
roof.RidgeAlongX = !roof.RidgeAlongX;
Commit( "Turn Ridge" );
} );
}
void ApproachMenu( Menu menu, Ray ray ) {
if ( ApproachUnder( ray ) is not { } approach ) {
return;
}
if ( approach.Kind == ApproachKind.Ramp ) {
var kerbs = menu.AddOption( $"{approach.Name} kerbs", "align_horizontal_center",
() => { approach.Kerbs = !approach.Kerbs; Commit( "Ramp Kerbs" ); } );
kerbs.Checkable = true;
kerbs.Checked = approach.Kerbs;
} else {
var rails = menu.AddOption( $"{approach.Name} handrails", "fence",
() => { approach.Rails = !approach.Rails; Commit( "Approach Handrails" ); } );
rails.Checkable = true;
rails.Checked = approach.Rails;
}
menu.AddSeparator();
}
ArchApproachPart ApproachUnder( Ray ray ) {
return AimPoint( ray, out var point ) ? ArchPick.ApproachUnder( Plan, Level, point, out _ ) : null;
}
ArchRoofPart Under( Ray ray ) {
return AimPoint( ray, out var point ) ? ArchPick.RoofUnder( Plan, Level, point, out _ ) : null;
}
public bool Cursor( out ArchCursor cursor ) {
var ray = Gizmo.CurrentRay;
var plane = ArchWorkPlane.For( this );
var standing = plane.Standing ?? float.MinValue;
if ( cursorTaken && cursorStamp == ArchTerrain.Stamped && cursorAxis == plane.Axis && cursorDepth == plane.Depth
&& cursorStanding == standing && cursorReach == plane.WallReach
&& ray.Position == cursorRay.Position && ray.Forward == cursorRay.Forward ) {
cursor = cursorFound;
return cursorFound.Found;
}
cursorRay = ray;
cursorStamp = ArchTerrain.Stamped;
cursorAxis = plane.Axis;
cursorDepth = plane.Depth;
cursorStanding = standing;
cursorReach = plane.WallReach;
cursorTaken = true;
plane.Locate( Grid, Scene, ray, out cursorFound );
cursor = cursorFound;
return cursorFound.Found;
}
public bool GroundPoint( out Vector2 point ) {
var found = Cursor( out var cursor );
point = cursor.Plan;
return found;
}
public bool GroundPoint( Ray ray, out Vector2 point ) {
var found = ArchWorkPlane.For( this ).Locate( Grid, Scene, ray, out var cursor );
point = cursor.Plan;
return found;
}
// Un-snapped ray hit — for picks and measurements, not placement.
public bool AimPoint( out Vector2 point ) {
var found = Cursor( out var cursor );
point = cursor.Free;
return found;
}
public bool AimPoint( Ray ray, out Vector2 point ) {
var found = ArchWorkPlane.For( this ).Locate( Grid, Scene, ray, out var cursor );
point = cursor.Free;
return found;
}
public Vector2 Snap( Vector2 point ) {
return Grid.Base( point );
}
public bool Walled( Vector2 point, out Vector2 at, int? storey = null, ArchRoofPart deck = null ) {
at = point;
if ( !SnapsToWalls ) {
return false;
}
var found = ArchWallSnap.Nearest( ArchWallSnap.On( Plan, storey ?? Level, deck ?? StandingDeck ), point,
SnapReach() );
at = found.Point;
return found.Took;
}
public Vector2 Settled( Vector2 point, int? storey = null, ArchRoofPart deck = null ) {
return Walled( point, out var at, storey, deck ) ? at : Grid.Base( point );
}
public float SeatAt( Vector2 at, ArchRoom room, float? beneath = null, float? upon = null ) {
var host = Plan.OwnerOf( room ) ?? ActiveBuilding();
// Use the room's base only if it's on this storey — active room may be a storey up.
var datum = room is { } standing && standing.Floor == Level
? standing.BaseHeight
: ArchBuild.FloorOf( host, EnsureKit(), Level );
var covered = ArchPillarSoffit.Covering( Plan, Kit, at, datum );
var ceiling = covered < float.MaxValue
? MathF.Max( datum + ArchPillarSeat.Clear, covered )
: datum + ArchPillarSeat.Clear;
// The aimed-at surface overrides the computed ceiling.
if ( upon is { } deck ) {
ceiling = MathF.Max( ceiling, deck + ArchPillarSeat.Clear );
}
return ArchPillarSeat.Under( Plan, host, Kit, new ArchGround( Scene ), at, datum,
beneath is { } aimed ? MathF.Min( ceiling, aimed ) : ceiling );
}
public void ClearPlan() {
if ( !Plan.HasContent && Generated() ) {
Log.Warning( "Architecture: this scene's plan is empty but the scene still holds generated geometry, so clearing could"
+ " not be undone. Reopen the scene to load its plan, or delete the Generated Architecture object by hand." );
return;
}
Plan = new ArchPlan { KitName = Plan.KitName };
Picked = null;
ActiveBuildingId = 0;
ActiveRoomId = 0;
Commit( "Clear Architecture Plan" );
}
bool Generated() {
var root = ArchScene.FindRoot( Scene );
return root.IsValid() && root.Components.GetAll<MeshComponent>( FindMode.EverythingInDescendants ).Any();
}
public void Select( ArchSelection picked ) {
Picked = picked;
(CurrentTool as ArchSubtool)?.Forget();
if ( picked?.Room is { } room ) {
Level = room.Floor;
ActiveBuildingId = picked.Building?.Id ?? 0;
ActiveRoomId = room.Id;
} else if ( picked?.Item is ArchBuilding building ) {
ActiveBuildingId = building.Id;
}
(CurrentTool as ArchSubtool)?.RefreshSidebar();
}
// Affects the whole group — neighbours must rebuild without the deleted layer.
public void DeleteSelected() {
if ( Picked is null ) {
return;
}
var layer = SelectedLayer?.ItemId ?? 0;
var scope = ArchLayerGroups.Holding( Plan, layer )?.Id ?? layer;
Picked.RemoveFrom( Plan );
Picked = null;
Affect( scope );
Commit();
}
public ArchWall NearestWall( Vector2 point, out ArchRoom room, out float along, out float distance ) {
var storey = Plan.Buildings.SelectMany( building => building.Rooms ).Where( entry => entry.Floor == Level );
return NearestWall( storey, point, out room, out along, out distance );
}
public static ArchWall NearestWall( IEnumerable<ArchRoom> rooms, Vector2 point, out ArchRoom room, out float along, out float distance ) {
ArchWall best = null;
room = null;
along = 0f;
distance = float.MaxValue;
foreach ( var candidate in rooms ) {
var wall = NearestWall( candidate, point, out var at, out var gap );
if ( wall is null || gap >= distance ) {
continue;
}
best = wall;
room = candidate;
along = at;
distance = gap;
}
return best;
}
// Measured to the wall CENTRELINE; ArchWallSnap.NearestWallFace measures to the near face instead.
public static ArchWall NearestWall( ArchRoom room, Vector2 point, out float along, out float distance ) {
ArchWall best = null;
along = 0f;
distance = float.MaxValue;
if ( room is null ) {
return null;
}
foreach ( var wall in room.Walls ) {
var length = wall.Length;
if ( length < ArchGridService.LeastWallLength ) {
continue;
}
var projected = Math.Clamp( Vector2.Dot( point - wall.Start, wall.Direction ), 0f, length );
var closest = wall.PointAt( projected );
var gap = (point - closest).Length;
if ( gap >= distance ) {
continue;
}
distance = gap;
along = projected;
best = wall;
}
return best;
}
}