Editor/Services/ArchChamfer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly record struct ArchChamferFacet( Vector2 Corner, Vector2 From, Vector2 To ) {
public float Reach => (From - Corner).Length;
public bool Stands => (To - From).Length >= 1f;
public Vector2 Face => Stands ? (To - From).Normal : new Vector2( 1f, 0f );
public Vector2 Normal {
get {
var face = Face;
return new Vector2( -face.y, face.x );
}
}
}
public sealed class ArchChamferReport {
public List<ArchWall> Facets { get; } = new();
public int Loops { get; set; }
public int Ends { get; set; }
public int Runs { get; set; }
public int Stranded { get; set; }
public bool Chamfered => Loops > 0 || Facets.Count > 0;
}
public static class ArchChamfer {
public static ArchChamferReport Corner( ArchPlan plan, ArchBuilding building, Vector2 corner, float setback ) {
var report = new ArchChamferReport();
if ( plan is null || building is null ) {
return report;
}
foreach ( var room in building.Rooms ) {
Storey( plan, room, corner, setback, report );
}
foreach ( var roof in building.Roofs ) {
Deck( plan, roof, corner, setback, report );
}
foreach ( var platform in building.Platforms ) {
Outline( platform.Outline(), corner, setback, platform.Reshape, report );
}
return report;
}
public static ArchChamferFacet? Facing( IReadOnlyList<Vector2> loop, Vector2 corner, float setback ) {
if ( loop is not { Count: >= 3 } ) {
return null;
}
var wound = ArchFootprint.Wind( loop.ToList() );
var index = Nearest( wound, corner );
if ( index < 0 ) {
return null;
}
var at = wound[index];
var back = wound[(index - 1 + wound.Count) % wound.Count] - at;
var forward = wound[(index + 1) % wound.Count] - at;
// Setback on the ladder, not the points — snapping them would rotate neighbouring elevations
var reach = MathF.Min( ArchGridService.Fine( setback ), MathF.Min( back.Length, forward.Length ) * 0.45f );
if ( reach < 1f ) {
return null;
}
return new ArchChamferFacet( at, at + back.Normal * reach, at + forward.Normal * reach );
}
static void Storey( ArchPlan plan, ArchRoom room, Vector2 corner, float setback, ArchChamferReport report ) {
if ( Facing( ArchFloorGen.Footprint( room ), corner, setback ) is not { Stands: true } facet ) {
return;
}
if ( room.HasFootprint && Split( room.Footprint, facet, true ) is not null ) {
report.Loops++;
}
var faced = Stopped( room.Walls, facet, report );
if ( faced.Count > 0 ) {
Seat( room.Walls, Facet( plan, faced, facet ), report );
}
// A following run traces its host and re-derives; a hand-drawn one keeps whatever path it was given.
foreach ( var trim in room.Trims.Where( trim => !trim.Follows ) ) {
report.Runs += Split( trim, facet ) ? 1 : 0;
}
}
static void Deck( ArchPlan plan, ArchRoofPart roof, Vector2 corner, float setback, ArchChamferReport report ) {
var outline = roof.Outline();
if ( Facing( outline, corner, setback ) is not { Stands: true } facet ) {
return;
}
if ( Split( outline, facet, true ) is not null ) {
roof.Reshape( outline );
report.Loops++;
}
var faced = Stopped( roof.Walls, facet, report );
if ( faced.Count > 0 ) {
Seat( roof.Walls, Facet( plan, faced, facet ), report );
}
}
static void Outline( List<Vector2> loop, Vector2 corner, float setback, Action<List<Vector2>> reshape, ArchChamferReport report ) {
if ( Facing( loop, corner, setback ) is not { Stands: true } facet || Split( loop, facet, true ) is null ) {
return;
}
reshape( loop );
report.Loops++;
}
static List<ArchWall> Stopped( List<ArchWall> walls, ArchChamferFacet facet, ArchChamferReport report ) {
var faced = new List<ArchWall>();
foreach ( var wall in walls ) {
if ( wall.Length < 1f ) {
continue;
}
if ( ArchCarry.Coincident( wall.Start, facet.Corner ) ) {
var moved = Retracted( wall.Start, wall.Direction, wall.Length, facet );
wall.Start = moved;
Restationed( wall, (moved - facet.Corner).Length, report );
} else if ( ArchCarry.Coincident( wall.End, facet.Corner ) ) {
wall.End = Retracted( wall.End, -wall.Direction, wall.Length, facet );
Restationed( wall, 0f, report );
} else {
continue;
}
faced.Add( wall );
report.Ends++;
}
return faced;
}
// Retracting the start slides openings/modifiers; anything now off the run is removed
static void Restationed( ArchWall wall, float slid, ArchChamferReport report ) {
foreach ( var opening in wall.Openings ) {
opening.Offset -= slid;
}
foreach ( var block in wall.Modifiers ) {
block.Along -= slid;
}
var length = wall.Length;
report.Stranded += wall.Openings.RemoveAll( opening => !Within( opening.Offset, opening.Width, length ) );
report.Stranded += wall.Modifiers.RemoveAll( block => !Within( block.Along, block.Width, length ) );
}
static bool Within( float centre, float width, float length ) {
var reach = MathF.Max( 0f, width ) * 0.5f;
return centre - reach > -0.05f && centre + reach < length + 0.05f;
}
static Vector2 Retracted( Vector2 end, Vector2 away, float length, ArchChamferFacet facet ) {
var facing = Vector2.Dot( facet.Normal, away );
var travel = MathF.Abs( facing ) < 0.001f
? facet.Reach
: Vector2.Dot( facet.Normal, facet.From - end ) / facing;
return end + away * Math.Clamp( travel, 0f, length * 0.45f );
}
// Dressed from the adjacent run so the facet matches its neighbours
static ArchWall Facet( ArchPlan plan, IReadOnlyList<ArchWall> faced, ArchChamferFacet facet ) {
var wall = new ArchWall {
Id = plan.AllocateId(),
Start = facet.From,
End = facet.To
};
var source = faced.FirstOrDefault( run => run.Exterior ) ?? faced.FirstOrDefault();
if ( source is not null ) {
ArchWallTreatments.Dress( source, wall );
}
return wall;
}
static void Seat( List<ArchWall> walls, ArchWall facing, ArchChamferReport report ) {
walls.Insert( Ring( walls, facing ), facing );
report.Facets.Add( facing );
}
// Seated in the ring so a derived-footprint walk still closes
static int Ring( List<ArchWall> walls, ArchWall facing ) {
for ( var index = 0; index < walls.Count; index++ ) {
if ( ArchCarry.Coincident( walls[index].End, facing.Start ) ) {
return index + 1;
}
if ( ArchCarry.Coincident( walls[index].Start, facing.End ) ) {
return index;
}
}
return walls.Count;
}
readonly record struct ArchChamferSplit( int At, int Count );
// Ordered by the stored loop's winding, not the wound copy's — open runs take one point, not two
static ArchChamferSplit? Split( List<Vector2> points, ArchChamferFacet facet, bool closed ) {
var index = Nearest( points, facet.Corner );
if ( index < 0 ) {
return null;
}
var count = points.Count;
var leading = closed || index > 0 ? (index - 1 + count) % count : -1;
var trailing = closed || index < count - 1 ? (index + 1) % count : -1;
if ( leading < 0 || trailing < 0 ) {
points[index] = Nearer( facet, points[leading < 0 ? trailing : leading] );
return new ArchChamferSplit( index, 1 );
}
var forward = (points[leading] - facet.From).Length <= (points[leading] - facet.To).Length;
points.RemoveAt( index );
points.InsertRange( index, forward ? new[] { facet.From, facet.To } : new[] { facet.To, facet.From } );
return new ArchChamferSplit( index, 2 );
}
static bool Split( ArchTrimPart trim, ArchChamferFacet facet ) {
var flats = trim.Path.Select( Flat ).ToList();
var index = Nearest( flats, facet.Corner );
if ( index < 0 ) {
return false;
}
var height = trim.Path[index].z;
if ( Split( flats, facet, trim.Closed ) is not { } split ) {
return false;
}
trim.Path.RemoveAt( split.At );
trim.Path.InsertRange( split.At, Enumerable.Range( 0, split.Count )
.Select( offset => Raised( flats[split.At + offset], height ) ) );
return true;
}
static int Nearest( IReadOnlyList<Vector2> points, Vector2 corner ) {
var found = -1;
var best = ArchCarry.CornerReach;
for ( var index = 0; index < points.Count; index++ ) {
var away = (points[index] - corner).Length;
if ( away >= best ) {
continue;
}
best = away;
found = index;
}
return found;
}
static Vector2 Nearer( ArchChamferFacet facet, Vector2 to ) {
return (facet.From - to).Length <= (facet.To - to).Length ? facet.From : facet.To;
}
static Vector2 Flat( Vector3 point ) => new( point.x, point.y );
static Vector3 Raised( Vector2 point, float height ) => new( point.x, point.y, height );
}