Editor utility for moving architectural elements when a building, room, roof, porch or corner is translated. It applies an ArchRemap to many contained parts and provides logic to carry items that are coincident with or along a dragged point/edge, updating coordinates, boxes, loops and related derived geometry.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One answer to "the shape moved - carry everything standing on it". ArchRemap is the arithmetic; this is the
// WALK, and a list forgotten here is a part left standing where its host used to be. Two questions, one set of
// lists: a whole host remapped, and a single authored CORNER carried to where it was dragged.
public static class ArchCarry
{
// A roof outline is authored on the wall it caps and a parapet stands on the deck above that, so points
// sharing a corner arrive near each other rather than exactly on each other.
public const float CornerReach = 1f;
public static void Shift( ArchBuilding building, Vector2 shift ) => Unit( building, ArchRemap.Translation( shift ) );
public static void Shift( ArchRoom room, Vector2 shift ) => Room( room, ArchRemap.Translation( shift ) );
// Everything on the shell rides with it: rooms, roofs, the walls standing on those roofs, yard parts,
// service runs, fixtures and shafts. Pulling a corner of the blueprint is the same walk as sliding it -
// only the map differs.
public static void Unit( ArchBuilding building, ArchRemap map )
{
if ( building is null )
{
return;
}
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 );
}
foreach ( var bracket in building.Brackets )
{
var min = bracket.Min;
var max = bracket.Max;
map.Box( ref min, ref max );
bracket.Min = min;
bracket.Max = max;
}
foreach ( var ladder in building.Ladders )
{
ladder.Anchor = map.Of( ladder.Anchor );
}
foreach ( var balcony in building.Balconies )
{
balcony.Anchor = map.Of( balcony.Anchor );
}
foreach ( var flight in building.ExteriorStairs )
{
flight.Anchor = map.Of( flight.Anchor );
}
foreach ( var cut in building.Cuts )
{
Cut( cut, map );
}
foreach ( var cutout in building.Cutouts )
{
Cutout( cutout, map );
}
}
// Must carry the footprint cache too, or walls stand where the slab isn't - and the fittings with them,
// or a moved room leaves its stairs standing in the dirt.
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 );
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.Origin = map.Of( pillar.Origin );
}
// A span re-reads the pier it names, so only the point it fell back to has to travel - a wall end whose
// station was left behind reprojects onto the moved wall at the wrong place along it.
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 );
}
foreach ( var porch in room.Porches )
{
Porch( porch, map );
}
foreach ( var approach in room.Approaches )
{
approach.Origin = map.Of( approach.Origin );
map.Nodes( approach.Nodes );
}
}
// The walls standing on the deck ride the deck. A parapet is filed on the roof rather than a room, so a
// walk that stops at Min/Max and the outline leaves every one of them behind.
public static void Roof( ArchRoofPart roof, ArchRemap map )
{
if ( roof is null )
{
return;
}
var min = roof.Min;
var max = roof.Max;
map.Box( ref min, ref max );
roof.Min = min;
roof.Max = max;
map.Loop( roof.Footprint );
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.Box( ref lightMin, ref lightMax );
light.Min = lightMin;
light.Max = lightMax;
}
}
public static void Platform( ArchPlatformPart platform, ArchRemap map )
{
if ( platform is null )
{
return;
}
var min = platform.Min;
var max = platform.Max;
map.Box( ref min, ref max );
platform.Min = min;
platform.Max = max;
map.Loop( platform.Footprint );
foreach ( var stair in platform.Stairs )
{
Stair( stair, map );
}
}
// A porch is a host, so its own children move with it - a walk that carries only the legs slides the deck
// out from under its steps, columns and balustrade.
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.Box( 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.Origin = map.Of( pillar.Origin );
}
foreach ( var trim in porch.Trims )
{
map.Path( trim.Path );
}
}
// Every flight is stated in the shaft's own frame, so carrying a stair is carrying its four corners: the
// lanes inside it ride along with no coordinate of their own to forget.
public static void Stair( ArchStairPart stair, ArchRemap map )
{
if ( stair?.Core is not { } core )
{
return;
}
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 );
}
public static void Cut( ArchCutPart cut, ArchRemap map )
{
if ( cut is null )
{
return;
}
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.Loop( cutout.Loop );
return;
}
var min = cutout.Min;
var max = cutout.Max;
map.Box( ref min, ref max );
cutout.Min = min;
cutout.Max = max;
}
// The other question: ONE authored point moved, and everything that shares it follows. A wall is a corner
// of its shell rather than a line floating in front of one, so dragging an end carries the slab's footprint,
// the neighbouring walls that meet there, the storeys stacked above and the roof outline over the lot - or
// the wall walks off and takes the shell open with it. Scoped to the BUILDING, which is what makes it the
// same reach the blueprint has.
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 );
}
// The same walk pushed along an EDGE, because a party wall meets an exterior run anywhere along it: only the
// anchors standing ON the run ride the push, endpoints included.
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;
}
// One walk over the same inventory Unit remaps, so a corner drag and a body drag bring the same parts. The
// lists Unit carries whole are filtered here by what the gesture actually reached.
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;
}
// A following run traces its host and re-derives, so moving its baked path would only be undone.
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;
}
// The run is derived from the loop, so it is refitted along the yaw the shape already stood at - taking
// a fresh yaw from a corner that just moved would swing the frame the carve reads.
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;
}
// A reached corner TRANSLATES the shaft, the way a box fitting travels: a stair is a rectangle, so pulling
// one of its corners on its own would only be the shape coming apart rather than being edited.
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;
}
// A reached box corner TRANSLATES the box, because a bracket or a porch leg is a fitting standing at the
// corner rather than a shape whose corner was authored there.
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;
}
}