Code/EmittersShapes/ConchplexCylinderEmitter.cs
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static Sandbox.VertexLayout;

namespace ConchplexEmitters;



/// <summary>
/// Emits particles within a cylinder / disk
/// </summary>
[Title( "Conchplex Cylinder Emitter" ), Category( "Particles" ), Icon( "donut_large" )]
public sealed class ConchplexCylinderEmitter : ConchplexEmitter
{

	/// <summary>
	/// 1 = spawns on edge, 0 = spawns in centre
	/// </summary>
	[Property, Group("Spawn Bias"), Order(0)]
	[Range(0f, 1f), Step(0.01f)]
	public float DistanceBias { get; set; } = 0.5f;

	/// <summary>
	/// Direction + intensity to bias particles to spawn towards
	/// </summary>
	[Property, Group("Spawn Bias"), Order(0)]
	[Range(-1, 1)]
	public Vector3 PositionBias { get; set; } = 0f;

	/// <summary>
	/// Particles only spawn on the axes marked
	/// </summary>
	[Property, Group("Spawn Bias"), Order(0)]
	public AxisClampFlags AxisClamp { get; set; }
	/// <summary>
	/// Which axis to clamp to / in between
	/// </summary>
	public enum EmitFaces
	{
		Both,
		Sides,
	}

	public enum ThicknessMode
	{
		Hollow,
		hole
	}

	[Property]
	public float OutterRadius { get; set; } = 25f;

	[Property]
	[Range(0, 1)]
	public float Thickness { get; set; } = 0;

	[Property]
	public float height { get; set; } = 50f;

	[Property]
	public ThicknessMode ThickMode { get; set; }


	[Property, Group( "Initial Velocity" )]
	public float VelocityFromCylinder = 0f;

	private float ThickRadius => MathX.Lerp(0, 1, Thickness);
	private float MidRadius => Thickness > 0 ? (ThickRadius + 1) * 0.5f : 0f;
	private float MidHeight => (1 + Thickness) * 0.5f;

	protected override void DrawGizmos()
	{
		// TODO - THIS IS FUCKED 

		if (!Gizmo.IsSelected) return;
		
		Gizmo.Draw.Color = Color.White.WithAlpha(0.5f);
		Gizmo.GizmoDraw draw = Gizmo.Draw;

		// draw outter ring
		var top = Vector3.Zero.WithZ(height * 0.5f);
		var bottom = top.WithZ(-top.z);
		draw.LineCylinder(bottom, top, OutterRadius, OutterRadius, 12);

		var sThick = ThickRadius * OutterRadius;
		var sMid = MidRadius * OutterRadius;
		var sThickHeight = MidHeight * height; 

	
		// draw thickness
		if( Thickness != 0 )
		{
			var thickTop = ThickMode == ThicknessMode.Hollow ? top * Thickness : top;
			draw.LineCylinder(thickTop.WithZ(-thickTop.z), thickTop, sThick, sThick, 12);
		}


		// draw distance bias
		if( DistanceBias != 0 )
		{
			draw.Color = Color.Red.WithAlpha(0.5f);
			var biasHeight = MathX.Lerp(sThickHeight * 0.5f, top.z, DistanceBias);
			var thickBiasHeight = ThickMode == ThicknessMode.Hollow ? top.z * Thickness : top.z;
			var fucksake = MathX.Lerp(sThickHeight * 0.5f, thickBiasHeight, DistanceBias);
			var biasOutterRadius = MathX.Lerp(sMid, OutterRadius, DistanceBias);
			var biasInnerRadius = MathX.Lerp(sMid, sThick, DistanceBias);

			if(ThickMode == ThicknessMode.Hollow)
			{
				var biasHollowRadius = MathX.Lerp(sThick, OutterRadius, DistanceBias);
				var biasHollowHeight = MathX.Lerp(top.z * Thickness, top.z, DistanceBias);
				draw.LineCylinder(bottom.WithZ(-biasHollowHeight), top.WithZ(biasHollowHeight), biasHollowRadius, biasHollowRadius, 12);
			}
			else
			{
				draw.LineCylinder(bottom.WithZ(-biasHeight), top.WithZ(biasHeight), biasOutterRadius, biasOutterRadius, 12);
				draw.LineCylinder(bottom.WithZ(-fucksake), top.WithZ(fucksake), biasInnerRadius, biasInnerRadius, 12);
			}
				
		}

		// draw Position Bias
		if (PositionBias.IsNearlyZero()) return;
		Gizmo.Draw.Color = Color.Red.WithAlpha(0.8f);
		Vector3 biasClamped = Vector3.Clamp(PositionBias, new Vector3(-1f), new Vector3(1f)) * (OutterRadius) * 1.1f;
		draw.Arrow(Vector3.Zero, biasClamped, biasClamped.Length * 0.1f, biasClamped.Length * 0.1f);
		

	}

