Editor tool that draws a hedge or a row of planters along a path. HedgeRunGenerator sweeps a rounded, box or tapered cross section along the curve (with seeded bulges), or places square/round planters with rim, soil and a shrub or tree lathe; HedgeRunEditor wires it into the primitive path tool with live preview and object placement.
namespace AdvancedMappingTools;
public enum HedgeRunStyle
{
Hedge,
Planter
}
public enum HedgeShape
{
Rounded,
Box,
Tapered
}
public enum PlanterShape
{
Square,
Round
}
public enum PlanterPlant
{
Shrub,
Tree
}
public sealed class HedgeRunSettings : GeneratorSettings
{
public PathCurve Path { get; set; } = PathCurve.FromPoints( [Vector3.Zero, new Vector3( 512.0f, 0.0f, 0.0f )] );
[Property, Group( "Hedge Run" ), Title( "Style" ), WideMode]
public HedgeRunStyle Style { get; set; } = HedgeRunStyle.Hedge;
[Property, Group( "Hedge Run" ), Range( 8, 256, slider: false ), WideMode, Description( "Height of the hedge, or of the plants above the planters." )]
public float Height { get; set; } = 64.0f;
[Property, Group( "Hedge Run" ), Range( 8, 128, slider: false ), WideMode, Description( "Width of the hedge at its foot, or the size of a planter." )]
public float Width { get; set; } = 32.0f;
[Property, ShowIf( nameof( Style ), HedgeRunStyle.Planter ), Group( "Hedge Run" ), Title( "Planter Spacing" ), Range( 16, 256, slider: false ), WideMode, Description( "Distance between planters when the style is Planter." )]
public float Spacing { get; set; } = 64.0f;
[Property, Group( "Hedge Run" ), Range( 0, 9999, slider: false ), WideMode]
public int Seed { get; set; } = 1;
[Property, ShowIf( nameof( Style ), HedgeRunStyle.Hedge ), Group( "Hedge Run" ), Title( "Hedge Shape" ), WideMode, Description( "Rounded crown, a sharp cut box, or narrowing towards the top." )]
public HedgeShape Shape { get; set; } = HedgeShape.Rounded;
[Property, ShowIf( nameof( Style ), HedgeRunStyle.Hedge ), Group( "Hedge Run" ), Title( "Unevenness" ), Range( 0, 1 ), WideMode, Description( "How much the hedge bulges and dips along its length." )]
public float Unevenness { get; set; } = 0.5f;
[Property, ShowIf( nameof( Style ), HedgeRunStyle.Planter ), Group( "Hedge Run" ), Title( "Planter Shape" ), WideMode]
public PlanterShape Planter { get; set; } = PlanterShape.Square;
[Property, ShowIf( nameof( Style ), HedgeRunStyle.Planter ), Group( "Hedge Run" ), Title( "Plant" ), WideMode, Description( "What grows in the planters." )]
public PlanterPlant Plant { get; set; } = PlanterPlant.Shrub;
[Property, ShowIf( nameof( Style ), HedgeRunStyle.Planter ), Group( "Hedge Run" ), Title( "Planter Height" ), Range( 8, 96, slider: false ), WideMode]
public float PlanterHeight { get; set; } = 28.0f;
[Property, Advanced, Group( "Hedge Run" ), Title( "Curve Detail" ), Range( 4, 128, slider: false ), WideMode, Description( "Longest straight piece in a curve. Smaller is rounder." )]
public float CurveDetail { get; set; } = 16.0f;
}
/// <summary>
/// A trimmed hedge along a line, one body through corners and curves with a rounded, square or
/// tapered section that bulges a little along its length; or a row of square or round planters
/// with rim, soil and a shrub or small tree in each.
/// </summary>
public sealed class HedgeRunGenerator : IPartGenerator
{
public string Id => "hedgerun";
public string Title => "Hedge & Planter Run";
public Type SettingsType => typeof( HedgeRunSettings );
const string HedgeCategory = "hedge";
const int CrownPoints = 5;
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (HedgeRunSettings)settings;
var root = context.Group( "root", "Hedge Run", "Building", HedgeCategory );
if ( s.Path is null || s.Path.Nodes.Count < 2 ) return root;
var height = MathF.Max( s.Height, 8.0f );
var width = MathF.Max( s.Width, 8.0f );
var leaves = context.Material( null );
var track = s.Path.Track( Math.Clamp( s.CurveDetail, 4.0f, 128.0f ), FrameMode.Upright, 2.0f ).Plumbed().Reach( width, width );
if ( track.Count < 2 || track.Length < 1.0f ) return root;
if ( s.Style == HedgeRunStyle.Hedge ) Hedge( s, context, root, track, width, height, leaves );
else Planters( s, context, root, track, width, height, leaves );
return root;
}
/// <summary>
/// A smooth wave from the seed, between -1 and 1, the same for the same distance.
/// </summary>
static float Wave( int seed, float d, float salt )
{
var a = (seed * 0.137f + salt) % 6.283f;
return (MathF.Sin( d * 0.031f + a ) * 0.55f + MathF.Sin( d * 0.077f + a * 2.3f ) * 0.3f + MathF.Sin( d * 0.19f + a * 3.1f ) * 0.15f);
}
static void Hedge( HedgeRunSettings s, GenContext context, PartPlan root, PathTrack track, float width, float height, Material leaves )
{
var closed = s.Path.Closed;
var frames = closed ? track.Count - 1 : track.Count;
var uneven = Math.Clamp( s.Unevenness, 0.0f, 1.0f );
// Section across the hedge, counter clockwise in the left/up plane, scaled per station.
List<Vector2> Section( float half, float h )
{
var top = s.Shape switch { HedgeShape.Tapered => half * 0.55f, HedgeShape.Box => half, _ => half * 0.92f };
var points = new List<Vector2> { new( -half, 0.0f ), new( half, 0.0f ) };
if ( s.Shape == HedgeShape.Rounded )
{
// The crown is a quarter circle on each side, its radius a third of the width.
var r = MathF.Min( top * 0.66f, h * 0.4f );
points.Add( new( top, h - r ) );
// Up to a quarter turn, so the crown reaches the full height.
for ( var i = 1; i <= CrownPoints + 1; i++ )
{
var a = MathF.PI * 0.5f * i / (CrownPoints + 1);
points.Add( new( top - r + MathF.Cos( a ) * r, h - r + MathF.Sin( a ) * r ) );
}
for ( var i = CrownPoints + 1; i >= 1; i-- )
{
var a = MathF.PI * 0.5f * i / (CrownPoints + 1);
points.Add( new( -(top - r + MathF.Cos( a ) * r), h - r + MathF.Sin( a ) * r ) );
}
points.Add( new( -top, h - r ) );
}
else
{
points.Add( new( top, h ) );
points.Add( new( -top, h ) );
}
return points;
}
var count = Section( 1.0f, 1.0f ).Count;
var rings = new Vector3[count][];
for ( var p = 0; p < count; p++ ) rings[p] = new Vector3[frames];
for ( var i = 0; i < frames; i++ )
{
var d = track.Frames[i].Distance;
var half = width * 0.5f * (1.0f + 0.08f * uneven * Wave( s.Seed, d, 0.0f ));
var h = height * (1.0f + 0.07f * uneven * Wave( s.Seed, d, 1.7f ));
// A closed hedge has to meet itself where it started.
if ( closed && i == 0 ) { half = width * 0.5f; h = height; }
var section = Section( half, h );
for ( var p = 0; p < count; p++ )
rings[p][i] = track.PlaceBetween( Math.Min( i, track.Count - 2 ), i == track.Count - 1 ? 1.0f : 0.0f, section[p].x, section[p].y );
}
root.Add( context.Solid( "hedge", "Hedge", "Hedge", HedgeCategory, leaves, kit => { kit.SmoothingAngle = 60.0f; kit.Sweep( rings, closed, true ); } ) );
}
static void Planters( HedgeRunSettings s, GenContext context, PartPlan root, PathTrack track, float width, float height, Material leaves )
{
var size = width;
var spacing = MathF.Max( s.Spacing, size * 1.25f );
var count = Math.Max( 1, (int)MathF.Floor( (track.Length - size) / spacing ) + 1 );
var start = (track.Length - (count - 1) * spacing) * 0.5f;
var body = context.Material( null );
var pot = MathF.Max( s.PlanterHeight, 8.0f );
var stream = context.Stream( HedgeCategory, "plants" );
// Planters keep their distance in the plane as well: at a corner two stations along the line come closer.
// Square planters turned against each other need their diagonal, trees the width of two crowns.
var footprint = s.Planter == PlanterShape.Square ? (size + 2.0f) * 1.42f : size + 2.0f;
var crowns = s.Plant == PlanterPlant.Tree ? MathF.Min( size * 0.8f, height * 0.45f ) * 2.0f * 1.15f + 1.0f : 0.0f;
var clear = MathF.Max( MathF.Max( size * 1.25f, footprint + 1.0f ), crowns );
var kept = new List<PathFrame>();
for ( var n = 0; n < count; n++ )
{
var candidate = track.FrameAt( Math.Clamp( start + n * spacing, 0.0f, track.Length ) );
if ( kept.All( k => (k.Position - candidate.Position).WithZ( 0 ).Length >= clear ) ) kept.Add( candidate );
}
for ( var n = 0; n < kept.Count; n++ )
{
var frame = kept[n];
var forward = frame.Forward.WithZ( 0.0f ).Normal;
var yaw = MathF.Atan2( forward.y, forward.x ) * 180.0f / MathF.PI;
var at = new Transform( frame.Position, Rotation.FromYaw( yaw ) );
var grow = stream.Range( 0.85f, 1.15f );
var id = $"planter{n:00}";
var half = size * 0.5f;
var rim = 2.5f;
root.Add( context.Solid( id, "Planter", "Planter", HedgeCategory, body, kit =>
{
// Body and rim bars meet face to face on purpose.
kit.LiftFlushFaces = false;
kit.Frame = at;
if ( s.Planter == PlanterShape.Round )
{
kit.Cylinder( Vector3.Zero, half * 0.82f, half, 0.0f, pot, 20 );
kit.Ring( Vector3.Zero, half + 1.0f, half - rim, pot, pot + 2.0f, 20 );
}
else
{
var low = half * 0.85f;
kit.Hex( new Vector3( -low, -low, 0 ), new Vector3( low, -low, 0 ), new Vector3( low, low, 0 ), new Vector3( -low, low, 0 ),
new Vector3( -half, -half, pot ), new Vector3( half, -half, pot ), new Vector3( half, half, pot ), new Vector3( -half, half, pot ) );
// The rim: four bars around the soil, the front and back ones full length.
kit.Box( new Vector3( -half - 1, -half - 1, pot ), new Vector3( half + 1, -half + rim, pot + 2 ) );
kit.Box( new Vector3( -half - 1, half - rim, pot ), new Vector3( half + 1, half + 1, pot + 2 ) );
kit.Box( new Vector3( -half - 1, -half + rim, pot ), new Vector3( -half + rim, half - rim, pot + 2 ) );
kit.Box( new Vector3( half - rim, -half + rim, pot ), new Vector3( half + 1, half - rim, pot + 2 ) );
}
} ) );
root.Add( context.Solid( $"{id}/soil", "Soil", "Soil", HedgeCategory, body, kit =>
{
kit.Frame = at;
if ( s.Planter == PlanterShape.Round ) kit.Cylinder( Vector3.Zero, half - rim, half - rim, pot, pot + 1.0f, 20 );
else kit.Box( new Vector3( -half + rim, -half + rim, pot ), new Vector3( half - rim, half - rim, pot + 1.0f ) );
} ) );
var soil = pot + 1.0f;
var plantHeight = height * grow;
if ( s.Plant == PlanterPlant.Tree )
{
var trunk = MathF.Max( size * 0.05f, 1.0f );
var crown = MathF.Min( half * 1.6f, plantHeight * 0.45f );
var crownBottom = soil + plantHeight - crown * 1.8f;
root.Add( context.Solid( $"{id}/trunk", "Trunk", "Trunk", HedgeCategory, body, kit =>
{
kit.Frame = at;
kit.Cylinder( Vector3.Zero, trunk * 1.2f, trunk, soil, crownBottom, 8 );
} ) );
root.Add( context.Solid( $"{id}/crown", "Crown", "Leaves", HedgeCategory, leaves, kit =>
{
kit.Frame = at;
kit.SmoothingAngle = 70.0f;
kit.Lathe( Vector3.Zero, [new( 0, crownBottom ), new( crown * 0.7f, crownBottom + crown * 0.25f ), new( crown, crownBottom + crown * 0.8f ), new( crown * 0.85f, crownBottom + crown * 1.35f ), new( crown * 0.45f, crownBottom + crown * 1.7f ), new( 0, crownBottom + crown * 1.8f )], 14 );
} ) );
}
else
{
var r = MathF.Min( half - rim - 0.5f, plantHeight * 0.6f ) * 1.15f;
root.Add( context.Solid( $"{id}/shrub", "Shrub", "Leaves", HedgeCategory, leaves, kit =>
{
kit.Frame = at;
kit.SmoothingAngle = 70.0f;
kit.Lathe( Vector3.Zero, [new( 0, soil ), new( r * 0.75f, soil + plantHeight * 0.1f ), new( r, soil + plantHeight * 0.4f ), new( r * 0.92f, soil + plantHeight * 0.72f ), new( r * 0.6f, soil + plantHeight * 0.93f ), new( 0, soil + plantHeight )], 16 );
} ) );
}
}
}
}
/// <summary>
/// Draws a hedge or a planter row along a line, straight, with bends, as a curve or as a closed loop. The line is the centre.
/// </summary>
[ToolCategory( "Vegetation" )]
[Title( "Hedge & Planter Run" ), Icon( "park" )]
public sealed class HedgeRunEditor : PathEditor
{
readonly HedgeRunGenerator _generator = new();
public HedgeRunEditor( PrimitiveTool tool ) : base( tool ) { }
public HedgeRunSettings Settings { get; } = new();
public override object ExtraSettings => Settings;
protected override int MinPoints => 2;
protected override bool CanClose => true;
protected override bool PlacesObjects => true;
protected override bool AllowsHeight => true;
protected override IEnumerable<string> Hints => [.. base.Hints, "Hedge or planter row, depending on Style"];
protected override void BuildMesh( MeshKit kit ) { }
void Prepare() => Settings.Path = Curve();
protected override PolygonMesh BuildPreview()
{
Prepare();
return PartService.Plan( _generator, Settings ).Flatten( new Transform( Vector3.Zero, Rotation.Identity ) );
}
protected override void PlaceObjects()
{
Prepare();
PartTool.Place( _generator, Settings, new Transform( Vector3.Zero, Rotation.Identity ) );
}
}