Editor/Carve/ArchCutAffector.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Wall holes are authored openings owned by the cut — re-derived on every commit, gated by ArchLayerGate
public static class ArchCutAffector {
// The narrowest bite worth taking out of a wall - below this the cut is only clipping the skin's corner.
const float LeastBite = 4f;
public static int Resolve( ArchPlan plan, ArchKit kit ) {
var opened = 0;
foreach ( var building in plan.Buildings.ToList() ) {
foreach ( var cut in building.Cuts.ToList() ) {
if ( Recut( plan, kit, building, cut ) ) {
opened++;
}
}
}
return opened;
}
static bool Recut( ArchPlan plan, ArchKit kit, ArchBuilding home, ArchCutPart cut ) {
var wanted = Wanted( plan, kit, home, cut );
// Slotted by the wall and the ordinal on it, so a cut nudged over the same wall re-dresses the hole
// it already owns there rather than closing it and allocating another id.
var moved = ArchOwned.Settle( plan, ArchOwned.Openings, cut.Id,
wanted.Select( bite => Hole( cut, bite ) ).ToList(), () => new ArchOpening() );
Swallowed( plan, kit, home, cut );
Regroup( plan, kit, home, cut, wanted );
return moved;
}
static ArchWant<ArchWall, ArchOpening> Hole( ArchCutPart cut, Bite bite ) {
return new ArchWant<ArchWall, ArchOpening>( bite.Wall, opening => {
opening.Preset = "doorway";
opening.Kind = OpeningKind.Archway;
opening.Offset = bite.Offset;
opening.Width = bite.Width;
opening.SillHeight = bite.Sill;
opening.Height = bite.Height;
opening.Cased = cut.Edge;
opening.HasSill = false;
opening.CasingWidth = cut.EdgeWidth;
opening.Leaf = false;
opening.Glazed = false;
opening.AutoLayout = false;
} );
}
// Disables (not deletes) units inside the cut — ungating or moving the cut restores them
static void Swallowed( ArchPlan plan, ArchKit kit, ArchBuilding home, ArchCutPart cut ) {
var taking = cut.Mode == ArchZoneMode.Carve && ArchCut.Affects( cut, ArchCutAffects.Windows ) && cut.HasContent && ArchLayerGate.On( cut );
var reach = ArchLayerGroups.Reach( plan, cut.Id, home.Id );
foreach ( var room in plan.AllRooms() ) {
if ( reach is not null && plan.OwnerOf( room ) is { } owner && !reach.Contains( owner.Id ) ) {
continue;
}
foreach ( var wall in room.Walls ) {
// Only hand-placed or wall-rhythm openings — never holes another part punched
foreach ( var opening in wall.Openings.Where( opening => opening.OwnerId == 0 || opening.OwnerId == wall.Id ) ) {
var buried = taking && Buried( room, wall, opening, cut );
if ( buried ) {
opening.SwallowedBy = cut.Id;
} else if ( opening.SwallowedBy == cut.Id ) {
opening.SwallowedBy = 0;
}
}
}
}
}
// Only buried when the cut covers the ENTIRE opening — partial clip leaves it standing
static bool Buried( ArchRoom room, ArchWall wall, ArchOpening opening, ArchCutPart cut ) {
foreach ( var leg in cut.Legs() ) {
if ( leg.BaseHeight - room.BaseHeight > opening.SillHeight + 0.5f || leg.TopHeight - room.BaseHeight < opening.Top - 0.5f ) {
continue;
}
foreach ( var (from, to) in ArchFootprint.Inside( leg.Outline(), wall.Start, wall.End ) ) {
if ( from * wall.Length <= opening.Left + 0.5f && to * wall.Length >= opening.Right - 0.5f ) {
return true;
}
}
}
return false;
}
static void Regroup( ArchPlan plan, ArchKit kit, ArchBuilding home, ArchCutPart cut, IReadOnlyList<Bite> bites ) {
var kinds = ArchKinds.Load();
var reached = plan.Buildings
.Where( building => building != home
&& (bites.Any( bite => building.Rooms.Any( room => plan.Filed( ArchKind.Wall, room, kinds ).Contains( bite.Wall ) ) )
|| Touches( building, cut, kit )) )
.ToList();
var homeGroup = ArchLayerGroups.Holding( plan, home.Id );
if ( reached.Count == 0 ) {
if ( homeGroup is { Kind: not ArchAssemblyKind.SharedCore } ) {
ArchLayerGroups.Join( plan, homeGroup, cut.Id );
} else {
ArchLayerGroups.Leave( plan, cut.Id );
}
return;
}
var join = ArchLayerGroups.Holding( plan, cut.Id )
?? homeGroup
?? ArchLayerGroups.Create( plan, ArchLayerGroups.Named( plan ), ArchAssemblyKind.SharedCore, Array.Empty<int>() );
var buildings = join.Children.Where( id => plan.FindBuilding( id ) is not null )
.Concat( reached.Select( building => building.Id ) )
.Append( home.Id )
.Distinct()
.ToList();
foreach ( var member in buildings.Append( cut.Id ) ) {
ArchLayerGroups.Leave( plan, member );
}
join.Children.Clear();
join.Children.AddRange( buildings );
join.Children.Add( cut.Id );
}
static bool Touches( ArchBuilding building, ArchCutPart cut, ArchKit kit ) {
foreach ( var volume in ArchCut.Resolve( cut, kit ) ) {
foreach ( var room in building.Rooms.Where( room => room.HasFootprint ) ) {
var wallHeight = ArchFloorGen.WallHeight( room, kit );
if ( ArchCut.Reaches( volume, room.BaseHeight - kit.FloorThickness, room.BaseHeight + wallHeight )
&& ArchFootprint.Overlaps( ArchFloorGen.Footprint( room ), volume.Footprint ) ) {
return true;
}
}
foreach ( var platform in building.Platforms ) {
if ( ArchCut.Reaches( volume, platform.GradeHeight, platform.TopHeight )
&& ArchFootprint.Overlaps( platform.Outline(), volume.Footprint ) ) {
return true;
}
}
}
return false;
}
static List<Bite> Wanted( ArchPlan plan, ArchKit kit, ArchBuilding home, ArchCutPart cut ) {
var bites = new List<Bite>();
if ( cut.Mode != ArchZoneMode.Carve || !cut.HasContent || !ArchLayerGate.On( cut ) || !ArchCut.Affects( cut, ArchCutAffects.Walls ) ) {
return bites;
}
var reach = ArchLayerGroups.Reach( plan, cut.Id, home.Id );
foreach ( var room in plan.AllRooms() ) {
if ( reach is not null && plan.OwnerOf( room ) is { } owner && !reach.Contains( owner.Id ) ) {
continue;
}
var wallHeight = ArchFloorGen.WallHeight( room, kit );
// Only walls that were standing when the cut was placed (stack-ordered)
foreach ( var wall in room.Walls.Where( wall => wall.Length >= 1f && ArchLayerOrder.Applies( plan, cut.Id, wall.Id ) ) ) {
foreach ( var leg in cut.Legs() ) {
Bitten( room, wall, wallHeight, leg, bites );
}
}
}
return bites;
}
static void Bitten( ArchRoom room, ArchWall wall, float wallHeight, ArchCutSegment leg, List<Bite> into ) {
var sill = MathF.Max( 0f, leg.BaseHeight - room.BaseHeight );
var head = MathF.Min( wallHeight, leg.TopHeight - room.BaseHeight );
if ( head - sill < 1f ) {
return;
}
foreach ( var (from, to) in ArchFootprint.Inside( leg.Outline(), wall.Start, wall.End ) ) {
var start = from * wall.Length;
var finish = to * wall.Length;
if ( finish - start < LeastBite ) {
continue;
}
into.Add( new Bite {
Wall = wall,
Offset = (start + finish) * 0.5f,
Width = finish - start,
Sill = sill,
Height = head - sill
} );
}
}
// Plan-wide whatever the cut's reach, or a cut regrouped out of a scope leaves its old holes standing
// with nobody left who is allowed to close them.
public static void Close( ArchPlan plan, ArchCutPart cut ) {
ArchOwned.Close( plan, ArchOwned.Openings, cut.Id );
}
public static void Remove( ArchPlan plan, ArchCutPart cut ) {
Close( plan, cut );
ArchLayerGroups.Leave( plan, cut.Id );
}
sealed class Bite {
public ArchWall Wall;
public float Offset;
public float Width;
public float Sill;
public float Height;
}
}