editor/AdvancedMappingTools/Placement/DistrictEditor.cs
namespace AdvancedMappingTools;
/// <summary>
/// Click the outline of a quarter and get its layout: real roads with their junctions, blocks with
/// curbs and one pad per plot. The pad height shows what the plot is for. Buildings, canals and bridges are optional editable generated objects.
/// </summary>
[ToolCategory( "World" )]
[Title( "World Builder" ), Icon( "location_city" )]
public sealed partial class DistrictEditor( PrimitiveTool tool ) : PathEditor( tool )
{
protected override int MinPoints => 3;
protected override bool AlwaysClosed => true;
protected override IEnumerable<string> Hints => [.. base.Hints, "Click the outline of the quarter", "Change Style: block, street and plot sizes follow", "An outline smaller than one block gives a flat square"];
public override bool ChangesOtherSettings( string property ) => property is nameof( Style ) or nameof( BuildingMix ) or nameof( PlaceBuildings ) or nameof( AddCanal ) or nameof( AddOverpass ) or nameof( VaryStreets );
DistrictStyle _style = DistrictStyle.Residential;
// Style comes first, so loading saved settings sets it before the sizes it would reset.
[Property, Group( "District" ), WideMode, Description( "Sets block, street and plot sizes and what stands on the plots. Change any size afterwards." )]
public DistrictStyle Style
{
get => _style;
set
{
if ( _style == value ) return;
_style = value;
var d = DistrictLayout.Defaults( value );
BlockSize = d.BlockSize;
StreetWidth = d.StreetWidth;
PlotWidth = d.PlotWidth;
PlotDepth = d.PlotDepth;
Irregularity = d.Irregularity;
RoadType = DefaultRoad( value );
}
}
/// <summary>
/// The kind of road each style lays, before it is fitted to <see cref="StreetWidth"/>.
/// </summary>
public static RoadType DefaultRoad( DistrictStyle style ) => style switch
{
DistrictStyle.Industrial => RoadType.Industrial,
DistrictStyle.OldTown or DistrictStyle.Coastal => RoadType.Medieval,
_ => RoadType.Residential,
};
[Property, Group( "District" ), Title( "Block Size" ), Range( 256, 16384, slider: false ), WideMode, Description( "Distance between two streets, without the street." )]
public float BlockSize { get; set; } = 2048.0f;
[Property, Group( "District" ), Title( "Street Width" ), Range( 64, 2048, slider: false ), WideMode]
public float StreetWidth { get; set; } = 384.0f;
[Property, Group( "District" ), Title( "Grid Angle" ), Range( -180, 180, slider: false ), WideMode, Description( "Turns the street grid inside the outline." )]
public float GridAngle { get; set; } = 0.0f;
[Property, Group( "District" ), Range( 0, 0.4f ), WideMode, Description( "Zero gives a straight grid. Higher values move the crossings for grown, crooked streets." )]
public float Irregularity { get; set; } = 0.0f;
[Property, Group( "District" ), WideMode, Description( "Another number gives other crossings and other uses for the plots." )]
public int Seed { get; set; } = 1;
[Property, Group( "Streets" ), Title( "Place Roads" ), WideMode, Description( "Lays real Road objects along the streets, the junctions come by themselves. Off, the streets are one flat surface in the layout." )]
public bool PlaceRoads { get; set; } = true;
[Property, ShowIf( nameof( PlaceRoads ), true ), Group( "Streets" ), Title( "Road Type" ), WideMode, Description( "Paving, curbs, sidewalks and lamps of the roads. Lanes and sidewalks are fitted to Street Width. Without room for sidewalks, signs, lamps and props stay off." )]
public RoadType RoadType { get; set; } = RoadType.Residential;
[Property, Group( "Plots" ), Title( "Plot Width" ), Range( 64, 8192, slider: false ), WideMode, Description( "Frontage of a plot along the street." )]
public float PlotWidth { get; set; } = 448.0f;
[Property, Group( "Plots" ), Title( "Plot Depth" ), Range( 64, 8192, slider: false ), WideMode, Description( "How far a plot reaches from the street into the block. At most half the block." )]
public float PlotDepth { get; set; } = 512.0f;
[Property, Group( "Plots" ), Title( "Plot Gap" ), Range( 0, 64, slider: false ), WideMode, Description( "Space left between the pads of two plots, so each one is visible." )]
public float PlotGap { get; set; } = 8.0f;
[Property, Group( "Plots" ), Title( "Curb Height" ), Range( 0, 32, slider: false ), WideMode, Description( "Height of the blocks above the street." )]
public float CurbHeight { get; set; } = 6.0f;
public DistrictLayoutSettings LayoutSettings() => new()
{
Style = Style,
BlockSize = BlockSize,
BlockAspect = BlockAspect * (StreetPattern == WorldStreetPattern.LongBlocks ? 2 : 1),
StreetWidth = StreetWidth,
PlotWidth = PlotWidth,
PlotDepth = PlotDepth,
Irregularity = StreetPattern == WorldStreetPattern.Organic ? MathF.Max( Irregularity, .12f ) : Irregularity,
Angle = GridAngle,
Seed = Seed,
};
/// <summary>
/// The plan for the drawn outline, in the plane of the outline.
/// </summary>
public DistrictPlan Layout() => DistrictLayout.Plan( SamplePath().Select( x => new Vector2( x.x, x.y ) ).ToList(), LayoutSettings() );
protected override bool IsShapeValid => SamplePath().Count >= 3 && MathF.Abs( SignedArea( SamplePath() ) ) > 1.0f;
/// <summary>
/// Pad height above the block per use, so the layout reads at a glance before any building stands there.
/// </summary>
public static float PadHeight( PlotUse use ) => use switch
{
PlotUse.Apartment => 12.0f,
PlotUse.Warehouse => 16.0f,
PlotUse.Parking => 1.0f,
PlotUse.Green => 0.0f,
_ => 4.0f,
};
protected override void BuildMesh( MeshKit kit )
{
var ring = SamplePath();
if ( SignedArea( ring ) < 0.0f )
ring.Reverse();
var z = ring.Average( x => x.z );
var plan = DistrictLayout.Plan( ring.Select( x => new Vector2( x.x, x.y ) ).ToList(), LayoutSettings() );
// Without real roads the ground between the blocks is the street, its top on the drawn outline.
// An outline too small for a single block becomes one flat square.
var outline = ring.Select( x => new Vector2( x.x, x.y ) ).ToList();
if ( !PlaceRoads || plan.Blocks.Count == 0 )
foreach ( var land in DistrictContent.Land( this, outline, outline ) )
Slab( kit, land, z - GroundThickness, z );
// Every body is closed and rests a hair above the one below. Touching, their faces would
// lie back to back inside one mesh.
kit.Material = TrimMaterial;
var curb = MathF.Max( CurbHeight, 0.0f );
var blockTop = curb > 0.0f ? z + Rest + curb : z;
foreach ( var block in plan.Blocks )
foreach ( var land in DistrictContent.Land( this, block.Ring, outline ) )
Slab( kit, land, curb > 0 ? z + Rest : z - GroundThickness, blockTop );
kit.Material = SurfaceMaterial;
foreach ( var plot in plan.Plots )
{
var height = PadHeight( plot.Use );
if ( height <= 0.0f )
continue;
var pad = Shrink( plot.Ring, PlotGap * 0.5f ) ?? plot.Ring;
foreach ( var land in DistrictContent.Land( this, pad, outline ) )
Slab( kit, land, blockTop + Rest, blockTop + Rest + height );
}
if ( IsPreview )
DistrictContent.Preview( kit, DistrictContent.Plan( this, plan, outline, z ) );
}
readonly RoadGenerator _roads = new();
/// <summary>
/// A road of <paramref name="type"/> exactly <paramref name="width"/> wide from sidewalk edge to
/// sidewalk edge, so it fills the street between two blocks. Lanes get narrower or fewer, and
/// sidewalks and verges go first when the street is too narrow for them.
/// </summary>
public static RoadSettings RoadFor( RoadType type, float width )
{
var s = new RoadSettings { Type = type };
RoadTypes.Apply( s, type );
// Both sides alike, the road runs down the middle of the street.
var walk = s.LeftSidewalk && s.RightSidewalk;
var curb = s.LeftCurb && s.RightCurb;
s.LeftSidewalk = s.RightSidewalk = walk;
s.LeftCurb = s.RightCurb = curb;
float Side() => (s.Shoulder == ShoulderKind.Gravel ? s.ShoulderWidth : 0.0f) + (s.LeftCurb ? s.GutterWidth + s.CurbWidth : 0.0f) + s.VergeWidth + (s.LeftSidewalk ? s.SidewalkWidth : 0.0f);
float Middle() => (s.Divider != RoadDivider.None ? s.DividerWidth : 0.0f) + (s.Shoulder == ShoulderKind.Paved ? s.ShoulderWidth * 2.0f : 0.0f);
float Lanes() => width - Side() * 2.0f - Middle();
const float NarrowestLane = 48.0f;
if ( Lanes() < s.Lanes * NarrowestLane ) s.VergeWidth = 0.0f;
if ( Lanes() < s.Lanes * NarrowestLane ) s.LeftSidewalk = s.RightSidewalk = false;
if ( Lanes() < s.Lanes * NarrowestLane && s.Shoulder != ShoulderKind.None ) s.Shoulder = ShoulderKind.None;
if ( Lanes() < s.Lanes * NarrowestLane ) s.Lanes = Math.Max( 1, (int)(Lanes() / NarrowestLane) );
// A wide street gets more lanes rather than lanes wider than a road allows.
s.Lanes = Math.Max( s.Lanes, (int)MathF.Ceiling( Lanes() / 512.0f ) );
s.LaneWidth = Math.Clamp( Lanes() / s.Lanes, 24.0f, 512.0f );
// Without sidewalks there is no room beside the road: signs, lamps and props would stand on the plots.
if ( !s.LeftSidewalk )
{
s.Signs = false;
s.StreetLights = false;
s.RoadsideProps = false;
}
s.Rise = 0.0f;
s.Stretches = [];
return s;
}
/// <summary>
/// The full width of a road from one outer edge to the other.
/// </summary>
public static float FullWidth( RoadSettings s ) =>
s.Half * 2.0f + ((s.Shoulder == ShoulderKind.Gravel ? s.ShoulderWidth : 0.0f) + (s.LeftCurb ? s.GutterWidth + s.CurbWidth : 0.0f) + s.VergeWidth + (s.LeftSidewalk ? s.SidewalkWidth : 0.0f)) * 2.0f;
/// <summary>
/// One road per street of the plan, at the height of the drawn outline.
/// </summary>
public List<RoadSettings> Roads( DistrictPlan plan, float z ) => ContentPlan( plan, z ).Roads;
public DistrictContentPlan ContentPlan( DistrictPlan plan, float z )
{
var outline = SamplePath().Select( p => new Vector2( p.x, p.y ) ).ToList();
if ( outline.Count < 3 )
{
var vertices = plan.Blocks.SelectMany( b => b.Ring ).Concat( plan.Streets.SelectMany( s => new[] { s.A, s.B } ) ).ToList();
if ( vertices.Count == 0 )
return new DistrictContentPlan();
var min = new Vector2( vertices.Min( p => p.x ), vertices.Min( p => p.y ) );
var max = new Vector2( vertices.Max( p => p.x ), vertices.Max( p => p.y ) );
outline = [min, new( max.x, min.y ), max, new( min.x, max.y )];
}
if ( DistrictLayout.Area( outline ) < 0 )
outline.Reverse();
return DistrictContent.Plan( this, plan, outline, z );
}
List<RoadSettings> RoadsOfOutline()
{
var ring = SamplePath();
if ( ring.Count < 3 ) return [];
if ( SignedArea( ring ) < 0.0f ) ring.Reverse();
return Roads( DistrictLayout.Plan( ring.Select( x => new Vector2( x.x, x.y ) ).ToList(), LayoutSettings() ), ring.Average( x => x.z ) );
}
/// <summary>
/// The roads come once the layout object stands, as their own step of undo: they are normal
/// roads of the network and connect to roads already there.
/// </summary>
protected override void OnBuilt( MeshComponent component )
{
if ( SceneEditorSession.Active is null )
return;
if ( !PartService.CanStoreParts )
{
Log.Warning( "World Builder: restart the editor to register generated parts before creating content." );
return;
}
var ring = SamplePath();
var content = ContentPlan( Layout(), ring.Average( p => p.z ) );
var scene = SceneEditorSession.Active.Scene;
if ( AddOverpass && !content.Bridges.Any( b => b.Id == "overpass" ) )
Log.Warning( "World Builder: no continuous street corridor fits an overpass. Enlarge the outline or reduce Block Size." );
var changed = PlaceRoads ? RoadLinker.Network( scene ) : [];
var removed = PlaceRoads ? RoadLinker.Removed( scene, content.Roads ) : [];
// The tool's own undo step around this already records everything created here; recording it
// a second time copied every object twice and took most of the time. This step only keeps
// roads and junctions of the scene that the new streets change or take away.
var undo = SceneEditorSession.Active.UndoScope( "Place World Builder Content" );
if ( changed.Count > 0 )
undo = undo.WithGameObjectChanges( changed, GameObjectUndoFlags.All );
if ( removed.Count > 0 )
undo = undo.WithGameObjectDestructions( removed );
using ( SceneEditorSession.Scope() )
using ( changed.Count + removed.Count > 0 ? undo.Push() : null )
{
if ( PlaceRoads && content.Roads.Count > 0 )
PartTool.PlaceRoads( _roads, content.Roads );
var created = DistrictContent.Place( content );
_lastContentStatus = $"Created {created.Count - content.Bridges.Count - (content.Canal is null ? 0 : 1)} / {content.Buildings.Count} buildings, {content.Bridges.Count} bridges. Limits and the part budget control density.";
Log.Info( $"World Builder: {content.Buildings.Count} building recipes, {content.Bridges.Count} bridges, {(content.Canal is null ? 0 : 1)} canal; {created.Count} content roots created." );
}
}
const float GroundThickness = 8.0f;
const float Rest = 0.1f;
/// <summary>
/// A closed flat slab of a counter clockwise outline.
/// </summary>
static void Slab( MeshKit kit, IReadOnlyList<Vector2> ring, float bottom, float top )
{
if ( ring.Count < 3 || top - bottom < 0.01f ) return;
kit.Face( [.. ring.Select( p => new Vector3( p.x, p.y, top ) )] );
kit.Face( [.. ring.Reverse().Select( p => new Vector3( p.x, p.y, bottom ) )] );
for ( var i = 0; i < ring.Count; i++ )
{
var a = ring[i];
var b = ring[(i + 1) % ring.Count];
kit.Face( new Vector3( a.x, a.y, bottom ), new Vector3( b.x, b.y, bottom ), new Vector3( b.x, b.y, top ), new Vector3( a.x, a.y, top ) );
}
}
/// <summary>
/// A counter clockwise ring moved inward, by clipping it with each edge pushed in. Null when it vanishes.
/// </summary>
static IReadOnlyList<Vector2> Shrink( IReadOnlyList<Vector2> ring, float distance )
{
if ( distance <= 0.0f ) return ring;
var result = ring.ToList();
for ( var i = 0; i < ring.Count && result.Count >= 3; i++ )
{
var a = ring[i];
var b = ring[(i + 1) % ring.Count];
var d = b - a;
if ( d.Length < 1e-3f ) continue;
var n = new Vector2( -d.y, d.x ) / d.Length * distance;
var far = d.Normal * 1e6f;
var side = new Vector2( -d.y, d.x ).Normal * 1e6f;
var half = new List<Vector2> { a + n - far, b + n + far, b + n + far + side, a + n - far + side };
result = DistrictLayout.Clip( result, half );
}
return result.Count >= 3 && DistrictLayout.Area( result ) > 1.0f ? result : null;
}
}