editor/AdvancedMappingTools/Procedural/TreeGenerator.cs

Editor-only procedural tree tool. Defines a TreeSettings prop with kind and height, a TreeGenerator that builds palm, olive, cypress, bush and pine shapes from primitives, and a TreeEditor primitive tool wrapper.

namespace AdvancedMappingTools;

public enum TreeKind
{
	Palm,
	Olive,
	[Title( "Cypress" )] Cypress,
	Pine,
	Bush
}

public sealed class TreeSettings : PropSettings
{
	[Property, Group( "Tree" ), WideMode]
	public TreeKind Kind { get; set; } = TreeKind.Palm;

	[Property, Group( "Tree" ), Range( 32, 2048, slider: false ), WideMode]
	public float Height { get; set; } = 320.0f;

	public override string PropName => Kind.ToString();
}

/// <summary>
/// A stylised Mediterranean tree: palm, olive, cypress or pine. Trunk and crown are separate
/// parts, so a tree can be edited or replaced part by part.
/// </summary>
public sealed class TreeGenerator : PropGenerator
{
	public override string Id => "tree";
	public override string Title => "Tree";
	public override Type SettingsType => typeof( TreeSettings );

	protected override void Build( PropSettings settings, PropBuilder b )
	{
		var s = (TreeSettings)settings;
		var height = MathF.Max( s.Height, 32.0f );

		switch ( s.Kind )
		{
			case TreeKind.Palm: Palm( b, height ); break;
			case TreeKind.Olive: Olive( b, height ); break;
			case TreeKind.Cypress: Cypress( b, height ); break;
			case TreeKind.Bush: Bush( b, height ); break;
			default: Pine( b, height ); break;
		}
	}

	static void Palm( PropBuilder b, float height )
	{
		b.Round( "trunk", "Trunk", b.Main, 0, 0, 5.5f, 2.5f, 0.0f, height );

		var top = height;
		var fronds = 9;

		for ( var i = 0; i < fronds; i++ )
		{
			var angle = i * MathF.PI * 2.0f / fronds;
			var cos = MathF.Cos( angle );
			var sin = MathF.Sin( angle );
			var start = new Vector3( 0.0f, 0.0f, top );
			var mid = new Vector3( cos * height * 0.18f, sin * height * 0.18f, top - height * 0.05f );
			var tip = new Vector3( cos * height * 0.3f, sin * height * 0.3f, top - height * 0.2f );

			b.Loose( $"frond{i}", "Frond", b.Detail, kit =>
			{
				kit.Beam( start, mid, 3.0f, 1.2f );
				kit.Beam( mid, tip, 2.2f, 0.8f );
			} );
		}

		b.LooseBox( "coconuts", "Coconuts", b.Metal, new Vector3( -6, -6, top - 10 ), new Vector3( 6, 6, top - 2 ) );
	}

	static void Olive( PropBuilder b, float height )
	{
		b.Round( "trunk", "Trunk", b.Main, 0, 0, 7.0f, 4.0f, 0.0f, height * 0.5f );

		b.Round( "crown_low", "Crown", b.Detail, 0, 0, height * 0.28f, height * 0.32f, height * 0.42f, height * 0.62f, 12 );
		b.Round( "crown_mid", "Crown", b.Detail, 0, 0, height * 0.34f, height * 0.3f, height * 0.52f, height * 0.78f, 12 );
		b.Round( "crown_top", "Crown", b.Detail, 0, 0, height * 0.24f, height * 0.1f, height * 0.7f, height * 0.92f, 12 );
	}

	static void Cypress( PropBuilder b, float height )
	{
		b.Round( "trunk", "Trunk", b.Main, 0, 0, 5.0f, 4.0f, 0.0f, height * 0.2f );
		b.Round( "body", "Cypress", b.Detail, 0, 0, height * 0.12f, 1.0f, height * 0.12f, height, 14 );
	}

	static void Bush( PropBuilder b, float height )
	{
		var r = height * 0.45f;

		b.Round( "crown_low", "Bush", b.Detail, 0, 0, r, r * 0.8f, 0.0f, height * 0.45f, 10 );
		b.Round( "crown_mid", "Bush", b.Detail, r * 0.35f, -r * 0.2f, r * 0.75f, r * 0.6f, height * 0.3f, height * 0.7f, 10 );
		b.Round( "crown_top", "Bush", b.Detail, -r * 0.3f, r * 0.25f, r * 0.55f, r * 0.4f, height * 0.5f, height * 0.85f, 10 );
	}

	static void Pine( PropBuilder b, float height )
	{
		b.Round( "trunk", "Trunk", b.Main, 0, 0, 8.0f, 5.0f, 0.0f, height * 0.4f );

		b.Round( "crown_low", "Crown", b.Detail, 0, 0, height * 0.22f, height * 0.12f, height * 0.25f, height * 0.58f, 14 );
		b.Round( "crown_mid", "Crown", b.Detail, 0, 0, height * 0.18f, height * 0.08f, height * 0.46f, height * 0.76f, 14 );
		b.Round( "crown_top", "Crown", b.Detail, 0, 0, height * 0.12f, 1.0f, height * 0.65f, height * 0.98f, 14 );
	}
}

/// <summary>
/// Places a single tree. Palm, olive, cypress and pine are the four Mediterranean kinds.
/// </summary>
[ToolCategory( "Vegetation" )]
[Title( "Tree" ), Icon( "park" )]
public sealed class TreeEditor( PrimitiveTool tool ) : PropEditor( tool )
{
	readonly TreeGenerator _generator = new();

	public TreeSettings Settings { get; } = new();

	protected override IPartGenerator Generator => _generator;

	public override PropSettings Props => Settings;
}