editor/AdvancedMappingTools/Geometry/RoadMarkingsEditor.cs

An editor tool that paints road markings along a drawn path as generated geometry. Supports a solid line, dashes with configurable length and gap, and a double line, and builds the ribbon quads into a MeshKit mesh with a set line width and height offset.

namespace AdvancedMappingTools;

public enum MarkingStyle
{
	Solid,
	Dashed,
	[Title( "Double" )] Double
}

/// <summary>
/// Painted road markings along a path: a solid line, dashes or a double line.
/// </summary>
[ToolCategory( "Roads & Paths" )]
[Title( "Road Markings" ), Icon( "more_horiz" )]
public sealed class RoadMarkingsEditor( PrimitiveTool tool ) : PathEditor( tool )
{
	protected override int MinPoints => 2;

	[Property, Group( "Markings" ), WideMode]
	public MarkingStyle Style { get; set; } = MarkingStyle.Solid;

	[Property, Group( "Markings" ), Range( 1, 32, slider: false ), WideMode]
	public float LineWidth { get; set; } = 4.0f;

	[Property, Group( "Markings" ), Title( "Dash Length" ), Range( 4, 256, slider: false ), WideMode]
	public float DashLength { get; set; } = 32.0f;

	[Property, Group( "Markings" ), Title( "Dash Gap" ), Range( 4, 256, slider: false ), WideMode]
	public float DashGap { get; set; } = 32.0f;

	[Property, Group( "Markings" ), Title( "Height" ), Range( 0, 4, slider: false ), WideMode]
	public float Lift { get; set; } = 0.6f;

	protected override void BuildMesh( MeshKit kit )
	{
		var path = SamplePath();

		if ( path.Count < 2 ) return;

		var width = MathF.Max( LineWidth, 1.0f );
		var lift = MathF.Max( Lift, 0.0f );
		var dash = Style == MarkingStyle.Solid ? 0.0f : MathF.Max( DashLength, 4.0f );
		var gap = Style == MarkingStyle.Solid ? 0.0f : MathF.Max( DashGap, 4.0f );
		var lines = Style == MarkingStyle.Double ? 2 : 1;
		var offset = width * 1.4f;
		var walked = 0.0f;

		kit.Material = TrimMaterial;

		for ( var i = 0; i + 1 < path.Count; i++ )
		{
			var a = path[i];
			var b = path[i + 1];
			var length = (b - a).Length;

			if ( length < 0.01f ) continue;

			var direction = (b - a) / length;
			var normal = new Vector3( -direction.y, direction.x, 0.0f );

			for ( var line = 0; line < lines; line++ )
			{
				var shift = lines == 1 ? 0.0f : (line == 0 ? -offset : offset) * 0.5f;

				if ( dash <= 0.0f )
				{
					Ribbon( kit, a, b, normal, width, lift, shift );
					continue;
				}

				var pattern = dash + gap;
				var segmentStart = walked;
				var segmentEnd = walked + length;

				for ( var k = (int)Math.Floor( segmentStart / pattern ); k * pattern < segmentEnd; k++ )
				{
					var fromDistance = MathF.Max( k * pattern, segmentStart );
					var toDistance = MathF.Min( k * pattern + dash, segmentEnd );

					if ( toDistance <= fromDistance ) continue;

					var from = a + direction * (fromDistance - segmentStart);
					var to = a + direction * (toDistance - segmentStart);

					Ribbon( kit, from, to, normal, width, lift, shift );
				}
			}

			walked += length;
		}
	}

	static void Ribbon( MeshKit kit, Vector3 a, Vector3 b, Vector3 normal, float width, float lift, float shift )
	{
		var n = normal * (width * 0.5f) + normal * shift;

		kit.Face( a + n + Vector3.Up * lift, a - n + Vector3.Up * lift, b - n + Vector3.Up * lift, b + n + Vector3.Up * lift );
		kit.Face( b + n + Vector3.Up * lift, b - n + Vector3.Up * lift, a - n + Vector3.Up * lift, a + n + Vector3.Up * lift );
	}
}