Editor/Carve/ArchCut.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchCut {
public const float MinRun = 18f;
public static Vector2 WorldBand( ArchCutSegment segment, float lift ) {
return new Vector2( segment.BaseHeight + lift, segment.TopHeight + lift );
}
// Memoised — every host a shaft reaches asks for the same volumes
public static IReadOnlyList<ArchCarveVolume> Resolve( ArchCutPart cut, ArchKit kit ) {
return ArchBuildMemo.Held( memo => memo.Volumes, cut?.Id ?? 0, () => Resolved( cut, kit ).ToList() );
}
static IEnumerable<ArchCarveVolume> Resolved( ArchCutPart cut, ArchKit kit ) {
if ( cut is not { HasContent: true } ) {
yield break;
}
if ( cut.IsDamage ) {
foreach ( var volume in ArchDamage.Resolve( cut, kit ) ) {
yield return volume;
}
yield break;
}
var legs = cut.Segments.Where( segment => segment.HasLoop || segment.Length > 1f ).ToList();
var breaking = Breaking( cut, kit );
if ( cut.Profile != CutProfile.Steps ) {
foreach ( var leg in legs ) {
yield return Bite( cut, leg ).Breaking( breaking );
}
yield break;
}
var going = cut.StepGoing > 0.5f ? cut.StepGoing : kit.StepGoing;
var rise = cut.StepRise > 0.5f ? cut.StepRise : kit.StepRise;
foreach ( var leg in legs.Where( leg => !leg.HasLoop ) ) {
var climbed = leg.BaseHeight;
var steps = Math.Max( 1, (int)MathF.Round( leg.Length / MathF.Max( 1f, going ) ) );
var tread = leg.Length / steps;
var axes = leg.Axes;
var half = MathF.Max( 1f, leg.Width ) * 0.5f;
for ( var step = 0; step < steps; step++ ) {
var footprint = axes.Rect( step * tread, (step + 1) * tread, -half, half );
climbed += rise;
yield return ArchCarveVolume.Over( footprint, climbed, leg.TopHeight ).Breaking( breaking );
}
}
}
public static ArchCarveVolume Bite( ArchCutPart cut, ArchCutSegment leg ) {
return ArchRamp.Rakes( cut )
? ArchCarveVolume.Above( leg.Outline(), Floor( cut, leg ), leg.TopHeight )
: ArchCarveVolume.Over( leg.Outline(), leg.BaseHeight, leg.TopHeight );
}
public static ArchCarvePlane Floor( ArchCutPart cut, ArchCutSegment leg ) {
return ArchRamp.Rakes( cut )
? ArchRamp.Deck( cut, leg.Outline(), leg.TopHeight, leg.TopHeight - leg.BaseHeight )
: ArchCarvePlane.Level( leg.BaseHeight );
}
public static float Toe( ArchCutPart cut, ArchCutSegment leg ) {
return leg.TopHeight - ArchRamp.Fall( cut, leg.TopHeight - leg.BaseHeight );
}
// Seeded by id so break edges are stable across hotloads
static ArchCarveBreak Breaking( ArchCutPart cut, ArchKit kit ) {
return cut.BreakEdges ? new ArchCarveBreak { Seed = cut.Id, Jitter = kit?.BreakJitter ?? 0f } : default;
}
public static bool Affects( ArchCutPart cut, ArchCutAffects target ) => (cut.Affects & target) == target;
public static IEnumerable<ArchFloorCutout> Stored( ArchBuilding building, int level ) {
return building.Cutouts.Where( cutout => cutout.Level == level && ArchLayerGate.Owned( cutout.OwnerId ) );
}
public static IEnumerable<ArchFloorCutout> Holes(
ArchPlan plan,
int level,
ArchKit kit,
float bottom,
float top,
int hostId,
ArchCutAffects target = ArchCutAffects.Floors,
int standing = 0 ) {
foreach ( var (cut, volume) in Reaching( plan, level, kit, bottom, top, hostId, target, standing ) ) {
var hole = new ArchFloorCutout { Level = level, Name = cut.Name, Break = volume.Break };
hole.Reshape( volume.Footprint );
yield return hole;
}
}
public static IEnumerable<List<Vector2>> Guarded( ArchPlan plan, int level, ArchKit kit, float bottom, float top, int hostId ) {
var low = MathF.Min( bottom, top );
var high = MathF.Max( bottom, top );
foreach ( var (cut, volume) in Reaching( plan, level, kit, bottom, top, hostId, ArchCutAffects.Floors ) ) {
if ( cut.GuardsOpenedEdges && Pierces( volume, low, high ) ) {
yield return volume.Footprint.ToList();
}
}
}
public static IEnumerable<ArchCarveVolume> Volumes(
ArchPlan plan,
int level,
ArchKit kit,
float bottom,
float top,
int hostId,
ArchCutAffects target,
int standing = 0 ) {
return Reaching( plan, level, kit, bottom, top, hostId, target, standing ).Select( found => found.Volume );
}
public static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Damage(
ArchPlan plan,
int level,
ArchKit kit,
float bottom,
float top,
int hostId,
ArchCutAffects target,
int standing = 0 ) {
return Reaching( plan, level, kit, bottom, top, hostId, target, standing, ArchZoneMode.Damage );
}
public static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Extrusions(
ArchPlan plan,
int level,
ArchKit kit,
float bottom,
float top,
int hostId,
ArchCutAffects target,
int standing = 0 ) {
return Reaching( plan, level, kit, bottom, top, hostId, target, standing, ArchZoneMode.Extrude );
}
static IEnumerable<(ArchCutPart Cut, ArchCarveVolume Volume)> Reaching(
ArchPlan plan,
int level,
ArchKit kit,
float bottom,
float top,
int hostId,
ArchCutAffects target,
int standing = 0,
ArchZoneMode mode = ArchZoneMode.Carve ) {
if ( plan is null ) {
yield break;
}
var low = MathF.Min( bottom, top );
var high = MathF.Max( bottom, top );
foreach ( var cut in Cuts( plan, level, hostId, target, standing, mode ) ) {
foreach ( var volume in Resolve( cut, kit ) ) {
if ( Reaches( volume, low, high ) ) {
yield return (cut, volume);
}
}
}
}
// Only reaches layers that were STANDING at the time this cut was authored (ArchLayerOrder)
static IEnumerable<ArchCutPart> Cuts( ArchPlan plan, int level, int hostId, ArchCutAffects target, int standing = 0, ArchZoneMode mode = ArchZoneMode.Carve ) {
foreach ( var building in plan.Buildings ) {
foreach ( var cut in Filed( plan, building.Cuts, building.Id, level, hostId, target, standing, mode ) ) {
yield return cut;
}
}
foreach ( var road in plan.Roads() ) {
foreach ( var cut in Filed( plan, road.Cuts, road.Id, level, hostId, target, standing, mode ) ) {
yield return cut;
}
}
}
static IEnumerable<ArchCutPart> Filed(
ArchPlan plan,
IReadOnlyList<ArchCutPart> cuts,
int ownerId,
int level,
int hostId,
ArchCutAffects target,
int standing,
ArchZoneMode mode ) {
foreach ( var cut in cuts ) {
if ( cut.Mode != mode || cut.Level > level || !cut.HasContent || !ArchLayerGate.On( cut ) || !Affects( cut, target ) ) {
continue;
}
if ( !ArchLayerOrder.Applies( plan, cut.Id, standing ) ) {
continue;
}
var reach = ArchLayerGroups.Reach( plan, cut.Id, ownerId );
if ( hostId == 0 || reach is null || reach.Contains( hostId ) ) {
yield return cut;
}
}
}
public static bool Reaches( ArchCarveVolume volume, float low, float high ) {
var floor = float.MaxValue;
var ceiling = float.MinValue;
foreach ( var corner in volume.Footprint ) {
floor = MathF.Min( floor, volume.Floor.At( corner ) );
ceiling = MathF.Max( ceiling, volume.Ceiling.At( corner ) );
}
return ceiling > low + ArchCarve.Grain && floor < high - ArchCarve.Grain;
}
// True when the volume spans the entire band — a well, not a recess
public static bool Pierces( ArchCarveVolume volume, float low, float high ) {
var floor = float.MinValue;
var ceiling = float.MaxValue;
foreach ( var corner in volume.Footprint ) {
floor = MathF.Max( floor, volume.Floor.At( corner ) );
ceiling = MathF.Min( ceiling, volume.Ceiling.At( corner ) );
}
return floor <= low + ArchCarve.Grain && ceiling >= high - ArchCarve.Grain;
}
public static IEnumerable<(float From, float To)> Outside(
ArchPlan plan,
ArchKit kit,
int level,
int hostId,
Vector2 from,
Vector2 to,
float bottom,
float top,
ArchCutAffects target = ArchCutAffects.WallFittings,
int standing = 0 ) {
var blocked = new List<(float From, float To)>();
if ( plan is not null && (to - from).Length > ArchCarve.Grain ) {
foreach ( var cut in Cuts( plan, level, hostId, target, standing ) ) {
foreach ( var volume in Resolve( cut, kit ).Where( volume => Reaches( volume, bottom, top ) ) ) {
blocked.AddRange( ArchFootprint.Inside( volume.Footprint, from, to ) );
}
}
}
var marks = new List<float> { 0f, 1f };
foreach ( var range in blocked ) {
marks.Add( Math.Clamp( range.From, 0f, 1f ) );
marks.Add( Math.Clamp( range.To, 0f, 1f ) );
}
marks = marks.Distinct().OrderBy( value => value ).ToList();
for ( var index = 0; index + 1 < marks.Count; index++ ) {
var start = marks[index];
var finish = marks[index + 1];
var middle = (start + finish) * 0.5f;
if ( finish - start > 0.001f && !blocked.Any( range => middle > range.From && middle < range.To ) ) {
yield return (start, finish);
}
}
}
public static IEnumerable<List<Vector3>> OutsidePath(
ArchPlan plan,
ArchKit kit,
int level,
int hostId,
IReadOnlyList<Vector3> path,
ArchCutAffects target ) {
var runs = new List<List<Vector3>>();
for ( var index = 0; index + 1 < (path?.Count ?? 0); index++ ) {
var from = path[index];
var to = path[index + 1];
var flatFrom = new Vector2( from.x, from.y );
var flatTo = new Vector2( to.x, to.y );
var spans = Outside( plan, kit, level, hostId, flatFrom, flatTo, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), target ).ToList();
foreach ( var span in spans ) {
var start = Vector3.Lerp( from, to, span.From );
var finish = Vector3.Lerp( from, to, span.To );
var current = runs.LastOrDefault();
if ( current is null || (current[^1] - start).Length > 0.05f ) {
current = new List<Vector3> { start };
runs.Add( current );
}
if ( (current[^1] - finish).Length > 0.01f ) {
current.Add( finish );
}
}
if ( spans.Count == 0 || spans.Any( span => span.From > 0.001f || span.To < 0.999f ) ) {
runs.Add( null );
}
}
return runs.Where( run => run is { Count: >= 2 } );
}
public static IEnumerable<ArchRunPath> Runs(
ArchPlan plan,
ArchKit kit,
int level,
int hostId,
ArchRunPath run,
ArchCutAffects target ) {
var path = run.Raised();
if ( run.Closed && path.Count >= 3 ) {
path.Add( path[0] );
}
foreach ( var surviving in OutsidePath( plan, kit, level, hostId, path, target ) ) {
yield return ArchRunPath.Normalized( surviving, run.Height );
}
}
public static IEnumerable<(List<Vector3> Points, bool Closed)> Surviving(
ArchPlan plan,
ArchKit kit,
int level,
int hostId,
IReadOnlyList<Vector3> path,
ArchCutAffects target ) {
foreach ( var surviving in OutsidePath( plan, kit, level, hostId, path, target ) ) {
var closed = ArchRunPath.IsClosed( surviving );
yield return (closed ? surviving.Take( surviving.Count - 1 ).ToList() : surviving, closed);
}
}
public static bool Covers( ArchPlan plan, ArchKit kit, int level, int hostId, Vector3 from, Vector3 to, ArchCutAffects target ) {
var at = new Vector2( (from.x + to.x) * 0.5f, (from.y + to.y) * 0.5f );
return Volumes( plan, level, kit, MathF.Min( from.z, to.z ), MathF.Max( from.z, to.z ), hostId, target )
.Any( volume => volume.Covers( at ) );
}
public static IEnumerable<ArchCutPart> Over(
ArchPlan plan,
int level,
IReadOnlyList<Vector2> outline,
int hostId,
ArchCutAffects target = ArchCutAffects.Platforms,
int standing = 0 ) {
if ( plan is null || outline is not { Count: >= 3 } ) {
yield break;
}
foreach ( var cut in Cuts( plan, level, hostId, target, standing ) ) {
if ( cut.Outlines().Any( loop => ArchFootprint.Overlaps( loop, outline ) ) ) {
yield return cut;
}
}
}
public static ArchCutSegment Extend( ArchCutPart cut, Vector2 from, Vector2 to, float width, float baseHeight, float topHeight ) {
var segment = Next( cut, from, to, width, baseHeight, topHeight );
if ( segment is null ) {
return null;
}
cut.Segments.Add( segment );
return segment;
}
public static ArchCutSegment Next( ArchCutPart cut, Vector2 from, Vector2 to, float width, float baseHeight, float topHeight, bool? snapAngle = null ) {
var last = cut?.Segments.Count > 0 ? cut.Segments[^1] : null;
var lift = last is null ? 0f : cut.Rise;
return Sketch(
last?.End ?? from,
to,
width,
(last?.BaseHeight ?? baseHeight) + lift,
(last?.TopHeight ?? topHeight) + lift,
snapAngle ?? cut?.SnapAngle ?? true );
}
// Loop legs derive Start/End from their loop outline — keeps shape and handles in sync
public static void Fit( ArchCutSegment segment, float yaw ) {
if ( segment is null || !segment.HasLoop ) {
return;
}
var axes = new ArchStairAxes { Yaw = yaw };
var along = axes.Along;
var centre = segment.Loop.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / segment.Loop.Count;
var reach = segment.Loop.Select( point => Vector2.Dot( point - centre, along ) ).ToList();
segment.Start = centre + along * reach.Min();
segment.End = centre + along * reach.Max();
}
public static void Shift( ArchCutSegment segment, Vector2 by ) {
if ( segment is null || by.Length < ArchGridService.FinestSize ) {
return;
}
segment.Start += by;
segment.End += by;
if ( segment.HasLoop ) {
segment.Loop = segment.Loop.Select( point => point + by ).ToList();
}
}
// Applies a delta swing, not an absolute angle — preserves existing yaw
public static void Turn( ArchCutSegment segment, Vector2 about, float degrees, bool snapAngle ) {
if ( segment is null ) {
return;
}
var swing = snapAngle ? ArchGridService.Angle( degrees ) : degrees;
var wanted = segment.Yaw + swing;
if ( MathF.Abs( swing ) < 0.001f ) {
return;
}
if ( segment.HasLoop ) {
segment.Loop = ArchFootprint.Turned( segment.Loop, about, swing );
Fit( segment, wanted );
return;
}
var ends = ArchFootprint.Turned( new[] { segment.Start, segment.End }, about, swing );
segment.Start = ends[0];
segment.End = ends[1];
}
public static bool Reshape( ArchCutSegment segment, Vector2 from, Vector2 to, bool snapAngle = true ) {
if ( segment is { HasLoop: true } ) {
Shift( segment, from - segment.Start );
return true;
}
if ( segment is null || Sketch( from, to, segment.Width, segment.BaseHeight, segment.TopHeight, snapAngle ) is not { } redrawn ) {
return false;
}
segment.Start = redrawn.Start;
segment.End = redrawn.End;
return true;
}
public static void Relink( ArchCutPart cut ) {
if ( cut is null ) {
return;
}
for ( var index = 1; index < cut.Segments.Count; index++ ) {
var previous = cut.Segments[index - 1];
var leg = cut.Segments[index];
Reshape( leg, previous.End, previous.End + (leg.End - leg.Start), cut.SnapAngle );
}
}
public static ArchCutSegment Sketch( Vector2 from, Vector2 to, float width, float baseHeight, float topHeight, bool snapAngle = true ) {
var span = to - from;
if ( span.Length < MinRun ) {
return null;
}
var yaw = ArchGridService.Snap( MathF.Atan2( span.y, span.x ).RadianToDegree(), snapAngle ? ArchGridService.AngleStep : 1f ).DegreeToRadian();
var along = new Vector2( MathF.Cos( yaw ), MathF.Sin( yaw ) );
return new ArchCutSegment {
Start = from,
End = from + along * ArchGridService.Fine( Vector2.Dot( span, along ) ),
Width = MathF.Max( 1f, width ),
BaseHeight = MathF.Min( baseHeight, topHeight ),
TopHeight = MathF.Max( baseHeight, topHeight )
};
}
}