Editor/Bool/ArchBool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public enum ArchBoolOp {
Add,
Subtract,
Damage,
Fixture,
Detail,
Extrude
}
public enum ArchBoolAdd {
Slab,
Ramp,
Pillar,
Beam,
Slats
}
public sealed class ArchBoolPlacement {
public ArchBoolOp Op { get; init; }
public ArchBoolAdd Kind { get; init; }
public ArchBoolReach Reach { get; init; }
public ArchRoom Room { get; init; }
public ArchPlatformPart Platform { get; init; }
public ArchCutPart Cut { get; init; }
public ArchPillarPart Pillar { get; init; }
public ArchBeamPart Beam { get; init; }
}
public enum ArchBoolReach {
Dragged,
Through,
IntoFloor,
IntoCeiling,
IntoWall
}
public sealed class ArchBoolDrag {
public ArchBoolForm Form { get; init; }
public int Segments { get; init; } = ArchFootprint.LeastSegments;
public Vector2 Min { get; init; }
public Vector2 Max { get; init; }
public float Bottom { get; init; }
public float Top { get; init; }
public float Least { get; init; } = ArchBool.MinSize;
// Gesture direction — bounds lose the sign, ramp needs it
public float Climb { get; init; }
public bool Stands => Max.x - Min.x >= Least && Max.y - Min.y >= Least;
public float Rise => MathF.Max( 1f, Top - Bottom );
public List<Vector2> Loop() {
return Form == ArchBoolForm.Circle
? ArchFootprint.Ellipse( Min, Max, Segments )
: ArchFootprint.Rect( Min, Max );
}
}
public sealed class ArchBoolHost {
public ArchBuilding Building { get; init; }
public ArchRoadPart Road { get; init; }
public int Id => Road?.Id ?? Building?.Id ?? 0;
public bool Stands => Building is not null || Road is not null;
public List<ArchCutPart> Cuts => Road?.Cuts ?? Building?.Cuts;
// Roads have no storey
public int Storey( int level ) => Road is null ? level : 0;
}
public static class ArchBool {
public const float MinSize = ArchPlatform.MinSize;
// Zones need a smaller minimum than solids (detail features like joints)
public const float ZoneSize = 4f;
public static float Least( ArchBoolOp op ) => op switch {
ArchBoolOp.Damage or ArchBoolOp.Extrude => ZoneSize,
_ => MinSize
};
public static ArchBoolHost Host( ArchPlan plan, ArchBuilding preferred, IReadOnlyList<Vector2> outline ) {
if ( plan is null || outline is not { Count: >= 3 } ) {
return new ArchBoolHost { Building = preferred };
}
return Host( plan, preferred, outline.Aggregate( Vector2.Zero, ( total, point ) => total + point ) / outline.Count );
}
public static ArchBoolHost Host( ArchPlan plan, ArchBuilding preferred, Vector2 centre ) {
if ( plan is null ) {
return new ArchBoolHost { Building = preferred };
}
var standing = plan.Buildings
.Where( building => building.Rooms.Any( room => room.HasFootprint && ArchFootprint.Contains( ArchFloorGen.Footprint( room ), centre ) )
|| building.Platforms.Any( platform => ArchFootprint.Contains( platform.Outline(), centre ) ) )
.ToList();
if ( standing.Contains( preferred ) ) {
return new ArchBoolHost { Building = preferred };
}
if ( standing.FirstOrDefault() is { } found ) {
return new ArchBoolHost { Building = found };
}
if ( Carrying( plan, centre ) is { } road ) {
return new ArchBoolHost { Road = road };
}
return new ArchBoolHost { Building = preferred ?? plan.Buildings.FirstOrDefault() };
}
static ArchRoadPart Carrying( ArchPlan plan, Vector2 point ) {
ArchRoadPart carrying = null;
var nearest = float.MaxValue;
foreach ( var road in plan.Roads() ) {
if ( !road.Curve().Nearest( point, out _, out var gap ) || gap >= nearest || gap > road.Reach() + Bored( road ) ) {
continue;
}
nearest = gap;
carrying = road;
}
return carrying;
}
static float Bored( ArchRoadPart road ) {
var reach = 0f;
foreach ( var tunnel in road.Tunnels ) {
reach = MathF.Max( reach, MathF.Max( 0f, tunnel.Clearance ) + MathF.Max( 2f, tunnel.Lining ) );
}
return reach;
}
public static ArchBoolDrag Sketch(
ArchGridService grid,
ArchKit kit,
ArchBoolOp op,
Vector2 from,
Vector2 to,
float plane,
float reach,
ArchBoolForm form,
int segments,
ArchBoolReach into = ArchBoolReach.Through,
float depth = 0f,
float surface = 0f ) {
var start = grid.Base( from );
var finish = grid.Base( to );
var (bottom, top) = Band( kit, op, into, plane, reach, depth, surface );
return new ArchBoolDrag {
Form = form,
Segments = Math.Max( ArchFootprint.LeastSegments, segments ),
Min = new Vector2( MathF.Min( start.x, finish.x ), MathF.Min( start.y, finish.y ) ),
Max = new Vector2( MathF.Max( start.x, finish.x ), MathF.Max( start.y, finish.y ) ),
Bottom = bottom,
Top = top,
Least = Least( op ),
Climb = ArchRamp.Climbed( from, to )
};
}
static (float Bottom, float Top) Band( ArchKit kit, ArchBoolOp op, ArchBoolReach into, float plane, float reach, float depth, float surface ) {
var bite = ArchLap.Bite( kit );
var bitten = MathF.Max( 1f, depth );
if ( op is not (ArchBoolOp.Subtract or ArchBoolOp.Damage or ArchBoolOp.Extrude) ) {
return (plane, plane + MathF.Max( 4f, reach ));
}
return into switch {
ArchBoolReach.IntoCeiling => (surface - bite, surface + bitten),
ArchBoolReach.IntoFloor => (surface - bitten, surface + bite),
ArchBoolReach.IntoWall => (surface - bite, surface + bitten),
// Through starts below the plane — the plane is the top of the slab being cut
_ => (plane - kit.FloorThickness, plane + MathF.Max( 4f, reach ))
};
}
public static ArchBoolReach Aimed( ArchBoolReach picked, ArchCursorFace face, bool wall = false ) {
if ( picked != ArchBoolReach.Dragged ) {
return picked;
}
if ( wall ) {
return ArchBoolReach.IntoWall;
}
return face == ArchCursorFace.Ceiling ? ArchBoolReach.IntoCeiling : ArchBoolReach.Through;
}
public static ArchCutAffects Aimed( ArchBoolReach into, ArchCutAffects authored ) => into switch {
ArchBoolReach.IntoCeiling => ArchCutAffects.Ceilings | ArchCutAffects.Roofs,
ArchBoolReach.IntoFloor => ArchCutAffects.Floors,
ArchBoolReach.IntoWall => ArchCutAffects.Walls | ArchCutAffects.Trims | ArchCutAffects.Windows,
_ => authored
};
public static float Through( ArchPlan plan, ArchKit kit, int level, Vector2 min, Vector2 max, float plane ) {
var head = plane + kit.WallHeight;
if ( plan is null ) {
return head - plane;
}
var footprint = ArchFootprint.Rect( min, max );
var bite = ArchLap.Bite( kit );
foreach ( var building in plan.Buildings ) {
foreach ( var room in building.Rooms.Where( room => room.Floor >= level && room.HasFootprint ) ) {
if ( ArchFootprint.Overlaps( ArchFloorGen.Footprint( room ), footprint ) ) {
head = MathF.Max( head, room.BaseHeight + ArchFloorGen.WallHeight( room, kit ) + bite );
}
}
}
return ArchAsks.Headroom( plan, kit, level, footprint, head ) - plane;
}
public static object Place( ArchPlan plan, ArchBoolHost host, int level, ArchBoolDrag drag, ArchBoolPlacement placing ) {
if ( !drag.Stands || host is not { Stands: true } ) {
return null;
}
if ( placing.Op != ArchBoolOp.Add ) {
return Sink( plan, host, level, drag, placing );
}
if ( host.Building is null ) {
return null;
}
return placing.Kind switch {
ArchBoolAdd.Slab or ArchBoolAdd.Ramp
=> ArchPlatform.Add( plan, host.Building, level, drag.Bottom, drag.Top, drag.Loop(), Raked( placing, drag ) ),
ArchBoolAdd.Pillar => Column( plan, drag, placing ),
_ => Timber( plan, drag, placing )
};
}
// Works on a copy — the subtool holds the draft for the whole session
static ArchPlatformPart Raked( ArchBoolPlacement placing, ArchBoolDrag drag ) {
var draft = placing.Platform.Dressing();
draft.Ramp = placing.Kind == ArchBoolAdd.Ramp;
if ( draft.Ramp ) {
draft.RampYaw = drag.Climb;
}
return draft;
}
static ArchPillarPart Column( ArchPlan plan, ArchBoolDrag drag, ArchBoolPlacement placing ) {
if ( placing.Room is not { } room ) {
return null;
}
var part = placing.Pillar.Section();
part.Id = plan.AllocateId();
part.Name = $"Pillar{room.Pillars.Count + 1}";
part.Placement = PillarPlacement.Freestanding;
part.Origin = (drag.Min + drag.Max) * 0.5f;
part.BaseHeight = drag.Bottom;
part.Height = drag.Rise;
part.Width = drag.Max.x - drag.Min.x;
part.Depth = drag.Max.y - drag.Min.y;
part.Sides = drag.Form == ArchBoolForm.Circle ? drag.Segments : 0;
room.Pillars.Add( part );
return part;
}
static ArchBeamPart Timber( ArchPlan plan, ArchBoolDrag drag, ArchBoolPlacement placing ) {
if ( placing.Room is not { } room ) {
return null;
}
var beam = Hung( drag, placing );
beam.Id = plan.AllocateId();
beam.Name = $"{(beam.Members == BeamMembers.Field ? "Slats" : "Beam")}{room.Beams.Count + 1}";
beam.Level = room.Floor;
room.Beams.Add( beam );
return beam;
}
public static ArchBeamPart Hung( ArchBoolDrag drag, ArchBoolPlacement placing ) {
var draft = placing.Beam;
var beam = new ArchBeamPart {
Members = placing.Kind == ArchBoolAdd.Slats ? BeamMembers.Field : BeamMembers.One,
TopHeight = drag.Top,
Drop = drag.Rise,
Type = draft.Type,
Sides = draft.Sides,
MemberWidth = draft.MemberWidth,
MemberGap = draft.MemberGap,
Yaw = draft.Yaw,
Level = placing.Room?.Floor ?? 0
};
beam.Reshape( drag.Loop() );
return beam;
}
static ArchCutPart Sink( ArchPlan plan, ArchBoolHost host, int level, ArchBoolDrag drag, ArchBoolPlacement placing ) {
var draft = placing.Cut;
var filed = host.Cuts;
var cut = new ArchCutPart {
Id = plan.AllocateId(),
Name = placing.Op switch {
ArchBoolOp.Damage => $"Damage{filed.Count + 1}",
ArchBoolOp.Extrude => $"Extrude{filed.Count + 1}",
_ => $"Cut{filed.Count + 1}"
},
Level = host.Storey( level ),
Profile = CutProfile.Poly,
Affects = placing.Op == ArchBoolOp.Extrude
? ArchExtrude.Targets( draft.Extrude )
: Aimed( placing.Reach, draft.Affects ),
Ramp = draft.Ramp,
RampFall = draft.RampFall,
RampYaw = draft.Ramp ? drag.Climb : draft.RampYaw,
Enclosure = draft.Enclosure,
WallThickness = draft.WallThickness,
Edge = draft.Edge,
EdgeWidth = draft.EdgeWidth,
Head = draft.Head,
HeadHeight = draft.HeadHeight,
HeadRoof = draft.HeadRoof,
Damage = placing.Op == ArchBoolOp.Damage ? draft.Damage : ArchDamageKind.None,
Extrude = placing.Op == ArchBoolOp.Extrude ? draft.Extrude : ArchExtrudeKind.None,
ExtrudeDepth = draft.ExtrudeDepth,
ExtrudeCourse = draft.ExtrudeCourse,
ExtrudeJoint = draft.ExtrudeJoint,
ExtrudeBothFaces = draft.ExtrudeBothFaces,
ExtrudePillarType = draft.ExtrudePillarType,
ExtrudeFooting = draft.ExtrudeFooting,
DamageAmount = draft.DamageAmount,
DamageCellWidth = draft.DamageCellWidth,
DamageCellLength = draft.DamageCellLength,
DamageDepth = draft.DamageDepth,
MasonryPattern = draft.MasonryPattern,
MasonryCarves = draft.MasonryCarves,
DamageDrop = draft.DamageDrop,
DamageTilt = draft.DamageTilt
};
cut.Segments.Add( new ArchCutSegment {
Start = drag.Min,
End = drag.Max,
BaseHeight = drag.Bottom,
TopHeight = drag.Top,
Loop = drag.Loop()
} );
filed.Add( cut );
return cut;
}
}