	public override Particle Emit(ParticleEffect target )
	{

		// Random point in a unity cylinder
		Vector3 random = Random.Shared.VectorInCircle(1f);
		random.z = Random.Shared.Float(-0.5f, 0.5f);

		// POSITION BIAS
		var positionBias = PositionBias.Clamp(-1, 1);
		random = random + positionBias;
		// Correct bounds
		var randomXY = new Vector2(random.x, random.y);
		Vector2 randomXYNormal = randomXY.Normal;
		random.x = random.x.Clamp(0f, randomXYNormal.x);
		random.y = random.y.Clamp(0f, randomXYNormal.y);
		random.z = random.z.Clamp(0.5f,-0.5f);
		randomXY = new Vector2(random.x, random.y);

		// get probabilty if a particle should be shoved to sides or top
		var outterTopBottom = 2 * (OutterRadius* OutterRadius);
		var innerTopBottom = 2 * ((OutterRadius * Thickness) * (OutterRadius * Thickness));
		outterTopBottom = positionBias.z != 0f ? outterTopBottom * 0.5f : outterTopBottom;
		innerTopBottom = positionBias.z != 0f ? innerTopBottom * 0.5f : innerTopBottom;

		var outterSides = (height * OutterRadius);
		var innerSides = (height * (Thickness * OutterRadius));

		var totalArea = outterTopBottom + outterSides + innerTopBottom + innerSides;
		var totalTopBottomArea = outterTopBottom + innerTopBottom;
		var sidesProbability = (outterSides + innerSides) / totalArea;
		var innerChance = (innerTopBottom + innerSides) / totalArea;



		// TODO
		/*
			- get all points pushed to cylinder outter edges
			- get all points pushed to cylinder inner thickness edges
			- get all points pushed into the "middle"

			- new Random
				- lerp between Outter edge / Inner edge, based on old random.length
			- Max pos
				- if pointsXY & or pointsZ are greater than the mid bounds
					TRUE - use outter edge
					FALSE - use innter edge
			- Min pos
				- use middle points
		*/

		// Prepare min/max bounds for hollow/hole modes
		Vector3 randomHollowMin = random, randomHollowMax = random;
		Vector2 minHollowRandomXY = randomXY, maxHollowRandomXY = randomXY;
		Vector3 randomHoleMax = random;
		Vector2 maxHoleRandomXY = randomXY;

		float randVal = Random.Shared.Float();

		// Only calculate min/max for the current mode
		if (ThickMode == ThicknessMode.Hollow)
		{
			if (randVal > sidesProbability)
			{
				// Top/bottom face
				bool isTop = random.z >= 0;
				if (positionBias.z > 0f) isTop = true;
				else if (positionBias.z < 0f) isTop = false;
				float zPos = isTop ? 0.5f : -0.5f;
				randomHollowMin *= Thickness;
				randomHollowMin.z = zPos * Thickness;
				randomHollowMax.z = zPos;
			}
			else
			{
				// Sides
				minHollowRandomXY = minHollowRandomXY.Normal * Thickness;
				randomHollowMin.x = minHollowRandomXY.x;
				randomHollowMin.y = minHollowRandomXY.y;
				randomHollowMin.z *= Thickness;
				maxHollowRandomXY = maxHollowRandomXY.Normal;
				randomHollowMax.x = maxHollowRandomXY.x;
				randomHollowMax.y = maxHollowRandomXY.y;
			}
		}
		else // Hole mode
		{
			if (randVal > sidesProbability)
			{
				bool isTop = random.z >= 0;
				if (positionBias.z > 0f) isTop = true;
				else if (positionBias.z < 0f) isTop = false;
				float zPos = isTop ? 0.5f : -0.5f;
				randomHoleMax.z = zPos;
				// Remap for hole
				randomHoleMax.x = MathX.Remap(randomHoleMax.x, 0f, maxHoleRandomXY.Normal.x, maxHoleRandomXY.Normal.x * Thickness, maxHoleRandomXY.Normal.x);
				randomHoleMax.y = MathX.Remap(randomHoleMax.y, 0f, maxHoleRandomXY.Normal.y, maxHoleRandomXY.Normal.y * Thickness, maxHoleRandomXY.Normal.y);
			}
			else
			{
				float innerSideChance = innerSides / totalArea;
				maxHoleRandomXY = Random.Shared.Float() < innerSideChance ? maxHoleRandomXY.Normal * Thickness : maxHoleRandomXY.Normal;
				randomHoleMax.x = maxHoleRandomXY.x;
				randomHoleMax.y = maxHoleRandomXY.y;
			}
		}

		// Interpolate between min/max for hollow/hole
		if (ThickMode == ThicknessMode.Hollow)
		{
			float bias = 1 - MathF.Pow(randVal, (Thickness + 1) * 10);
			random = Vector3.Lerp(randomHollowMin, randomHollowMax, bias);
		}
		else
		{
			random.x = MathX.Remap(random.x, 0, randomXYNormal.x, randomXYNormal.x * Thickness, randomXYNormal.x);
			random.y = MathX.Remap(random.y, 0, randomXYNormal.y, randomXYNormal.y * Thickness, randomXYNormal.y);
		}

		// Midpoint for distance bias
		Vector3 randomMid = random;
		randomMid.z *= MidHeight;
		randomXY = randomXYNormal * MidRadius;
		randomMid.x = randomXY.x;
		randomMid.y = randomXY.y;

		// Distance bias
		if (DistanceBias <= 0.5f)
		{
			float scaledBias = DistanceBias / 0.5f;
			var lowBias = ThickMode == ThicknessMode.Hollow ? randomHollowMin : randomMid;
			random = Vector3.Lerp(lowBias, random, scaledBias);
		}
		else
		{
			var maxRandom = ThickMode == ThicknessMode.Hollow ? randomHollowMax : randomHoleMax;
			float scaledBias = (DistanceBias - 0.5f) / 0.5f;
			random = Vector3.Lerp(random, maxRandom, scaledBias);
		}

		// Scale to world
		random.x *= OutterRadius;
		random.y *= OutterRadius;
		random.z *= height;

		var cylinderVelocity = random.WithZ(0f).Normal;

		var clampedParticlePoint = ProcessClampPosition(random, cylinderVelocity, AxisClamp);
		Vector3 pos = (clampedParticlePoint.Position * LocalScale * LocalRotation) + WorldPosition;
		
		Particle particle = target.Emit(pos, base.Delta);
		particle.Velocity += (clampedParticlePoint.Velocity * LocalRotation) * VelocityFromCylinder;

		return particle;
	}
}