Editor/Services/ArchCarry.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public interface IArchCarries {
ArchKind Kind { get; }
void Carry( ArchPlan plan, ArchBuilding building, ArchRemap map );
}
public sealed class ArchCarriers : ArchTable<ArchCarriers, ArchKind, IArchCarries> {
protected override IEnumerable<IArchCarries> Standing() => Enumerable.Empty<IArchCarries>();
protected override ArchKind KeyOf( IArchCarries carrier ) => carrier.Kind;
protected override bool Accepts( IArchCarries carrier ) => carrier.Kind.Known;
protected override string Collision( IArchCarries standing, IArchCarries carrier ) {
return $"{carrier.Kind.Ident} is carried by both {standing.GetType().Name} and {carrier.GetType().Name}";
}
public IReadOnlyCollection<IArchCarries> All => Entries;
}
public static class ArchCarry {
public const float CornerReach = 1f;
public static void Shift( ArchBuilding building, Vector2 shift, ArchPlan plan = null )
=> Unit( building, ArchRemap.Translation( shift ), plan );
public static bool Turn( ArchBuilding building, Vector2 about, float degrees, ArchPlan plan = null ) {
if ( building is null || MathF.Abs( degrees ) < 0.001f ) {
return false;
}
Unit( building, ArchRemap.Spun( about, degrees ), plan );
building.Facing = (building.Facing + degrees) % 360f;
if ( building.Facing < 0f ) {
building.Facing += 360f;
}
return true;
}
public static void Shift( ArchRoom room, Vector2 shift ) => Room( room, ArchRemap.Translation( shift ) );
public static void Unit( ArchBuilding building, ArchRemap map, ArchPlan plan = null ) {
if ( building is null ) {
return;
}
if ( plan is not null ) {
foreach ( var carrier in ArchCarriers.Load().All ) {
carrier.Carry( plan, building, map );
}
}
foreach ( var room in building.Rooms ) {
Room( room, map );
}
foreach ( var roof in building.Roofs ) {
Roof( roof, map );
}
foreach ( var fence in building.Fences ) {
map.Nodes( fence.Nodes );
}
foreach ( var platform in building.Platforms ) {
Platform( platform, map );
}
foreach ( var pipe in building.Downpipes ) {
pipe.Outlet = map.Of( pipe.Outlet );
pipe.Wall = map.Of( pipe.Wall );
}
foreach ( var run in building.Pipes ) {
map.Nodes( run.Nodes );
run.Outward = map.Spun( run.Outward );
}
foreach ( var bracket in building.Brackets ) {
var min = bracket.Min;
var max = bracket.Max;
map.Centred( ref min, ref max );
bracket.Min = min;
bracket.Max = max;
bracket.Outward = map.Spun( bracket.Outward );
bracket.AlongX = map.Axis( bracket.AlongX );
}
foreach ( var ladder in building.Ladders ) {
ladder.Anchor = map.Of( ladder.Anchor );
ladder.Outward = map.Spun( ladder.Outward );
}
foreach ( var balcony in building.Balconies ) {
balcony.Anchor = map.Of( balcony.Anchor );
balcony.Outward = map.Spun( balcony.Outward );
}
foreach ( var flight in building.ExteriorStairs ) {
flight.Anchor = map.Of( flight.Anchor );
flight.Outward = map.Spun( flight.Outward );
}
foreach ( var cut in building.Cuts ) {
Cut( cut, map );
}
foreach ( var cutout in building.Cutouts ) {
Cutout( cutout, map );
}
}
public static void Room( ArchRoom room, ArchRemap map ) {
if ( room is null ) {
return;
}
foreach ( var wall in room.Walls ) {
wall.Start = map.Of( wall.Start );
wall.End = map.Of( wall.End );
}
map.Loop( room.Footprint );
room.FloorBoardYaw = map.Spun( room.FloorBoardYaw );
foreach ( var stair in room.Stairs ) {
Stair( stair, map );
}
foreach ( var trim in room.Trims ) {
map.Path( trim.Path );
}
foreach ( var pillar in room.Pillars ) {
Pillar( pillar, map );
}
foreach ( var span in room.PierSpans ) {
span.From.At = map.Of( span.From.At );
span.To.At = map.Of( span.To.At );
}
foreach ( var beam in room.Beams ) {
var outline = beam.Outline();
map.Loop( outline );
beam.Reshape( outline );
beam.Yaw = map.Spun( beam.Yaw );
}
foreach ( var porch in room.Porches ) {
Porch( porch, map );
}
foreach ( var approach in room.Approaches ) {
approach.Origin = map.Of( approach.Origin );
approach.Yaw = map.Spun( approach.Yaw );
map.Nodes( approach.Nodes );
}
}
// On odd turns the axes swap — counts and spans must trade places too
static void Pillar( ArchPillarPart pillar, ArchRemap map ) {
pillar.Origin = map.Of( pillar.Origin );
pillar.Yaw = map.Spun( pillar.Yaw );
pillar.Spacing = map.Swapped( pillar.Spacing );
if ( !map.SwapsAxes ) {
return;
}
(pillar.CountX, pillar.CountY) = (pillar.CountY, pillar.CountX);
(pillar.SpanAlongX, pillar.SpanAlongY) = (pillar.SpanAlongY, pillar.SpanAlongX);
}
public static void Roof( ArchRoofPart roof, ArchRemap map ) {
if ( roof is null ) {
return;
}
if ( map.KeepsAxes ) {
var min = roof.Min;
var max = roof.Max;
map.Box( ref min, ref max );
roof.Min = min;
roof.Max = max;
map.Loop( roof.Footprint );
} else {
roof.Reshape( Turning( map, roof.Outline() ) );
}
roof.RidgeAlongX = map.Axis( roof.RidgeAlongX );
foreach ( var wall in roof.Walls ) {
wall.Start = map.Of( wall.Start );
wall.End = map.Of( wall.End );
}
foreach ( var light in roof.Lights ) {
var lightMin = light.Min;
var lightMax = light.Max;
map.Centred( ref lightMin, ref lightMax );
light.Min = lightMin;
light.Max = lightMax;
}
}
public static void Platform( ArchPlatformPart platform, ArchRemap map ) {
if ( platform is null ) {
return;
}
if ( map.KeepsAxes ) {
var min = platform.Min;
var max = platform.Max;
map.Box( ref min, ref max );
platform.Min = min;
platform.Max = max;
map.Loop( platform.Footprint );
} else {
platform.Reshape( Turning( map, platform.Outline() ) );
}
platform.RampYaw = map.Spun( platform.RampYaw );
foreach ( var stair in platform.Stairs ) {
Stair( stair, map );
}
}
public static void Porch( ArchPorchPart porch, ArchRemap map ) {
if ( porch is null ) {
return;
}
foreach ( var leg in porch.Legs ) {
var min = leg.Min;
var max = leg.Max;
map.Centred( ref min, ref max );
leg.Min = min;
leg.Max = max;
}
foreach ( var stair in porch.Stairs ) {
Stair( stair, map );
}
foreach ( var pillar in porch.Pillars ) {
Pillar( pillar, map );
}
foreach ( var trim in porch.Trims ) {
map.Path( trim.Path );
}
}
public static void Stair( ArchStairPart stair, ArchRemap map ) {
if ( stair?.Core is not { } core ) {
return;
}
var opening = stair.TopOpening is { Count: >= 3 }
? stair.TopOpening.Select( point => core.Flat( point.x, point.y ) ).ToList()
: null;
var openingSnap = stair.TopOpeningSnap is { Count: >= 3 }
? stair.TopOpeningSnap.Select( point => core.Flat( point.x, point.y ) ).ToList()
: null;
var frame = core.Axes;
var origin = map.Of( core.Origin );
var along = map.Of( frame.Flat( core.Length, 0f ) ) - origin;
var across = map.Of( frame.Flat( 0f, core.Width ) ) - origin;
core.Origin = origin;
core.Yaw = along.IsNearZeroLength ? core.Yaw : MathF.Atan2( along.y, along.x ).RadianToDegree();
core.Length = MathF.Max( 1f, along.Length );
core.Width = MathF.Max( 1f, across.Length );
if ( opening is not null ) {
map.Loop( opening );
stair.TopOpening = opening.Select( point => ArchStairLanes.Local( core, point ) ).ToList();
}
if ( openingSnap is not null ) {
map.Loop( openingSnap );
stair.TopOpeningSnap = openingSnap.Select( point => ArchStairLanes.Local( core, point ) ).ToList();
}
}
public static void Cut( ArchCutPart cut, ArchRemap map ) {
if ( cut is null ) {
return;
}
cut.RampYaw = map.Spun( cut.RampYaw );
foreach ( var segment in cut.Segments ) {
segment.Start = map.Of( segment.Start );
segment.End = map.Of( segment.End );
if ( segment.HasLoop ) {
map.Loop( segment.Loop );
}
}
}
public static void Cutout( ArchFloorCutout cutout, ArchRemap map ) {
if ( cutout is null ) {
return;
}
if ( cutout.HasLoop || !map.KeepsAxes ) {
cutout.Reshape( Turning( map, cutout.Outline() ) );
return;
}
var min = cutout.Min;
var max = cutout.Max;
map.Box( ref min, ref max );
cutout.Min = min;
cutout.Max = max;
}
static List<Vector2> Turning( ArchRemap map, List<Vector2> outline ) {
map.Loop( outline );
return outline;
}
public static bool Corner( ArchBuilding building, Vector2 from, Vector2 to ) {
if ( building is null || (to - from).Length < 0.01f ) {
return false;
}
return Carrying( building, point => Coincident( point, from ), _ => to );
}
public static bool Along( ArchBuilding building, Vector2 start, Vector2 end, Vector2 shift ) {
if ( building is null || shift.Length < 0.01f ) {
return false;
}
return Carrying( building, point => OnRun( point, start, end ), point => point + shift );
}
public static bool Coincident( Vector2 point, Vector2 corner ) => (point - corner).Length < CornerReach;
static bool OnRun( Vector2 point, Vector2 start, Vector2 end ) {
var span = end - start;
var length = span.Length;
if ( length < 0.01f ) {
return Coincident( point, start );
}
var along = Vector2.Dot( point - start, span / length );
if ( along < -CornerReach || along > length + CornerReach ) {
return false;
}
var nearest = start + span * Math.Clamp( along / length, 0f, 1f );
return (point - nearest).Length < CornerReach;
}
static bool Carrying( ArchBuilding building, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed ) {
var moved = false;
foreach ( var room in building.Rooms ) {
moved |= Loop( room.Footprint, reaches, landed );
moved |= Ends( room.Walls, reaches, landed );
foreach ( var pillar in room.Pillars.Where( pillar => reaches( pillar.Origin ) ) ) {
pillar.Origin = landed( pillar.Origin );
moved = true;
}
foreach ( var trim in room.Trims.Where( trim => !trim.Follows ) ) {
moved |= Path( trim.Path, reaches, landed );
}
foreach ( var stair in room.Stairs ) {
moved |= Shaft( stair, reaches, landed );
}
foreach ( var beam in room.Beams ) {
var outline = beam.Outline();
if ( !Loop( outline, reaches, landed ) ) {
continue;
}
beam.Reshape( outline );
moved = true;
}
foreach ( var porch in room.Porches ) {
moved |= Carrying( porch, reaches, landed );
}
foreach ( var approach in room.Approaches ) {
if ( reaches( approach.Origin ) ) {
approach.Origin = landed( approach.Origin );
moved = true;
}
moved |= Nodes( approach.Nodes, reaches, landed );
}
}
foreach ( var roof in building.Roofs ) {
var outline = roof.Outline();
if ( Loop( outline, reaches, landed ) ) {
roof.Reshape( outline );
moved = true;
}
moved |= Ends( roof.Walls, reaches, landed );
}
foreach ( var platform in building.Platforms ) {
var outline = platform.Outline();
if ( Loop( outline, reaches, landed ) ) {
platform.Reshape( outline );
moved = true;
}
foreach ( var stair in platform.Stairs ) {
moved |= Shaft( stair, reaches, landed );
}
}
foreach ( var fence in building.Fences ) {
moved |= Nodes( fence.Nodes, reaches, landed );
}
foreach ( var pipe in building.Downpipes ) {
if ( reaches( pipe.Outlet ) ) {
pipe.Outlet = landed( pipe.Outlet );
moved = true;
}
if ( reaches( pipe.Wall ) ) {
pipe.Wall = landed( pipe.Wall );
moved = true;
}
}
foreach ( var run in building.Pipes ) {
moved |= Nodes( run.Nodes, reaches, landed );
}
foreach ( var bracket in building.Brackets ) {
var min = bracket.Min;
var max = bracket.Max;
if ( !Box( ref min, ref max, reaches, landed ) ) {
continue;
}
bracket.Min = min;
bracket.Max = max;
moved = true;
}
foreach ( var ladder in building.Ladders.Where( ladder => reaches( ladder.Anchor ) ) ) {
ladder.Anchor = landed( ladder.Anchor );
moved = true;
}
foreach ( var balcony in building.Balconies.Where( balcony => reaches( balcony.Anchor ) ) ) {
balcony.Anchor = landed( balcony.Anchor );
moved = true;
}
foreach ( var flight in building.ExteriorStairs.Where( flight => reaches( flight.Anchor ) ) ) {
flight.Anchor = landed( flight.Anchor );
moved = true;
}
// Refit at existing yaw — deriving a new one from the moved corner would swing the frame
foreach ( var segment in building.Cuts.SelectMany( cut => cut.Segments ).Where( segment => segment.HasLoop ) ) {
var yaw = segment.Yaw;
if ( !Loop( segment.Loop, reaches, landed ) ) {
continue;
}
ArchCut.Fit( segment, yaw );
moved = true;
}
foreach ( var cutout in building.Cutouts.Where( cutout => cutout.HasLoop ) ) {
var loop = cutout.Loop.ToList();
if ( !Loop( loop, reaches, landed ) ) {
continue;
}
cutout.Reshape( loop );
moved = true;
}
return moved;
}
static bool Carrying( ArchPorchPart porch, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed ) {
var moved = false;
foreach ( var leg in porch.Legs ) {
var min = leg.Min;
var max = leg.Max;
if ( !Box( ref min, ref max, reaches, landed ) ) {
continue;
}
leg.Min = min;
leg.Max = max;
moved = true;
}
foreach ( var stair in porch.Stairs ) {
moved |= Shaft( stair, reaches, landed );
}
foreach ( var pillar in porch.Pillars.Where( pillar => reaches( pillar.Origin ) ) ) {
pillar.Origin = landed( pillar.Origin );
moved = true;
}
foreach ( var trim in porch.Trims ) {
moved |= Path( trim.Path, reaches, landed );
}
return moved;
}
// Translates the whole shaft — pulling one corner of a rectangle apart is not editing
static bool Shaft( ArchStairPart stair, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed ) {
if ( stair?.Core is not { } core ) {
return false;
}
foreach ( var corner in core.Outline() ) {
if ( !reaches( corner ) ) {
continue;
}
core.Origin += landed( corner ) - corner;
return true;
}
return false;
}
static bool Box( ref Vector2 min, ref Vector2 max, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed ) {
foreach ( var corner in new[] { min, max, new Vector2( min.x, max.y ), new Vector2( max.x, min.y ) } ) {
if ( !reaches( corner ) ) {
continue;
}
var shift = landed( corner ) - corner;
min += shift;
max += shift;
return true;
}
return false;
}
static bool Ends( IEnumerable<ArchWall> walls, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed ) {
var moved = false;
foreach ( var wall in walls ) {
if ( reaches( wall.Start ) ) {
wall.Start = landed( wall.Start );
moved = true;
}
if ( reaches( wall.End ) ) {
wall.End = landed( wall.End );
moved = true;
}
}
return moved;
}
static bool Loop( List<Vector2> loop, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed ) {
var moved = false;
for ( var index = 0; index < loop.Count; index++ ) {
if ( !reaches( loop[index] ) ) {
continue;
}
loop[index] = landed( loop[index] );
moved = true;
}
return moved;
}
static bool Path( List<Vector3> path, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed ) {
var moved = false;
for ( var index = 0; index < path.Count; index++ ) {
var flat = new Vector2( path[index].x, path[index].y );
if ( !reaches( flat ) ) {
continue;
}
var to = landed( flat );
path[index] = new Vector3( to.x, to.y, path[index].z );
moved = true;
}
return moved;
}
static bool Nodes( List<ArchCurveNode> nodes, Func<Vector2, bool> reaches, Func<Vector2, Vector2> landed ) {
var moved = false;
foreach ( var node in nodes ) {
var flat = new Vector2( node.Position.x, node.Position.y );
if ( !reaches( flat ) ) {
continue;
}
var to = landed( flat );
node.Position = new Vector3( to.x, to.y, node.Position.z );
moved = true;
}
return moved;
}
}