Editor subtool for placing fence architecture. Lets the user click points to create an ArchFencePart, samples terrain to drape nodes, previews ghost geometry and creates a fence part on finish.
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.";
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 );
protected override void BuildOptions( ToolSidebarWidget panel )
{
ArchSidebarSection.Show( panel, Scope( "placement" ), "Placement", group =>
group.Add( ArchPartUi.Check( "Follow terrain", conform, value => conform = value ) ) );
ArchBarrierUi.Build( panel, spec, Refresh );
}
}