Editor/Tool/Subtools/ArchFenceSubtool.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox;
namespace Sunless.Architecture;
[Title( "Fence" ), Icon( "fence" ), Group( "14" )]
public sealed class ArchFenceSubtool( ArchTool owner ) : ArchSubtool( owner ) {
protected override ArchKind? DraftKind => ArchKind.Fence;
public override ArchSurface[] Surfaces => new[]
{
ArchSurface.Railing, ArchSurface.Panel, ArchSurface.Post, ArchSurface.Wire, ArchSurface.Foundation
};
readonly List<ArchCurveNode> nodes = new();
readonly ArchBarrierSpec spec = new();
bool conform = true;
protected override bool UsesDrag => false;
protected override string Title() => "Fence";
protected override string Advice() => "Click to lay the line. Click the first node again to close the run.";
public override void OnEnabled() {
base.OnEnabled();
ArchWorkflow.Restart( Scope( "flow" ) );
}
protected override ArchRunState Run() => new( nodes.Count > 0, $"{nodes.Count} point{(nodes.Count == 1 ? "" : "s")}" );
protected override void FinishRun() => Finish( false );
protected override void CancelRun() => nodes.Clear();
protected override void OnClick( Vector2 point ) {
if ( nodes.Count >= 3 && (Flat( nodes[0] ) - point).Length < Owner.Kit.GridSize ) {
Finish( true );
return;
}
nodes.Add( ArchCurveNode.At( Grounded( point ) ) );
}
Vector3 Grounded( Vector2 point ) {
if ( conform && ArchTerrain.Sample( Owner.Scene, point, out var sampled ) ) {
return Owner.Grid.Draped( new Vector3( point.x, point.y, sampled + Owner.Kit.GroundClearance ) );
}
return Owner.Grid.Draped( new Vector3( point.x, point.y, Owner.LevelHeight ) );
}
protected override void DrawHover( Vector2 point ) {
base.DrawHover( point );
if ( nodes.Count == 0 ) {
ArchGhost.Note( new Vector3( point.x, point.y, Owner.LevelHeight + spec.Height ), "click to start the fence line" );
return;
}
var line = new List<ArchCurveNode>( nodes ) { ArchCurveNode.At( Grounded( point ) ) };
var curve = ArchCurve.Of( line );
var barrier = ArchBarrierShape.Resolve( curve, spec, 0f, 0f, Owner.Kit );
ArchGhost.Curve( curve );
if ( spec.Style == BarrierStyle.Retaining ) {
ArchGhost.Retaining( ArchRetainingShape.Resolve( barrier, Owner.Kit, new ArchGround( Owner.Scene ) ) );
} else {
ArchGhost.Barrier( barrier, spec );
}
Gizmo.Draw.Color = ArchGhost.Accent;
Gizmo.Draw.LineSphere( nodes[0].Position, Owner.Kit.GridSize * 0.5f );
}
void Finish( bool closed ) {
var building = Owner.ActiveBuilding();
if ( building is not null && nodes.Count >= 2 ) {
var fence = new ArchFencePart {
Id = Owner.Plan.AllocateId(),
Name = $"Fence{building.Fences.Count + 1}",
Nodes = nodes.Select( node => node.Copy() ).ToList(),
Closed = closed,
Barrier = spec.Copy()
};
building.Fences.Add( fence );
FinishDraft( fence.Id );
Owner.Commit( "Create Fence" );
}
nodes.Clear();
}
static Vector2 Flat( ArchCurveNode node ) => new( node.Position.x, node.Position.y );
// One list: how the line is laid, then the barrier's own rows contributed straight into this sequence rather
// than opened as a second one - the style decides which of them exist, and that branch lives with the barrier.
protected override void BuildOptions( ToolSidebarWidget panel ) {
using var flow = ArchWorkflow.In( panel, Scope( "flow" ), Refresh );
flow.Step( "Line", conform ? "Draped" : "Flat", conform ? "landscape" : "horizontal_rule", PickLine );
ArchBarrierUi.Steps( flow, spec, Refresh, null );
}
void PickLine( ArchWorkflowStep step ) {
using var grid = ArchIconGrid.In( step.Layout );
grid.Pick( "Draped — every node drops onto the ground under it, which is what a boundary on a slope needs",
"line_draped", "landscape", conform, () => { conform = true; step.Chose(); } );
grid.Pick( "Flat — the run lies on this storey however the ground falls", "line_flat", "horizontal_rule",
!conform, () => { conform = false; step.Chose(); } );
}
}