1933 results

namespace LobbySystem;

/// <summary>
/// Auto-hosts a lobby so Steam friends can join, and keeps one networked pawn per connection plus optional
/// bots by cloning <see cref="PlayerPrefab"/>. The pawn only has to implement <see cref="ILobbyAgent"/>.
/// Spawning is de-duped and runs in OnUpdate so a join can't fire mid-enumeration.
/// </summary>
public sealed class LobbyNetworkManager : Component, Component.INetworkListener
{
	[Property] public GameObject PlayerPrefab { get; set; }
	[Property] public int BotCount { get; set; } = 1;

	/// <summary>When true, bots only exist during an active round.</summary>
	[Property] public bool BotsOnlyDuringRound { get; set; } = true;

	[Property] public Color BotTint { get; set; } = new Color( 1f, 0.35f, 0.3f );

	// Lobby spawn ring, used before a round map loads.
	readonly Vector3[] _spawns =
	{
		new Vector3( 0f, -300f, 40f ), new Vector3( 300f, 0f, 40f ),
		new Vector3( 0f, 300f, 40f ),  new Vector3( -300f, 0f, 40f ),
		new Vector3( 250f, 250f, 40f ), new Vector3( -250f, -250f, 40f ),
	};
	int _spawnIndex;
	readonly Dictionary<Guid, GameObject> _pawns = new();
	readonly List<GameObject> _bots = new();

	bool _reconcileNow;
	TimeUntil _nextReconcile;
	TimeUntil _nextSweep;

	protected override async Task OnLoad()
	{
		// When joining a friend the engine is mid-connect and IsActive is briefly false, so poll for a
		// moment before hosting. Otherwise a joiner would spin up its own solo lobby.
		if ( Networking.IsActive ) return;
		for ( int i = 0; i < 6 && !Networking.IsActive; i++ )
			await Task.DelayRealtimeSeconds( 0.1f );
		if ( !Networking.IsActive )
			Networking.CreateLobby( new() );
	}

	void INetworkListener.OnActive( Connection channel ) => _reconcileNow = true;

	protected override void OnUpdate()
	{
		if ( !Networking.IsHost || PlayerPrefab is null ) return;

		if ( !_reconcileNow && _nextReconcile > 0f ) return;
		_reconcileNow = false;
		_nextReconcile = 0.25f;

		try
		{
			bool wantBots = !BotsOnlyDuringRound || (LobbyDirector.Current?.State == LobbyState.Active);
			ReconcileBots( wantBots ? Math.Max( 0, BotCount ) : 0 );

			foreach ( var conn in Connection.All.ToList() )
			{
				if ( conn is null || !conn.IsActive ) continue;
				if ( _pawns.TryGetValue( conn.Id, out var pawn ) && pawn.IsValid() ) continue;
				var id = conn.Id;
				_pawns[id] = FindConnectionPawn( id ) ?? SpawnPawn( false, conn.DisplayName, conn );
			}

			Sweep();
		}
		catch
		{
			// Connection or scene list changed during the pass; retry next frame.
		}
	}

	void ReconcileBots( int target )
	{
		_bots.RemoveAll( b => !b.IsValid() );
		while ( _bots.Count > target )
		{
			var b = _bots[_bots.Count - 1];
			_bots.RemoveAt( _bots.Count - 1 );
			if ( b.IsValid() ) b.Destroy();
		}
		while ( _bots.Count < target )
			_bots.Add( SpawnPawn( true, "Bot", null ) );
	}

	GameObject FindConnectionPawn( Guid id )
	{
		foreach ( var a in Scene.GetAllComponents<ILobbyAgent>() )
		{
			if ( !a.IsValid() || a.IsBot ) continue;
			if ( a is Component c && c.Network.OwnerId == id ) return c.GameObject;
		}
		return null;
	}

	void Sweep()
	{
		if ( _nextSweep > 0f ) return;
		_nextSweep = 1f;
		foreach ( var key in _pawns.Where( kv => !kv.Value.IsValid() ).Select( kv => kv.Key ).ToList() )
			_pawns.Remove( key );
	}

	GameObject SpawnPawn( bool isBot, string displayName, Connection owner )
	{
		var go = PlayerPrefab.Clone( NextSpawn() );
		go.Name = isBot ? "Bot" : $"Player - {displayName}";
		go.Enabled = true;

		var agent = go.Components.Get<ILobbyAgent>() ?? go.Components.GetInChildren<ILobbyAgent>();
		agent?.InitAgent( isBot, displayName );

		if ( isBot )
		{
			var rend = go.Components.GetInChildren<SkinnedModelRenderer>();
			if ( rend is not null ) rend.Tint = BotTint;
		}

		if ( owner is not null ) go.NetworkSpawn( owner );
		else go.NetworkSpawn();
		return go;
	}

	Vector3 NextSpawn()
	{
		int idx = _spawnIndex++;
		var dir = LobbyDirector.Current;
		if ( dir is not null && dir.UseRoundMap && dir.MapReady )
			return dir.RoundSpawnPoint( idx );
		return _spawns[idx % _spawns.Length];
	}
}
namespace LobbySystem;

/// <summary>Lifecycle of the lobby: Lobby, then Active, then Ended before looping back.</summary>
public enum LobbyState
{
	/// <summary>Free roam before and after a round. The mode button works here.</summary>
	Lobby,
	/// <summary>A round is running.</summary>
	Active,
	/// <summary>Round finished; results show before returning to the lobby.</summary>
	Ended
}
namespace LobbySystem;

/// <summary>
/// In-world button that opens the mode menu for the host, or a local suggestion menu for a client. It needs
/// a ModelRenderer to be visible and is hidden while a round is live. When the local player is within
/// <see cref="UseRange"/> and presses Use, the menu opens.
/// </summary>
public sealed class LobbyModeButton : Component
{
	[Property] public float UseRange { get; set; } = 130f;
	[Property] public bool GlowWhenInRange { get; set; } = true;
	[Property] public Color IdleTint { get; set; } = new Color( 0.85f, 0.4f, 0.15f );
	[Property] public Color ActiveTint { get; set; } = new Color( 1f, 0.85f, 0.3f );

	ModelRenderer _renderer;
	ILobbyAgent _me;

	protected override void OnStart()
	{
		_renderer = Components.Get<ModelRenderer>() ?? Components.GetInChildren<ModelRenderer>();
		if ( _renderer is not null ) _renderer.Tint = IdleTint;
	}

	protected override void OnUpdate()
	{
		var dir = LobbyDirector.Current;
		bool inLobby = dir is null || !dir.RoundLive;
		if ( _renderer is not null && _renderer.Enabled != inLobby )
			_renderer.Enabled = inLobby;
		if ( !inLobby ) return;

		var me = LocalPlayer();
		bool inRange = me is not null && WorldPosition.Distance( me.WorldPosition ) <= UseRange;

		if ( GlowWhenInRange && _renderer is not null )
			_renderer.Tint = inRange ? ActiveTint : IdleTint;

		if ( inRange && Input.Pressed( "Use" ) )
			dir?.RequestModeMenu();
	}

	ILobbyAgent LocalPlayer()
	{
		if ( _me is not null && _me.IsValid() && !_me.IsBot && !_me.IsProxy ) return _me;
		try { _me = Scene.GetAllComponents<ILobbyAgent>().FirstOrDefault( c => c.IsValid() && !c.IsBot && !c.IsProxy ); }
		catch { _me = null; }
		return _me;
	}
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "MC Clouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "trend" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "trend.mcclouds" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-16T17:04:05.4666731Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.124.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.124.0")]
using Sandbox;
using Sandbox.UI;
using System;

namespace SbTween;

public static class LightExtensions
{

	public static BaseTween TweenLightColor( this Light Light, Color target, float duration )
	{
		Color start = Light.LightColor;
		var tween = new BaseTween( duration );
		tween.Target = Light.GameObject;
		return TweenManager.Instance.AddTween( tween
			.OnStart( () => start = Light.LightColor )
			.OnUpdate( p => Light.LightColor = Color.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenRadius( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Radius;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Radius )
			.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenConeOuter( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.ConeOuter;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.ConeOuter )
			.OnUpdate( p => light.ConeOuter = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenInnerCone( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.ConeInner;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.ConeInner )
			.OnUpdate( p => light.ConeInner = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenAttenuation( this PointLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Attenuation;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Attenuation )
			.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenRadius( this PointLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Radius;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Radius )
			.OnUpdate( p => light.Radius = MathX.Lerp( start, target, p ) ) );
	}

	public static BaseTween TweenAttenuation( this SpotLight light, float target, float duration )
	{
		if ( !light.IsValid() ) return null;

		float start = light.Attenuation;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnStart( () => start = light.Attenuation )
			.OnUpdate( p => light.Attenuation = MathX.Lerp( start, target, p ) ) );
	}

	//FLICKERING LIGHT
	public static BaseTween TweenFlickerLight( this PointLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
			} ) );
	}

	public static BaseTween TweenFlickerLight( this SpotLight light, float minBrightness, float maxBrightness, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.Attenuation = MathX.Lerp( minBrightness, maxBrightness, t );
			} ) );
	}

	//FLICKERING Color

	public static BaseTween TweenFlickerColor( this PointLight light, Color colorA, Color colorB, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.LightColor = Color.Lerp( colorA, colorB, t );
			} ) );
	}

	public static BaseTween TweenFlickerColor( this SpotLight light, Color colorA, Color colorB, float duration, float speed = 10f )
	{
		if ( !light.IsValid() ) return null;

		float time = 0f;
		return TweenManager.Instance.AddTween( new BaseTween( duration )
			.OnUpdate( _ =>
			{
				time += Time.Delta * speed;
				float noise = MathF.Sin( time * 1.3f ) * MathF.Sin( time * 2.7f ) * MathF.Sin( time * 0.9f );
				float t = (noise + 1f) * 0.5f;
				light.LightColor = Color.Lerp( colorA, colorB, t );
			} ) );
	}

}
using Sandbox;
using System;

namespace SbTween;

public static class AudioExtensions
{
	public static BaseTween TweenVolume( this SoundPointComponent sound, float targetVolume, float duration )
	{
		float startVolume = sound.Volume;
		var tween = new BaseTween( duration );
		tween.Target = sound.GameObject;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startVolume = sound.Volume )
			.OnUpdate( p =>
			{
				if ( !sound.IsValid() ) return;
				sound.Volume = MathX.Lerp( startVolume, targetVolume, p );
			} )
		);
	}
	
	public static BaseTween TweenPitch( this SoundPointComponent sound, float targetPitch, float duration )
	{
		float startPitch = sound.Pitch;
		var tween = new BaseTween( duration );
		tween.Target = sound.GameObject;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startPitch = sound.Pitch )
			.OnUpdate( p =>
			{
				if ( !sound.IsValid() ) return;
				sound.Pitch = MathX.Lerp( startPitch, targetPitch, p );
			} )
		);
	}
}
using Sandbox;
using System;

namespace SbTween;

public static class MathTweenExtensions
{
	public static BaseTween TweenInCircle( this GameObject obj, float duration, Vector3 axis, float range, float speed, bool snapping = false )
	{
		Vector3 centerPos = obj.WorldPosition;
		var tween = new BaseTween( duration );
		tween.Target = obj;

		Vector3 normal = axis.Normal;
		Vector3 v1 = Vector3.Cross( normal, MathF.Abs( normal.z ) < 0.9f ? Vector3.Up : Vector3.Forward ).Normal;
		Vector3 v2 = Vector3.Cross( normal, v1 ).Normal;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;

				float angleDegrees = p * 360f * speed;
				float angleRadians = angleDegrees * (MathF.PI / 180f);

				float cos = MathF.Cos( angleRadians ) * range;
				float sin = MathF.Sin( angleRadians ) * range;

				Vector3 rotatedOffset = (v1 * cos) + (v2 * sin);
				obj.WorldPosition = centerPos + rotatedOffset;
			} )
		);
	}

	public static BaseTween TweenSpiral( this GameObject obj, float duration, Vector3 axis, float speed, float frequency )
	{
		Vector3 startPos = obj.WorldPosition;
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnStart( () => startPos = obj.WorldPosition )
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;

				float angle = p * MathF.PI * 2f * frequency;

				float currentRadius = p * speed;

				float x = MathF.Cos( angle ) * currentRadius;
				float y = MathF.Sin( angle ) * currentRadius;

				Vector3 axisOffset = axis * p;

				Vector3 circleOffset = new Vector3( x, y, 0 );

				obj.WorldPosition = startPos + axisOffset + circleOffset;
			} )
		);
	}
	
	public static BaseTween TweenPunchFloat( this GameObject obj, float v, float amplitude, float duration, int vibrations = 5, float elasticity = 1f, Action<float> setter = null )
	{
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;
				if ( p >= 1.0f )
				{
					setter?.Invoke( v );
					return;
				}
				
				float decay = MathF.Pow( 1f - p, elasticity * 3f );

				float omega = vibrations * MathF.PI * 2f;
				float oscillation = MathF.Sin( p * omega );

				float currentOffset = amplitude * oscillation * decay;

				setter?.Invoke( v + currentOffset );
			} )
			.OnComplete( () => setter?.Invoke( v ) )
		);
	}
	
	public static BaseTween TweenShakeFloat( this GameObject obj, float baseline, float strength, float duration, Action<float> setter = null )
	{
		var tween = new BaseTween( duration );
		tween.Target = obj;

		return TweenManager.Instance.AddTween( tween
			.OnUpdate( p =>
			{
				if ( !obj.IsValid() ) return;
				if ( p >= 1.0f )
				{
					setter?.Invoke( baseline );
					return;
				}

				float currentStrength = strength * (1.0f - p);
             
				float randomOffset = Game.Random.Float( -currentStrength, currentStrength );

				setter?.Invoke( baseline + randomOffset );
			} )
			.OnComplete( () => setter?.Invoke( baseline ) )
		);
	}
}
namespace Sandbox.UiPro;

public enum HorizontalAlignment
{
	Left,
	Center,
	Right
}

public enum VerticalAlignment
{
	Top,
	Center,
	Bottom
}

[Title( "Text Node - UI Pro" ), Category( "UI Pro" ), Icon( "text_fields" )]
public class TextNode : NodeComponent
{
	[Property, InlineEditor, Group( "Layout Settings" ), Order( -999 )] public override NodeStyle Style { get; set; } = GetDefaultStyle();

	[Property, Group("Text Settings")] public HorizontalAlignment HorizontalAlignment { get; set; } = HorizontalAlignment.Center;
	[Property, Group( "Text Settings" )] public VerticalAlignment VerticalAlignment { get; set; } = VerticalAlignment.Center;
	[Property, InlineEditor, Group( "Text Settings" )] public TextRendering.Scope TextScope { get; set; } = TextRendering.Scope.Default;

	private static NodeStyle GetDefaultStyle()
	{
		return new NodeStyle()
		{
			Anchor = NodePoint.CenterMiddle,
			Pivot = NodePoint.CenterMiddle,
			Offset = Vector2.Zero,
			Size = new Vector2( 100, 100 ),
			ChildPadding = Vector2.Zero,
			ClipChildren = false,
			StretchHorizontal = false,
			StretchVertical = false,
			CornerRadius = 0,
			BorderWidth = 0,
			BorderColor = Color.Black,
			Texture = Texture.White,
			Tint = Color.White,
			UvScale = Vector2.One,
			UvOffset = Vector2.Zero
		};
	}

	protected override void UpdateStyle(float scaleFactor)
	{
		Style.BorderWidth = 0; // for debugging
		Style.BorderColor = Color.White;

		TextRendering.Scope scope = TextScope;
		scope.FontSize *= scaleFactor;

		Texture texture = TextRendering.GetOrCreateTexture( scope );
		Style.Texture = texture;

		Vector2 scale = (Layout.Outer.Size * scaleFactor) / texture.Size;
		Style.UvScale = scale;

		float alignX = HorizontalAlignment switch
		{
			HorizontalAlignment.Left => 0f,
			HorizontalAlignment.Center => 0.5f,
			HorizontalAlignment.Right => 1f,
			_ => 0.5f,
		};

		float alignY = VerticalAlignment switch
		{
			VerticalAlignment.Top => 0f,
			VerticalAlignment.Center => 0.5f,
			VerticalAlignment.Bottom => 1f,
			_ => 0.5f,
		};

		float offsetX = alignX * (1f / scale.x - 1f);
		float offsetY = alignY * (1f / scale.y - 1f);

		Style.UvOffset = new Vector2( offsetX, offsetY );
	}
}
using Sandbox.UiPro;

namespace Sandbox;

// Hooks up the Button's OnClick event and responds
// by updating the TextNode
public class ExampleButtonController : Component
{
	[Property] public Button MyButton { get; set; }
	[Property] public TextNode MyText { get; set; }
	[Property, ReadOnly] public int ClickCount { get; set; } = 0;

	protected override void OnStart()
	{
		if ( !MyButton.IsValid() ) return;

		MyButton.OnClick = OnButtonClicked;
	}

	private void OnButtonClicked()
	{
		ClickCount++;

		if ( !MyText.IsValid() ) return;

		TextRendering.Scope scope = MyText.TextScope;
		scope.Text = $"Clicked {ClickCount} Times";
		MyText.TextScope = scope;
	}
}
using System;

namespace Sandbox.UiPro;

public enum NodePoint
{
	TopLeft, TopMiddle, TopRight,
	CenterLeft, CenterMiddle, CenterRight,
	BottomLeft, BottomMiddle, BottomRight,
}

public class NodeStyle
{
	[Property] public NodePoint Anchor { get; set; }
	[Property] public NodePoint Pivot { get; set; }
	[Property] public Vector2 Offset { get; set; }
	[Property] public Vector2 Size { get; set; }
	[Property] public Vector4 ChildPadding { get; set; }
	[Property] public bool ClipChildren { get; set; }
	[Property] public bool StretchHorizontal { get; set; }
	[Property] public bool StretchVertical { get; set; }
	
	[Property, Hide] public float CornerRadius { get; set; }
	[Property, Hide] public float BorderWidth { get; set; }
	[Property, Hide] public Color BorderColor { get; set; }
	[Property, Hide] public Texture Texture { get; set; }
	[Property, Hide] public Color Tint { get; set; }
	[Property, Hide] public Vector2 UvScale { get; set; }
	[Property, Hide] public Vector2 UvOffset { get; set; }
}

public class NodeLayout
{
	public Rect Outer { get; private set; }
	public Rect Inner { get; private set; }
	public Rect ClipRect { get; private set; }
	public float ClipRadius { get; private set; }
	public Rect ChildClipRect { get; private set; }
	public float ChildClipRadius { get; private set; }

	public static NodeLayout GetRootLayout( Vector2 size )
	{
		Rect rootRect = new Rect( Vector2.Zero, size );

		NodeLayout layout = new NodeLayout()
		{
			Outer = rootRect,
			Inner = rootRect,
			ClipRect = rootRect,
			ClipRadius = 0,
			ChildClipRect = rootRect,
			ChildClipRadius = 0
		};

		return layout;
	}

	public void Compute( NodeLayout parentLayout, NodeStyle style )
	{
		Vector2 anchor = AnchorFraction( style.Anchor );
		Vector2 pivot = AnchorFraction( style.Pivot );

		float width = style.StretchHorizontal ? parentLayout.Inner.Size.x : style.Size.x;
		float height = style.StretchVertical ? parentLayout.Inner.Size.y : style.Size.y;

		float x = style.StretchHorizontal
			? parentLayout.Inner.Position.x + style.Offset.x
			: parentLayout.Inner.Position.x + anchor.x * parentLayout.Inner.Size.x - pivot.x * width + style.Offset.x;

		float y = style.StretchVertical
			? parentLayout.Inner.Position.y + style.Offset.y
			: parentLayout.Inner.Position.y + anchor.y * parentLayout.Inner.Size.y - pivot.y * height + style.Offset.y;

		Outer = new Rect( new Vector2( x, y ), new Vector2( width, height ) );

		float innerW = Math.Max( 0f, width - style.ChildPadding.x - style.ChildPadding.z );
		float innerH = Math.Max( 0f, height - style.ChildPadding.y - style.ChildPadding.w );
		Inner = new Rect( new Vector2( x + style.ChildPadding.x, y + style.ChildPadding.y ), new Vector2( innerW, innerH ) );

		ClipRect = parentLayout.ChildClipRect;
		ClipRadius = parentLayout.ChildClipRadius;

		if ( style.ClipChildren )
		{
			float bw = Math.Max( 0f, style.BorderWidth );
			float clipW = Math.Max( 0f, Outer.Size.x - bw * 2f );
			float clipH = Math.Max( 0f, Outer.Size.y - bw * 2f );

			ChildClipRect = new Rect( new Vector2( Outer.Position.x + bw, Outer.Position.y + bw ), new Vector2( clipW, clipH ) );
			ChildClipRadius = Math.Max( 0f, style.CornerRadius - bw );
		}
		else
		{
			ChildClipRect = parentLayout.ChildClipRect;
			ChildClipRadius = parentLayout.ChildClipRadius;
		}
	}

	private static Vector2 AnchorFraction( NodePoint point )
	{
		float fx = point switch
		{
			NodePoint.TopLeft or NodePoint.CenterLeft or NodePoint.BottomLeft => 0f,
			NodePoint.TopMiddle or NodePoint.CenterMiddle or NodePoint.BottomMiddle => 0.5f,
			_ => 1f,
		};
		float fy = point switch
		{
			NodePoint.TopLeft or NodePoint.TopMiddle or NodePoint.TopRight => 0f,
			NodePoint.CenterLeft or NodePoint.CenterMiddle or NodePoint.CenterRight => 0.5f,
			_ => 1f,
		};
		return new Vector2( fx, fy );
	}
}
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Dreams.UltimateLightManager;

[Library( "UltimateLightManager" )]
[Title( "Ultimate Light Manager" )]
[Description( "Advanced light component with presets, runtime controls, grouping, and an integrated S&box editor workflow." )]
[Category( "Light" )]
[Icon( "tungsten" )]
public class UltimateLightManager : Component, Component.ExecuteInEditor
{
    public enum LightTypeEnum
    {
        Point,
        Spot
    }

    public enum LightPreset
    {
        Custom,
        Candle,
        Torch,
        Neon,
        Alarm,
        BrokenLamp,
        SciFi,
        StreetLight
    }

    public static void SetGroupState( string groupName, bool isEnabled )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.SetEnabledState( isEnabled );
        }
    }

    public static void SetGroupBrightness( string groupName, float brightness )
    {
        brightness = Math.Max( brightness, 0f );

        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.SetBrightnessLevel( brightness );
        }
    }

    public static void SetGroupColor( string groupName, Color color )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.SetLightColorValue( color );
        }
    }

    public static void ApplyPresetToGroup( string groupName, LightPreset preset )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.ApplyPreset( preset );
        }
    }

    public static void TriggerGroupFlash( string groupName, float duration = 0.15f, float brightnessMultiplier = 2f )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.TriggerFlash( duration, brightnessMultiplier );
        }
    }

    public static void TriggerGroupAlarm( string groupName, float duration = 2f )
    {
        foreach ( var light in GetLightsInGroup( groupName ) )
        {
            light.TriggerAlarm( duration );
        }
    }

    public static void SetPowerGridState( string powerGridTag, bool isPowered )
    {
        foreach ( var light in GetLightsInPowerGrid( powerGridTag ) )
        {
            light.SetPowered( isPowered );
        }
    }

    private static IEnumerable<UltimateLightManager> GetLightsInGroup( string groupName )
    {
        return EnumerateAllLights().Where( light => string.Equals( light.LightGroup, groupName, StringComparison.OrdinalIgnoreCase ) );
    }

    private static IEnumerable<UltimateLightManager> GetLightsInPowerGrid( string powerGridTag )
    {
        return EnumerateAllLights().Where( light => string.Equals( light.PowerGridTag, powerGridTag, StringComparison.OrdinalIgnoreCase ) );
    }

    private static IEnumerable<UltimateLightManager> EnumerateAllLights()
    {
        var visitedScenes = new HashSet<Scene>();
        var visitedLights = new HashSet<UltimateLightManager>();

        foreach ( var scene in EnumerateCandidateScenes() )
        {
            if ( scene == null || !visitedScenes.Add( scene ) )
            {
                continue;
            }

            foreach ( var light in scene.GetAllComponents<UltimateLightManager>() )
            {
                if ( light != null && visitedLights.Add( light ) )
                {
                    yield return light;
                }
            }
        }
    }

    private static IEnumerable<Scene> EnumerateCandidateScenes()
    {
        if ( Game.ActiveScene != null )
        {
            yield return Game.ActiveScene;
        }

        foreach ( var scene in Scene.All )
        {
            if ( scene != null )
            {
                yield return scene;
            }
        }
    }

    [Property, Order( -1 ), Group( "Management" )]
    public string LightGroup { get; set; } = "Default";

    [Property, Group( "Management" )]
    public string PowerGridTag { get; set; } = string.Empty;

    [Property, Group( "Management" )]
    public float StartDelay { get; set; } = 0.0f;

    [Property, Group( "Management" )]
    public bool AutoDesync { get; set; } = true;

    [Property, Group( "Management" )]
    public bool ShowDebugGizmos { get; set; } = false;

    [Property, Group( "Management" )]
    public bool ForceNetworkObjectMode { get; set; } = true;

    [Property, Group( "General" ), Sync( SyncFlags.FromHost )]
    public LightTypeEnum TargetLightType { get; set; } = LightTypeEnum.Point;

    [Property, Group( "General" ), Sync( SyncFlags.FromHost )]
    public bool IsEnabled { get; set; } = true;

    [Property, Group( "General" ), Sync( SyncFlags.FromHost )]
    public Color LightColor { get; set; } = Color.White;

    [Property, Group( "General" ), Range( 0, 100 ), Sync( SyncFlags.FromHost | SyncFlags.Interpolate )]
    public float Brightness { get; set; } = 1.0f;

    [Property, Group( "General" ), Range( 0, 10 )]
    public float VolumetricBoost { get; set; } = 1.0f;

    [Property, Group( "General" )]
    public bool CastShadows { get; set; } = true;

    [Property, Group( "Presets" ), Sync( SyncFlags.FromHost )]
    public LightPreset SelectedPreset { get; set; } = LightPreset.Custom;

    [Property, Group( "Presets" ), Sync( SyncFlags.FromHost )]
    public bool AutoApplyPreset { get; set; } = true;

    [Property, Group( "Transitions" )]
    public bool EnableFade { get; set; } = false;

    [Property, Group( "Transitions" )]
    public float FadeInDuration { get; set; } = 0.2f;

    [Property, Group( "Transitions" )]
    public float FadeOutDuration { get; set; } = 0.2f;

    [Property, Group( "Audio" )]
    public SoundEvent AmbientSound { get; set; }

    [Property, Group( "Audio" )]
    public SoundEvent ToggleOnSound { get; set; }

    [Property, Group( "Audio" )]
    public SoundEvent ToggleOffSound { get; set; }

    [Property, Group( "Audio" )]
    public bool ModulateVolumeWithLight { get; set; } = true;

    [Property, Group( "Audio" )]
    public bool ModulatePitchWithLight { get; set; } = false;

    [Property, Group( "Audio" ), Range( 0, 5 )]
    public float BaseVolume { get; set; } = 1.0f;

    [Property, Group( "Audio" ), Range( 0.5f, 2f )]
    public float MinPitch { get; set; } = 0.9f;

    [Property, Group( "Audio" ), Range( 0.5f, 2f )]
    public float MaxPitch { get; set; } = 1.1f;

    [Property, Group( "Optimization" )]
    public float MaxDistance { get; set; } = 2500.0f;

    [Property, Group( "Optimization" )]
    public float ShadowMaxDistance { get; set; } = 800.0f;

    [Property, Group( "Optimization" )]
    public bool EnableCulling { get; set; } = true;

    [Property, Group( "Optimization" )]
    public bool EnableAdaptiveUpdates { get; set; } = false;

    [Property, Group( "Optimization" ), Range( 1, 120 )]
    public float NearUpdateRate { get; set; } = 60.0f;

    [Property, Group( "Optimization" ), Range( 1, 120 )]
    public float FarUpdateRate { get; set; } = 12.0f;

    [Property, Group( "Gameplay" )]
    public float DefaultAlarmDuration { get; set; } = 2.0f;

    [Property, Group( "Gameplay" ), Sync( SyncFlags.FromHost )]
    public Color AlarmColor { get; set; } = new Color( 1.0f, 0.15f, 0.1f );

    [Property, Group( "Gameplay" )]
    public float AlarmStrobeSpeed { get; set; } = 8.0f;

    [Property, Group( "Gameplay" )]
    public float AlarmBrightnessMultiplier { get; set; } = 1.5f;

    [Property, FeatureEnabled( "Fire & Candle" )]
    public bool EnableFire { get; set; } = false;

    [Property, Feature( "Fire & Candle" )]
    public float FireSpeed { get; set; } = 12.0f;

    [Property, Feature( "Fire & Candle" ), Range( 0, 1 )]
    public float FireIntensity { get; set; } = 0.3f;

    [Property, Feature( "Fire & Candle" ), Range( 0, 2 )]
    public float FireChaos { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Horror Mode" )]
    public bool EnableHorror { get; set; } = false;

    [Property, Feature( "Horror Mode" )]
    public float MinFlickerDelay { get; set; } = 0.05f;

    [Property, Feature( "Horror Mode" )]
    public float MaxFlickerDelay { get; set; } = 0.4f;

    [Property, Feature( "Horror Mode" ), Range( 0, 1 )]
    public float DamageSeverity { get; set; } = 0.8f;

    [Property, Feature( "Horror Mode" )]
    public SoundEvent SparkSound { get; set; }

    [Property, FeatureEnabled( "Disco Mode" )]
    public bool EnableDisco { get; set; } = false;

    [Property, Feature( "Disco Mode" )]
    public float DiscoSpeed { get; set; } = 20.0f;

    [Property, Feature( "Disco Mode" ), Range( 0, 1 )]
    public float DiscoSaturation { get; set; } = 1.0f;

    [Property, Feature( "Disco Mode" ), Range( 0, 1 )]
    public float DiscoValue { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Color Transition" )]
    public bool EnableColorTransition { get; set; } = false;

    [Property, Feature( "Color Transition" ), Sync( SyncFlags.FromHost )]
    public Color SecondaryColor { get; set; } = new Color( 0.2f, 0.85f, 1.0f );

    [Property, Feature( "Color Transition" )]
    public float ColorTransitionSpeed { get; set; } = 1.0f;

    [Property, FeatureEnabled( "Proximity Sensor" )]
    public bool EnableSensor { get; set; } = false;

    [Property, Feature( "Proximity Sensor" )]
    public float SensorRange { get; set; } = 300.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
    public float SensorMinBrightness { get; set; } = 0.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 0, 1 )]
    public float SensorMaxBrightness { get; set; } = 1.0f;

    [Property, Feature( "Proximity Sensor" ), Range( 1, 20 )]
    public float SensorSmoothness { get; set; } = 5.0f;

    [Property, Feature( "Proximity Sensor" )]
    public bool InvertSensor { get; set; } = false;

    [Property, FeatureEnabled( "Motion Sway" )]
    public bool EnableSway { get; set; } = false;

    [Property, Feature( "Motion Sway" )]
    public float SwaySpeedPitch { get; set; } = 1.0f;

    [Property, Feature( "Motion Sway" )]
    public float SwayAmountPitch { get; set; } = 5.0f;

    [Property, Feature( "Motion Sway" )]
    public float SwaySpeedRoll { get; set; } = 0.7f;

    [Property, Feature( "Motion Sway" )]
    public float SwayAmountRoll { get; set; } = 3.0f;

    [Property, FeatureEnabled( "Flicker Pattern" )]
    public bool EnablePattern { get; set; } = false;

    [Property, Feature( "Flicker Pattern" )]
    public string Pattern { get; set; } = "mmnmmommommnonmmonqnmmo";

    [Property, Feature( "Flicker Pattern" )]
    public float PatternSpeed { get; set; } = 10.0f;

    [Property, FeatureEnabled( "Pulse" )]
    public bool EnablePulse { get; set; } = false;

    [Property, Feature( "Pulse" )]
    public float PulseSpeed { get; set; } = 1.0f;

    [Property, Feature( "Pulse" ), Range( 0, 1 )]
    public float PulseMin { get; set; } = 0.2f;

    [Property, FeatureEnabled( "Strobe" )]
    public bool EnableStrobe { get; set; } = false;

    [Property, Feature( "Strobe" )]
    public float StrobeSpeed { get; set; } = 10.0f;

    [Property, Feature( "Strobe" ), Range( 0.1f, 0.9f )]
    public float StrobeDutyCycle { get; set; } = 0.5f;

    [Property, FeatureEnabled( "Kelvin" )]
    public bool EnableKelvin { get; set; } = false;

    [Property, Feature( "Kelvin" ), Range( 1000, 12000 )]
    public int KelvinTemperature { get; set; } = 4500;

    [Property, FeatureEnabled( "Power Surge" )]
    public bool EnablePowerSurge { get; set; } = false;

    [Property, Feature( "Power Surge" )]
    public float SurgeMinInterval { get; set; } = 4.0f;

    [Property, Feature( "Power Surge" )]
    public float SurgeMaxInterval { get; set; } = 10.0f;

    [Property, Feature( "Power Surge" )]
    public float SurgeDuration { get; set; } = 0.15f;

    [Property, Feature( "Power Surge" )]
    public float SurgeBrightnessMultiplier { get; set; } = 1.8f;

    public bool Powered => PoweredState;
    public float ExternalBrightnessMultiplier => ExternalBrightnessMultiplierState;
    public bool HasColorOverride => HasExternalColorOverrideState;
    public bool AlarmActive => AlarmEndTimeState > RealTime.Now;

    private PointLight _pointLight;
    private SpotLight _spotLight;
    private Light ActiveLight => TargetLightType == LightTypeEnum.Point ? (Light)_pointLight : _spotLight;

    private int _lastKelvin = -1;
    private Color _cachedKelvinColor = Color.White;
    private Rotation _baseRotation;
    private bool _isInitialized;
    private bool _hasAppliedPreset;
    private LightPreset _lastAppliedPreset = LightPreset.Custom;
    private LightPreset _lastObservedPreset = LightPreset.Custom;
    private bool _lastObservedAutoApplyPreset = true;
    private LightTypeEnum _lastSyncedLightType = LightTypeEnum.Point;
    private float _brokenMultiplier = 1.0f;
    private float _nextFlicker;
    private float _sensorWeightTarget = 1.0f;
    private float _sensorWeightCurrent = 1.0f;
    private float _randomTimeOffset;
    private float _creationTime;
    private float _lastUpdateTimestamp;
    private float _enabledBlend = 1.0f;
    private float _nextAdaptiveUpdateTime;
    private float _nextViewerCameraRefreshTime;
    private float _nextSurgeTime;
    private float _surgeEndTime;
    private bool _hasOutputState;
    private bool _lastOutputEnabled;
    private CameraComponent _cachedViewerCamera;
    private SoundHandle _ambientSoundHandle;

    [Sync( SyncFlags.FromHost )]
    private bool PoweredState { get; set; } = true;

    [Sync( SyncFlags.FromHost | SyncFlags.Interpolate )]
    private float ExternalBrightnessMultiplierState { get; set; } = 1.0f;

    [Sync( SyncFlags.FromHost )]
    private bool HasExternalColorOverrideState { get; set; }

    [Sync( SyncFlags.FromHost )]
    private Color ExternalColorOverrideState { get; set; } = Color.White;

    [Sync( SyncFlags.FromHost )]
    private float FlashEndTimeState { get; set; }

    [Sync( SyncFlags.FromHost )]
    private float FlashBrightnessMultiplierState { get; set; } = 1.0f;

    [Sync( SyncFlags.FromHost )]
    private float AlarmEndTimeState { get; set; }

    protected override void OnAwake()
    {
        EnsureNetworkMode();
    }

    protected override void OnStart()
    {
        EnsureNetworkMode();

        _baseRotation = LocalRotation;
        _creationTime = RealTime.Now;
        _lastUpdateTimestamp = _creationTime;
        _enabledBlend = IsEnabled ? 1.0f : 0.0f;

        if ( AutoDesync )
        {
            _randomTimeOffset = Game.Random.Float( 0f, 100f );
        }

        SyncComponentsIfNeeded( force: true );

        if ( AutoApplyPreset )
        {
            ApplyPresetInternal( SelectedPreset );
        }

        ScheduleNextSurge( _creationTime );

        _lastObservedPreset = SelectedPreset;
        _lastObservedAutoApplyPreset = AutoApplyPreset;
        _isInitialized = true;
    }

    protected override void OnUpdate()
    {
        if ( !_isInitialized )
        {
            return;
        }

        SyncComponentsIfNeeded();
        SyncPresetSelectionIfNeeded();

        var light = ActiveLight;
        if ( light == null )
        {
            return;
        }

        bool isPlaying = Game.IsPlaying;
        float absoluteTime = RealTime.Now;
        float effectTime = (isPlaying ? Time.Now : absoluteTime) + _randomTimeOffset;
        float deltaTime = GetFrameDelta( absoluteTime );

        if ( isPlaying && StartDelay > 0f && (absoluteTime - _creationTime) < StartDelay )
        {
            DisableOutput( light );
            return;
        }

        var viewerCam = GetViewerCamera( absoluteTime );
        float distSq = viewerCam != null ? WorldPosition.DistanceSquared( viewerCam.WorldPosition ) : 0f;

        if ( ShouldSkipAdaptiveUpdate( isPlaying, viewerCam, distSq, absoluteTime ) )
        {
            UpdateAmbientSoundPosition();
            return;
        }

        UpdateSensorWeight( viewerCam, distSq, deltaTime );
        UpdateEnabledBlend( deltaTime );
        UpdateSway( effectTime );

        if ( isPlaying && EnableCulling && viewerCam != null && distSq > MaxDistance * MaxDistance )
        {
            DisableOutput( light );
            return;
        }

        float fx = 1.0f;

        fx *= EvaluatePulseAndStrobe( effectTime );
        fx *= EvaluatePattern( effectTime );
        fx *= EvaluateFire( effectTime );
        fx *= EvaluateHorror( effectTime, isPlaying );
        fx *= EvaluatePowerSurge( absoluteTime );
        fx *= EvaluateAlarm( effectTime, absoluteTime );

        if ( absoluteTime < FlashEndTimeState )
        {
            fx *= FlashBrightnessMultiplierState;
        }

        float finalBrightness = Brightness * fx * _sensorWeightCurrent * _enabledBlend * ExternalBrightnessMultiplierState;
        finalBrightness = Math.Max( finalBrightness, 0f );

        bool shouldBeEnabled = finalBrightness > 0.001f;
        light.Enabled = shouldBeEnabled;

        Color resolvedColor = ResolveLightColor( effectTime, absoluteTime );
        light.LightColor = resolvedColor * finalBrightness;
        light.Shadows = CastShadows && ( !isPlaying || distSq < ShadowMaxDistance * ShadowMaxDistance );
        light.FogStrength = VolumetricBoost;

        UpdateOutputState( shouldBeEnabled, true );
        ManageAudio( shouldBeEnabled, finalBrightness / Math.Max( Brightness, 0.01f ) );
    }

    public void TurnOn()
    {
        SetEnabledState( true );
    }

    [Button]
    public void ApplySelectedPreset()
    {
        ApplyPreset( SelectedPreset );
    }

    public void TurnOff()
    {
        SetEnabledState( false );
    }

    public void Toggle()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        IsEnabled = !IsEnabled;
    }

    public void SetPowered( bool powered )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        PoweredState = powered;
    }

    public void SetExternalBrightness( float multiplier )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        ExternalBrightnessMultiplierState = Math.Max( multiplier, 0f );
    }

    public void ResetExternalBrightness()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        ExternalBrightnessMultiplierState = 1.0f;
    }

    public void SetColorOverride( Color color )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        HasExternalColorOverrideState = true;
        ExternalColorOverrideState = color;
    }

    public void ClearColorOverride()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        HasExternalColorOverrideState = false;
        ExternalColorOverrideState = Color.White;
    }

    public void TriggerFlash( float duration = 0.15f, float brightnessMultiplier = 2f )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        FlashEndTimeState = RealTime.Now + Math.Max( duration, 0.01f );
        FlashBrightnessMultiplierState = Math.Max( brightnessMultiplier, 1.0f );
    }

    public void TriggerAlarm( float duration = -1f )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        if ( duration <= 0f )
        {
            duration = DefaultAlarmDuration;
        }

        AlarmEndTimeState = Math.Max( AlarmEndTimeState, RealTime.Now + duration );
    }

    public void ClearAlarm()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        AlarmEndTimeState = 0f;
    }

    [Button]
    public void PreviewFlash()
    {
        TriggerFlash();
    }

    [Button]
    public void PreviewAlarm()
    {
        TriggerAlarm();
    }

    [Button]
    public void ResetRuntimeOverrides()
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        ClearAlarm();
        ClearColorOverride();
        ResetExternalBrightness();
        SetPowered( true );
    }

    public void ApplyPreset( LightPreset preset )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        ApplyPresetInternal( preset );
    }

    public void SetEnabledState( bool isEnabled )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        IsEnabled = isEnabled;
    }

    public void SetBrightnessLevel( float brightness )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        Brightness = Math.Max( brightness, 0f );
    }

    public void SetLightColorValue( Color color )
    {
        if ( ShouldIgnoreNetworkMutation() )
        {
            return;
        }

        LightColor = color;
    }

    private void ApplyPresetInternal( LightPreset preset )
    {
        SelectedPreset = preset;
        ResetPresetControlledFeatures();

        switch ( preset )
        {
            case LightPreset.Candle:
                Brightness = 0.75f;
                LightColor = new Color( 1.0f, 0.76f, 0.5f );
                SecondaryColor = new Color( 1.0f, 0.66f, 0.35f );
                EnableKelvin = true;
                KelvinTemperature = 1800;
                EnableFire = true;
                FireSpeed = 10.0f;
                FireIntensity = 0.18f;
                FireChaos = 0.6f;
                VolumetricBoost = 0.6f;
                break;

            case LightPreset.Torch:
                Brightness = 1.35f;
                LightColor = new Color( 1.0f, 0.72f, 0.38f );
                SecondaryColor = new Color( 1.0f, 0.45f, 0.2f );
                EnableKelvin = true;
                KelvinTemperature = 2200;
                EnableFire = true;
                FireSpeed = 13.0f;
                FireIntensity = 0.28f;
                FireChaos = 1.0f;
                VolumetricBoost = 1.4f;
                break;

            case LightPreset.Neon:
                Brightness = 1.15f;
                LightColor = new Color( 0.2f, 0.95f, 1.0f );
                SecondaryColor = new Color( 1.0f, 0.2f, 0.85f );
                EnableColorTransition = true;
                ColorTransitionSpeed = 0.65f;
                EnablePulse = true;
                PulseSpeed = 1.2f;
                PulseMin = 0.75f;
                CastShadows = false;
                VolumetricBoost = 0.25f;
                break;

            case LightPreset.Alarm:
                Brightness = 2.0f;
                LightColor = new Color( 1.0f, 0.18f, 0.12f );
                AlarmColor = LightColor;
                EnableStrobe = true;
                StrobeSpeed = 7.0f;
                StrobeDutyCycle = 0.45f;
                CastShadows = false;
                VolumetricBoost = 1.1f;
                break;

            case LightPreset.BrokenLamp:
                Brightness = 1.0f;
                LightColor = new Color( 1.0f, 0.93f, 0.82f );
                EnableHorror = true;
                MinFlickerDelay = 0.04f;
                MaxFlickerDelay = 0.25f;
                DamageSeverity = 0.85f;
                EnableKelvin = true;
                KelvinTemperature = 3400;
                break;

            case LightPreset.SciFi:
                Brightness = 1.6f;
                LightColor = new Color( 0.35f, 0.78f, 1.0f );
                SecondaryColor = new Color( 0.1f, 1.0f, 0.8f );
                EnableColorTransition = true;
                ColorTransitionSpeed = 1.15f;
                EnablePulse = true;
                PulseSpeed = 0.85f;
                PulseMin = 0.55f;
                VolumetricBoost = 2.0f;
                break;

            case LightPreset.StreetLight:
                Brightness = 1.4f;
                LightColor = new Color( 1.0f, 0.84f, 0.68f );
                EnableKelvin = true;
                KelvinTemperature = 3500;
                MaxDistance = 4500.0f;
                ShadowMaxDistance = 1200.0f;
                VolumetricBoost = 0.55f;
                break;

            case LightPreset.Custom:
            default:
                break;
        }

        _lastAppliedPreset = preset;
        _hasAppliedPreset = true;
    }

    private void ResetPresetControlledFeatures()
    {
        EnableFire = false;
        EnableHorror = false;
        EnableDisco = false;
        EnableColorTransition = false;
        EnablePulse = false;
        EnableStrobe = false;
        EnableKelvin = false;
    }

    private void SyncPresetSelectionIfNeeded()
    {
        bool autoApplyChanged = AutoApplyPreset != _lastObservedAutoApplyPreset;
        bool presetChanged = SelectedPreset != _lastObservedPreset;

        _lastObservedAutoApplyPreset = AutoApplyPreset;
        _lastObservedPreset = SelectedPreset;

        if ( !AutoApplyPreset )
        {
            return;
        }

        if ( autoApplyChanged || presetChanged || !_hasAppliedPreset || SelectedPreset != _lastAppliedPreset )
        {
            ApplyPresetInternal( SelectedPreset );
        }
    }

    private void SyncComponentsIfNeeded( bool force = false )
    {
        bool missingActiveLight = TargetLightType == LightTypeEnum.Point ? _pointLight == null : _spotLight == null;
        if ( !force && !missingActiveLight && TargetLightType == _lastSyncedLightType )
        {
            return;
        }

        Component createdComponent = null;

        if ( TargetLightType == LightTypeEnum.Point )
        {
            if ( _pointLight == null )
            {
                _pointLight = Components.GetOrCreate<PointLight>();
                createdComponent = _pointLight;
            }

            if ( _spotLight != null && _spotLight.Enabled )
            {
                _spotLight.Enabled = false;
            }
        }
        else
        {
            if ( _spotLight == null )
            {
                _spotLight = Components.GetOrCreate<SpotLight>();
                createdComponent = _spotLight;
            }

            if ( _pointLight != null && _pointLight.Enabled )
            {
                _pointLight.Enabled = false;
            }
        }

        _lastSyncedLightType = TargetLightType;

        if ( createdComponent != null && Game.IsPlaying && GameObject.NetworkMode == NetworkMode.Object )
        {
            GameObject.Network.Refresh( createdComponent );
        }
    }

    private CameraComponent GetViewerCamera( float absoluteTime )
    {
        var sceneCamera = Scene.Camera;
        if ( sceneCamera != null )
        {
            _cachedViewerCamera = sceneCamera;
            _nextViewerCameraRefreshTime = absoluteTime + 0.25f;
            return sceneCamera;
        }

        if ( _cachedViewerCamera != null && absoluteTime < _nextViewerCameraRefreshTime )
        {
            return _cachedViewerCamera;
        }

        _nextViewerCameraRefreshTime = absoluteTime + 0.25f;
        _cachedViewerCamera = Scene.GetAllComponents<CameraComponent>().FirstOrDefault( camera => camera != null && camera.Enabled );
        return _cachedViewerCamera;
    }

    private void EnsureNetworkMode()
    {
        if ( !ForceNetworkObjectMode || !Game.IsPlaying || GameObject.NetworkMode == NetworkMode.Object )
        {
            return;
        }

        GameObject.NetworkMode = NetworkMode.Object;
    }

    private bool ShouldIgnoreNetworkMutation()
    {
        return Game.IsPlaying && IsProxy;
    }

    private float GetFrameDelta( float absoluteTime )
    {
        float delta = Math.Clamp( absoluteTime - _lastUpdateTimestamp, 0.0001f, 0.25f );
        _lastUpdateTimestamp = absoluteTime;
        return delta;
    }

    private bool ShouldSkipAdaptiveUpdate( bool isPlaying, CameraComponent viewerCam, float distSq, float absoluteTime )
    {
        if ( !EnableAdaptiveUpdates || !isPlaying || viewerCam == null )
        {
            return false;
        }

        if ( absoluteTime < _nextAdaptiveUpdateTime )
        {
            return true;
        }

        float maxDistanceSq = Math.Max( MaxDistance * MaxDistance, 1f );
        float distRatio = Math.Clamp( distSq / maxDistanceSq, 0f, 1f );
        float nearInterval = 1f / Math.Max( NearUpdateRate, 1f );
        float farInterval = 1f / Math.Max( FarUpdateRate, 1f );
        _nextAdaptiveUpdateTime = absoluteTime + MathX.Lerp( nearInterval, farInterval, distRatio );
        return false;
    }

    private void UpdateSensorWeight( CameraComponent viewerCam, float distSq, float deltaTime )
    {
        if ( EnableSensor && viewerCam != null && SensorRange > 0.01f )
        {
            float distRatio = Math.Clamp( 1.0f - (MathF.Sqrt( distSq ) / SensorRange), 0f, 1f );
            float rawWeight = InvertSensor ? 1.0f - distRatio : distRatio;
            _sensorWeightTarget = MathX.Lerp( SensorMinBrightness, SensorMaxBrightness, rawWeight );
        }
        else
        {
            _sensorWeightTarget = 1.0f;
        }

        _sensorWeightCurrent = MathX.Lerp( _sensorWeightCurrent, _sensorWeightTarget, Math.Clamp( deltaTime * SensorSmoothness, 0f, 1f ) );
    }

    private void UpdateEnabledBlend( float deltaTime )
    {
        float target = IsEnabled && PoweredState ? 1.0f : 0.0f;

        if ( !EnableFade )
        {
            _enabledBlend = target;
            return;
        }

        float duration = target > _enabledBlend ? Math.Max( FadeInDuration, 0.0001f ) : Math.Max( FadeOutDuration, 0.0001f );
        float lerp = Math.Clamp( deltaTime / duration, 0f, 1f );
        _enabledBlend = MathX.Lerp( _enabledBlend, target, lerp );

        if ( Math.Abs( _enabledBlend - target ) < 0.001f )
        {
            _enabledBlend = target;
        }
    }

    private void UpdateSway( float effectTime )
    {
        if ( EnableSway )
        {
            float pitch = MathF.Sin( effectTime * SwaySpeedPitch ) * SwayAmountPitch;
            float roll = MathF.Cos( effectTime * SwaySpeedRoll ) * SwayAmountRoll;
            LocalRotation = _baseRotation * Rotation.From( pitch, 0f, roll );
            return;
        }

        LocalRotation = _baseRotation;
    }

    private float EvaluatePulseAndStrobe( float effectTime )
    {
        if ( EnableStrobe )
        {
            float cycle = (effectTime * StrobeSpeed) % 1.0f;
            return cycle < StrobeDutyCycle ? 1.0f : 0.0f;
        }

        if ( EnablePulse )
        {
            float sine = (MathF.Sin( effectTime * PulseSpeed * 2.0f ) + 1.0f) * 0.5f;
            return MathX.Lerp( PulseMin, 1.0f, sine );
        }

        return 1.0f;
    }

    private float EvaluatePattern( float effectTime )
    {
        if ( !EnablePattern || string.IsNullOrWhiteSpace( Pattern ) )
        {
            return 1.0f;
        }

        int index = (int)(effectTime * PatternSpeed) % Pattern.Length;
        float value = Math.Max( 0, (char.ToLower( Pattern[index] ) - 'a') / 12.0f );
        return value;
    }

    private float EvaluateFire( float effectTime )
    {
        if ( !EnableFire )
        {
            return 1.0f;
        }

        float noise = MathF.Sin( effectTime * FireSpeed ) + MathF.Sin( effectTime * FireSpeed * 0.5f );

        if ( FireChaos > 0f )
        {
            noise += MathF.Sin( effectTime * FireSpeed * 1.5f ) * FireChaos;
        }

        return 1.0f - (noise * 0.15f * FireIntensity);
    }

    private float EvaluateHorror( float effectTime, bool isPlaying )
    {
        if ( !EnableHorror )
        {
            return 1.0f;
        }

        if ( effectTime > _nextFlicker )
        {
            bool isDamaged = Game.Random.Float( 0f, 1f ) < DamageSeverity;
            _brokenMultiplier = isDamaged ? Game.Random.Float( 0.0f, 0.4f ) : 1.0f;
            _nextFlicker = effectTime + Game.Random.Float( MinFlickerDelay, MaxFlickerDelay );

            if ( isPlaying && isDamaged && SparkSound != null && _brokenMultiplier < 0.2f )
            {
                Sound.Play( SparkSound, WorldPosition );
            }
        }

        return _brokenMultiplier;
    }

    private float EvaluatePowerSurge( float absoluteTime )
    {
        if ( !EnablePowerSurge )
        {
            return 1.0f;
        }

        if ( _nextSurgeTime <= 0f )
        {
            ScheduleNextSurge( absoluteTime );
        }

        if ( absoluteTime >= _nextSurgeTime )
        {
            _surgeEndTime = absoluteTime + Math.Max( SurgeDuration, 0.01f );
            ScheduleNextSurge( _surgeEndTime );
        }

        return absoluteTime < _surgeEndTime ? Math.Max( SurgeBrightnessMultiplier, 1.0f ) : 1.0f;
    }

    private float EvaluateAlarm( float effectTime, float absoluteTime )
    {
        if ( absoluteTime >= AlarmEndTimeState )
        {
            return 1.0f;
        }

        float cycle = (effectTime * AlarmStrobeSpeed) % 1.0f;
        float gate = cycle < 0.5f ? 1.0f : 0.15f;
        return gate * Math.Max( AlarmBrightnessMultiplier, 0f );
    }

    private void ScheduleNextSurge( float absoluteTime )
    {
        float minInterval = Math.Min( SurgeMinInterval, SurgeMaxInterval );
        float maxInterval = Math.Max( SurgeMinInterval, SurgeMaxInterval );
        _nextSurgeTime = absoluteTime + Game.Random.Float( Math.Max( minInterval, 0.01f ), Math.Max( maxInterval, 0.01f ) );
    }

    private Color ResolveLightColor( float effectTime, float absoluteTime )
    {
        Color color = LightColor;

        if ( EnableKelvin )
        {
            if ( KelvinTemperature != _lastKelvin )
            {
                _cachedKelvinColor = KelvinToColor( KelvinTemperature );
                _lastKelvin = KelvinTemperature;
            }

            color = _cachedKelvinColor;
        }

        if ( EnableColorTransition )
        {
            float lerp = (MathF.Sin( effectTime * ColorTransitionSpeed ) + 1.0f) * 0.5f;
            color = Color.Lerp( color, SecondaryColor, lerp, true );
        }

        if ( EnableDisco )
        {
            color = new ColorHsv( (effectTime * DiscoSpeed) % 360f, DiscoSaturation, DiscoValue ).ToColor();
        }

        if ( absoluteTime < AlarmEndTimeState )
        {
            color = Color.Lerp( color, AlarmColor, 0.85f, true );
        }

        if ( HasExternalColorOverrideState )
        {
            color = ExternalColorOverrideState;
        }

        return color;
    }

    private void UpdateOutputState( bool shouldBeEnabled, bool playOneShot )
    {
        if ( !_hasOutputState )
        {
            _hasOutputState = true;
            _lastOutputEnabled = shouldBeEnabled;
            return;
        }

        if ( _lastOutputEnabled == shouldBeEnabled )
        {
            return;
        }

        if ( playOneShot && Game.IsPlaying )
        {
            if ( shouldBeEnabled && ToggleOnSound != null )
            {
                Sound.Play( ToggleOnSound, WorldPosition );
            }
            else if ( !shouldBeEnabled && ToggleOffSound != null )
            {
                Sound.Play( ToggleOffSound, WorldPosition );
            }
        }

        _lastOutputEnabled = shouldBeEnabled;
    }

    private void DisableOutput( Light light )
    {
        light.Enabled = false;
        UpdateOutputState( false, false );
        ManageAudio( false, 0f );
    }

    private void UpdateAmbientSoundPosition()
    {
        if ( _ambientSoundHandle != null && !_ambientSoundHandle.IsStopped )
        {
            _ambientSoundHandle.Position = WorldPosition;
        }
    }

    private void ManageAudio( bool isLightEnabled, float intensityRatio )
    {
        if ( !Game.IsPlaying || AmbientSound == null )
        {
            return;
        }

        if ( isLightEnabled )
        {
            if ( _ambientSoundHandle == null || _ambientSoundHandle.IsStopped )
            {
                _ambientSoundHandle = Sound.Play( AmbientSound, WorldPosition );
            }

            if ( _ambientSoundHandle != null )
            {
                _ambientSoundHandle.Position = WorldPosition;
                _ambientSoundHandle.Volume = BaseVolume * (ModulateVolumeWithLight ? intensityRatio : 1.0f);
                _ambientSoundHandle.Pitch = ModulatePitchWithLight ? MathX.Lerp( MinPitch, MaxPitch, intensityRatio ) : 1.0f;
            }
        }
        else if ( _ambientSoundHandle != null )
        {
            _ambientSoundHandle.Stop();
            _ambientSoundHandle = null;
        }
    }

    private Color KelvinToColor( int kelvin )
    {
        float temperature = kelvin / 100.0f;
        float red;
        float green;
        float blue;

        if ( temperature <= 66f )
        {
            red = 255f;
            green = Math.Clamp( 99.47f * MathF.Log( temperature ) - 161.11f, 0f, 255f );
        }
        else
        {
            red = Math.Clamp( 329.698f * MathF.Pow( temperature - 60f, -0.133f ), 0f, 255f );
            green = Math.Clamp( 288.12f * MathF.Pow( temperature - 60f, -0.075f ), 0f, 255f );
        }

        if ( temperature >= 66f )
        {
            blue = 255f;
        }
        else if ( temperature <= 19f )
        {
            blue = 0f;
        }
        else
        {
            blue = Math.Clamp( 138.51f * MathF.Log( temperature - 10f ) - 305.04f, 0f, 255f );
        }

        return new Color( red / 255f, green / 255f, blue / 255f );
    }

    protected override void DrawGizmos()
    {
        if ( !ShowDebugGizmos )
        {
            return;
        }

        Gizmo.Draw.Text( $"Group: {LightGroup}", new Transform( Vector3.Up * 20f ), size: 12 );

        if ( !string.IsNullOrWhiteSpace( PowerGridTag ) )
        {
            Gizmo.Draw.Text( $"Grid: {PowerGridTag}", new Transform( Vector3.Up * 34f ), size: 12 );
        }

        if ( EnableSensor )
        {
            Gizmo.Draw.Color = Color.Cyan.WithAlpha( 0.2f );
            Gizmo.Draw.SolidSphere( Vector3.Zero, SensorRange );
            Gizmo.Draw.Color = Color.Cyan;
            Gizmo.Draw.LineSphere( Vector3.Zero, SensorRange );
        }

        if ( EnableCulling )
        {
            Gizmo.Draw.Color = Color.Red.WithAlpha( 0.05f );
            Gizmo.Draw.LineSphere( Vector3.Zero, MaxDistance );
            Gizmo.Draw.Text( $"Cull: {MaxDistance}", new Transform( Vector3.Up * (MaxDistance * 0.9f) ), size: 14 );
        }
    }

    protected override void OnDestroy()
    {
        if ( _ambientSoundHandle != null )
        {
            _ambientSoundHandle.Stop();
        }
    }
}
using System;
using Sandbox;
using Sandbox.ui;

public sealed class SceneGrassComponent : Component
{
    public static bool EditorPainterActive { get; set; }

	public GrassRenderObject Renderer { get; private set; }

	[Property] public GrassDensityMapResource DensityMapResource { get; set; }

	[Property] public float ChunkSize { get; set; } = 256.0f;

	[Property] public int ChunkResolution { get; set; } = 64;

	[Property] public int RenderRadius { get; set; } = 6;

	[Property] public float StreamingRadius { get; set; } = 1536.0f;

	[Property] public float LodCutoff { get; set; } = 2048.0f;

	[Property] public float DistanceCutoff { get; set; } = 4096.0f;

	[Property] public float LodTransitionRange { get; set; } = 200.0f;

	[Property] public float DistanceTransitionRange { get; set; } = 200.0f;

	[Property] public float DisplacementStrength { get; set; } = 200.0f;

	[Property] public float TerrainProbeTop { get; set; } = 4096.0f;

	[Property] public float TerrainProbeBottom { get; set; } = -4096.0f;

	[Property] public float TerrainHeightOffset { get; set; } = 0.0f;

	[Property] public float GrassHeightPadding { get; set; } = 128.0f;

	[Property] public float FallbackHeight { get; set; } = 0.0f;

	[Property] public float InteractionStrength { get; set; } = 8.0f;

	[Property] public float InteractionStampRate { get; set; } = 36.0f;

	[Property] public float InteractionBendHoldDuration { get; set; } = 0.5f;

	[Property] public float InteractionDecayUpdateInterval { get; set; } = 0.05f;

	[Property] public float CutDuration { get; set; } = 8.0f;

	protected override void OnAwake()
	{
		base.OnAwake();

		using ( Scene.Push() )
		{
			Renderer = new GrassRenderObject( Scene.SceneWorld );

		}

		SyncRendererSettings();
	}

	private void SyncRendererSettings()
	{
		if ( Renderer == null )
			return;

		float streamingRadius = StreamingRadius;
		if ( EditorPainterActive )
			streamingRadius *= 10.0f;

		Renderer.ChunkSize = ChunkSize;
		Renderer.ChunkResolution = ChunkResolution;
		float requiredStreamingRadius = Math.Max( streamingRadius, Math.Max( LodCutoff, DistanceCutoff ) + ChunkSize );
		Renderer.RenderRadius = Math.Max( 1, MathX.CeilToInt( requiredStreamingRadius / Math.Max( ChunkSize, 0.001f ) ) );
		Renderer.LodCutoff = LodCutoff;
		Renderer.LodTransitionRange = LodTransitionRange;
		Renderer.DistanceTransitionRange = DistanceTransitionRange;
		Renderer.DistanceCutoff = DistanceCutoff;
		Renderer.DisplacementStrength = DisplacementStrength;
		Renderer.TerrainProbeTop = TerrainProbeTop;
		Renderer.TerrainProbeBottom = TerrainProbeBottom;
		Renderer.TerrainHeightOffset = TerrainHeightOffset;
		Renderer.GrassHeightPadding = GrassHeightPadding;
		Renderer.FallbackHeight = FallbackHeight;
		Renderer.InteractionStrength = InteractionStrength;
		Renderer.InteractionStampRate = InteractionStampRate;
		Renderer.InteractionBendHoldDuration = InteractionBendHoldDuration;
		Renderer.CutDuration = CutDuration;
		Renderer.InteractionDecayUpdateInterval = InteractionDecayUpdateInterval;
		Renderer.SetDensityResource( DensityMapResource );
		Renderer.CullingCamera = Scene.Camera;
}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		UpdateRenderer();

	}

	private void UpdateRenderer()
	{
		if ( Renderer == null )
			return;

		if ( Scene.Camera == null )
			return;

		SyncRendererSettings();
		Renderer.SetDensityResource( DensityMapResource );
		Renderer.CullingCamera = Scene.Camera;
		Renderer.UpdateInteractionField( Scene.GetAllComponents<GrassInteractionSourceComponent>(), Time.Delta );

		Vector3 camPos = Scene.Camera.WorldPosition;

		Renderer.UpdateStreaming( camPos );

		Renderer.ProcessPendingDestroy();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		Renderer?.Disable();

		Renderer = null;
	}

	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		UpdateRenderer();

		if ( !EditorPainterActive )
			return;

		if ( Renderer == null )
			return;

		Gizmo.Draw.Color = Color.Yellow;

		foreach ( var pair in Renderer.ActiveChunks )
		{
			BBox bounds = BBox.FromPositionAndSize( pair.Value.Bounds.Center, pair.Value.Bounds.Size.WithZ( 0 ) );
			Gizmo.Draw.LineBBox( bounds );
		}
	}
}
using System;
using System.Collections.Generic;
using System.Text;

namespace Saandy.Tilemapper;

public interface ITilemapSceneEvent : ISceneEvent<ITilemapSceneEvent>
{

	/// <summary>
	/// Called when tilemap was changed this frame.
	/// </summary>
	void OnTilemapChanged() { }

	/// <summary>
	/// Called when the tilemap has been updated recently and stopped being updated.
	/// </summary>
	void OnTilemapStable() { }
}
using PanelRenderTarget;
using Sandbox;
using Sandbox.UI;
using System;
using System.Linq;

public class TargetScreen : Component, Component.DontExecuteOnServer, ITargetScreen
{
	[Property, Feature( "interaction" ), Description( "Enable verbose logs for screen mouse detection" )]
	public bool DebugMouseTrace { get; set; } = false;

	[Property] public string ScreenMaterialName { get; set; } = "screen-01";
	[Property] public Material ScreenMaterial { get; set; } = Material.Load( "materials/screen.vmat" );
	[Property] public Vector2Int ScreenTextureSize { get; set; } = new( 1280, 720 );
	[Property] public float TraceDistance { get; set; } = 200f;
	[Property] public bool ForceUpdate { get; set; } = false;
	[Property] public PanelTypeReference PanelType { get; set; } = new();
	[Property, Feature( "interaction" ), Description( "Small UV offset applied after triangle interpolation to correct slight drift" )]
	public Vector2 ScreenUvOffset { get; set; } = Vector2.Zero;

	[Property, Feature("interaction"), Description("interact with 2d mouse cursor")]
	public bool ScreenCursorInteraction { get; set; } = false;

	[Property,Feature("interaction"), Description("whether to show the virtual cursor") ]
	public bool ShowVirtualCursor { get; set; } = true;

	[Property, Feature( "Optimisation" ), Description("fps when the panel is focused") ]
	public int UpdateRateFocus { get; set; } = 60;

	[Property, Feature("Optimisation")]
	public bool UpdateWhenNotFocused { get; set; } = false;

	[Property, Feature( "Optimisation" ), Description( "fps update rate when the panel is visible in camera" ) ]
	public int UpdateRateNotFocused { get; set; } = 30;

	[Property, Feature( "Optimisation" ), Description("the distance of the update rate visible but not focus") ]
	public int UpdateDistanceMax { get; set; } = 500;


	private bool _firstUpdate = false;



	private readonly TargetPanelInput _input = new();

	public ModelRenderer Renderer { get; private set; }
	private Material _screenMaterialCopy;
	private Texture _screenTexture;
	private TargetRootPanel _rootPanel;
	private PanelSceneObject _panelObject;
	private Vertex[] _cachedVertices;
	private uint[] _cachedIndices;
	private Model _cachedMeshModel;
	private TriangleMaterialRange[] _cachedTriangleMaterialRanges = Array.Empty<TriangleMaterialRange>();
	private double _tracePerfAccumMs;
	private double _tracePerfMaxMs;
	private double _tracePerfCacheAccumMs;
	private double _tracePerfTrianglesAccumMs;
	private int _tracePerfSamples;
	private TimeSince _tracePerfLogTimer;

	private readonly struct TriangleMaterialRange
	{
		public int StartTriangle { get; init; }
		public int EndTriangleExclusive { get; init; }
		public Material Material { get; init; }
	}

	protected Panel Panel { get; private set; }
	public TargetRootPanel RootPanel => _rootPanel;
	public PanelSceneObject PanelObject => _panelObject;
	protected Texture ScreenTexture => _screenTexture;

	protected override void OnPreRender()
	{
		//ensure sceneobject have transform and correct bound for engine culling
		_panelObject.Transform = Renderer.Transform.World;
		_panelObject.Bounds = Renderer.Bounds;
	}

	protected override void OnEnabled()
	{
		base.OnEnabled();

		Renderer = Components.Get<ModelRenderer>();

		if ( !Renderer.IsValid() )
		{
			Log.Warning( "No ModelRenderer found." );
			Enabled = false;
			return;
		}

		RefreshMeshCache();
		CreateTexture();
		CreatePanel();
		SetupMaterial();
		CreatePanelObject();

		OnPanelCreated( Panel );

		var panelComponent = Components.Get<PanelComponent>();
		TargetPanelSystem.Current.RegisterScreen( this );
	}

	public Panel GetPanel()
	{
		return Panel;
	}

	protected virtual void OnPanelCreated( Panel panel )
	{
	}

	private void CreateTexture()
	{
		_screenTexture = Texture.CreateRenderTarget()
			.WithSize( ScreenTextureSize.x, ScreenTextureSize.y )
			.WithInitialColor( Color.Black )
			.WithMips()
			.Create();
	}

	private void CreatePanel()
	{
		var bounds = new Rect( 0, 0, ScreenTextureSize.x, ScreenTextureSize.y );

		_rootPanel = new TargetRootPanel
		{
			RenderedManually = true,
			FixedBounds = bounds,
			FixedScale = 1f,
			PanelBounds = bounds,
			MouseVisibility = ScreenCursorInteraction ? MouseVisibility.Visible : MouseVisibility.Hidden
		};

		_rootPanel.Style.Width = Length.Pixels( ScreenTextureSize.x );
		_rootPanel.Style.Height = Length.Pixels( ScreenTextureSize.y );

		var type = PanelType?.Resolve();

		if ( type?.TargetType is null || !typeof( Panel ).IsAssignableFrom( type.TargetType ) )
		{
			Log.Warning( $"Invalid panel type: {PanelType?.TypeName}" );
			return;
		}

		Panel = type.Create<Panel>();
		_rootPanel.AddChild( Panel );

		Panel.Style.Width = Length.Percent( 100 );
		Panel.Style.Height = Length.Percent( 100 );
	}

	private void CreatePanelObject()
	{
		_panelObject = new PanelSceneObject(
			GameObject.GetBounds(),
			Scene.SceneWorld,
			_rootPanel,
			_screenTexture,
			this
		);
	}

	public void Tick()
	{
		if ( !Renderer.IsValid() || Renderer.Model is null || !Panel.IsValid() )
			return;

		var camera = Scene.Camera;

		if ( camera is null )
			return;

		if ( !TryGetPanelPosition( camera, out var panelPos ) )
		{
			ClearInput();
			return;
		}

		_panelObject.CursorPosition = panelPos;
		
		_input.Tick(
			_rootPanel,
			panelPos,
			Input.Down( "attack1" ),
			Input.MouseWheel
		);

	}


	private bool TryGetPanelPosition( CameraComponent camera, out Vector2 panelPos )
	{
		panelPos = default;
		
		var screenCenter = new Vector2(
			camera.ScreenRect.Size.x * 0.5f,
			camera.ScreenRect.Size.y * 0.5f
		);

		var ray = camera.ScreenPixelToRay( screenCenter );

		if( ScreenCursorInteraction )
		{
			ray = camera.ScreenPixelToRay( Mouse.Position );
		}

		return TryGetPanelPositionFromMeshRaycast( ray, out panelPos );
	}

	private bool TryGetPanelPositionFromMeshRaycast( Ray ray, out Vector2 panelPos )
	{
		using var scope = Sandbox.Diagnostics.Performance.Scope( "TargetScreen.MouseTrace" );
		var totalTimer = Sandbox.Diagnostics.FastTimer.StartNew();
		panelPos = default;

		var cacheTimer = Sandbox.Diagnostics.FastTimer.StartNew();
		RefreshMeshCache();
		var cacheMs = cacheTimer.ElapsedMilliSeconds;

		if ( _cachedVertices is null || _cachedIndices is null || _cachedIndices.Length < 3 )
		{
			if ( DebugMouseTrace )
				Log.Info( $"[TargetScreen] Reject mesh cache: vertices={_cachedVertices?.Length ?? 0} indices={_cachedIndices?.Length ?? 0}" );
			UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, 0f );
			return false;
		}

		var localOrigin = Renderer.Transform.World.PointToLocal( ray.Position );
		var localEnd = Renderer.Transform.World.PointToLocal( ray.Position + ray.Forward * TraceDistance );
		var localDirection = (localEnd - localOrigin).Normal;

		var bestDistance = float.MaxValue;
		var bestUv = Vector2.Zero;
		var bestTriangle = -1;
		var triangleTimer = Sandbox.Diagnostics.FastTimer.StartNew();

		for ( int triStart = 0; triStart + 2 < _cachedIndices.Length; triStart += 3 )
		{
			var triangleIndex = triStart / 3;
			if ( !IsTriangleMaterialMatch( triangleIndex ) )
				continue;

			var i0 = _cachedIndices[triStart + 0];
			var i1 = _cachedIndices[triStart + 1];
			var i2 = _cachedIndices[triStart + 2];
			if ( i0 >= _cachedVertices.Length || i1 >= _cachedVertices.Length || i2 >= _cachedVertices.Length )
				continue;

			var v0 = _cachedVertices[i0];
			var v1 = _cachedVertices[i1];
			var v2 = _cachedVertices[i2];

			if ( !TryRayTriangleIntersection( localOrigin, localDirection, v0.Position, v1.Position, v2.Position, out var distance, out var barycentric ) )
				continue;

			if ( distance > TraceDistance || distance >= bestDistance )
				continue;

			var triangleUv =
				v0.TexCoord0 * barycentric.x +
				v1.TexCoord0 * barycentric.y +
				v2.TexCoord0 * barycentric.z;

			bestDistance = distance;
			bestUv = triangleUv;
			bestTriangle = triangleIndex;
		}

		var triangleMs = triangleTimer.ElapsedMilliSeconds;

		if ( bestTriangle < 0 )
		{
			if ( DebugMouseTrace )
				Log.Info( $"[TargetScreen] No triangle hit. vertices={_cachedVertices.Length} indices={_cachedIndices.Length}" );
			UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, triangleMs );
			return false;
		}

		if ( DebugMouseTrace )
			Log.Info( $"[TargetScreen] Mesh hit triangle={bestTriangle} distance={bestDistance} rawUv={bestUv}" );

		var uv = new Vector2(
			bestUv.x - MathF.Floor( bestUv.x ),
			bestUv.y - MathF.Floor( bestUv.y )
		);

		uv += ScreenUvOffset;

		panelPos = new Vector2(
			Math.Clamp( uv.x, 0f, 1f ) * ScreenTextureSize.x,
			Math.Clamp( uv.y, 0f, 1f ) * ScreenTextureSize.y
		);

		if ( DebugMouseTrace )
			Log.Info( $"[TargetScreen] Final UV={uv} panelPos={panelPos}" );

		UpdateDebugTracePerf( totalTimer.ElapsedMilliSeconds, cacheMs, triangleMs );
		return true;
	}

	private void UpdateDebugTracePerf( double totalMs, double cacheMs, double triangleMs )
	{
		_tracePerfAccumMs += totalMs;
		_tracePerfCacheAccumMs += cacheMs;
		_tracePerfTrianglesAccumMs += triangleMs;
		_tracePerfMaxMs = Math.Max( _tracePerfMaxMs, totalMs );
		_tracePerfSamples++;

		if ( _tracePerfLogTimer < 5f )
			return;

		var avgTotalMs = _tracePerfSamples > 0 ? _tracePerfAccumMs / _tracePerfSamples : 0d;
		var avgCacheMs = _tracePerfSamples > 0 ? _tracePerfCacheAccumMs / _tracePerfSamples : 0d;
		var avgTrianglesMs = _tracePerfSamples > 0 ? _tracePerfTrianglesAccumMs / _tracePerfSamples : 0d;
		var callsPerSecond = _tracePerfLogTimer > 0f ? _tracePerfSamples / _tracePerfLogTimer : 0f;
		var triangleCount = _cachedIndices?.Length / 3 ?? 0;

		Log.Info( $"[TargetScreen] Trace perf avg={avgTotalMs:F3}ms max={_tracePerfMaxMs:F3}ms cache={avgCacheMs:F3}ms triangles={avgTrianglesMs:F3}ms calls={callsPerSecond:F1}/s tris={triangleCount}" );

		_tracePerfAccumMs = 0d;
		_tracePerfCacheAccumMs = 0d;
		_tracePerfTrianglesAccumMs = 0d;
		_tracePerfMaxMs = 0d;
		_tracePerfSamples = 0;
		_tracePerfLogTimer = 0f;
	}

	private void RefreshMeshCache()
	{
		var model = Renderer?.Model;
		if ( model is null || model == _cachedMeshModel )
			return;

		_cachedMeshModel = model;
		_cachedVertices = model.GetVertices();
		_cachedIndices = model.GetIndices();
		_cachedTriangleMaterialRanges = BuildTriangleMaterialRanges( model );

		if ( DebugMouseTrace )
			Log.Info( $"[TargetScreen] Refreshed mesh cache for {model.Name}: vertices={_cachedVertices?.Length ?? 0} indices={_cachedIndices?.Length ?? 0} materialRanges={_cachedTriangleMaterialRanges.Length}" );
	}

	private TriangleMaterialRange[] BuildTriangleMaterialRanges( Model model )
	{
		var meshInfo = model.MeshInfo;
		if ( meshInfo?.Meshes is null )
			return Array.Empty<TriangleMaterialRange>();

		var ranges = new System.Collections.Generic.List<TriangleMaterialRange>();
		var triangleCursor = 0;

		foreach ( var mesh in meshInfo.Meshes )
		{
			if ( mesh?.DrawCalls is null )
				continue;

			foreach ( var drawCall in mesh.DrawCalls )
			{
				var triangleCount = Math.Max( 0, drawCall.Indices / 3 );
				if ( triangleCount == 0 )
					continue;

				ranges.Add( new TriangleMaterialRange
				{
					StartTriangle = triangleCursor,
					EndTriangleExclusive = triangleCursor + triangleCount,
					Material = drawCall.Material
				} );

				if ( DebugMouseTrace )
					Log.Info( $"[TargetScreen] Material range {triangleCursor}->{triangleCursor + triangleCount} material={drawCall.Material?.Name}" );

				triangleCursor += triangleCount;
			}
		}

		return ranges.ToArray();
	}

	private bool IsTriangleMaterialMatch( int triangleIndex )
	{
		if ( string.IsNullOrWhiteSpace( ScreenMaterialName ) || _cachedTriangleMaterialRanges.Length == 0 )
			return true;

		foreach ( var range in _cachedTriangleMaterialRanges )
		{
			if ( triangleIndex < range.StartTriangle || triangleIndex >= range.EndTriangleExclusive )
				continue;

			var match = range.Material?.Name?.Contains( ScreenMaterialName, StringComparison.OrdinalIgnoreCase ) == true;
			if ( DebugMouseTrace && triangleIndex == range.StartTriangle )
				Log.Info( $"[TargetScreen] Triangle {triangleIndex} material={range.Material?.Name} match={match}" );
			return match;
		}

		return true;
	}

	private static bool TryRayTriangleIntersection( Vector3 origin, Vector3 direction, Vector3 a, Vector3 b, Vector3 c, out float distance, out Vector3 barycentric )
	{
		distance = 0f;
		barycentric = default;

		const float epsilon = 0.0001f;
		var edge1 = b - a;
		var edge2 = c - a;
		var pvec = Vector3.Cross( direction, edge2 );
		var det = Vector3.Dot( edge1, pvec );
		if ( MathF.Abs( det ) < epsilon )
			return false;

		var invDet = 1f / det;
		var tvec = origin - a;
		var v = Vector3.Dot( tvec, pvec ) * invDet;
		if ( v < 0f || v > 1f )
			return false;

		var qvec = Vector3.Cross( tvec, edge1 );
		var w = Vector3.Dot( direction, qvec ) * invDet;
		if ( w < 0f || v + w > 1f )
			return false;

		distance = Vector3.Dot( edge2, qvec ) * invDet;
		if ( distance < 0f )
			return false;

		barycentric = new Vector3( 1f - v - w, v, w );
		return true;
	}

	private void SetupMaterial()
	{
		var oldMaterial = Renderer.Model.Materials
			.FirstOrDefault( x => x.Name.Contains( ScreenMaterialName ) );

		var index = Renderer.Model.Materials.IndexOf( oldMaterial );

		if ( index < 0 )
		{
			Log.Warning( $"Screen material not found: {ScreenMaterialName}" );
			return;
		}

		_screenMaterialCopy = ScreenMaterial.CreateCopy();
		_screenMaterialCopy.Set( "g_tColor", _screenTexture );

		Renderer.Materials.SetOverride( index, _screenMaterialCopy );
	}

	private void ClearInput()
	{
		_input.Clear();
	}

	protected override void OnDisabled()
	{
		base.OnDisabled();

		ClearInput();

		_panelObject?.Delete();
		_panelObject = null;

		_rootPanel?.Delete( true );
		_rootPanel = null;

		_screenTexture?.Dispose();
		_screenTexture = null;

		_screenMaterialCopy = null;
		Panel = null;
		Renderer = null;

		TargetPanelSystem.Current.UnregisterScreen( this );
	}
}
@using Sandbox;
@using Sandbox.UI;
@using System.Threading.Tasks;
@using System.Collections.Generic;
@using System;
@inherits PanelComponent

<root class="@(IsFadingOut ? "fade-out" : "fade-in")" style="background-image: @(!string.IsNullOrEmpty(BackgroundImage) ? $"url({BackgroundImage})" : "none");">
    
    <div class="content">
        @* Logo Image (.png / .jpg) *@
        @if (!string.IsNullOrEmpty(LogoImage))
        {
            <img class="logo" src="@LogoImage" />
        }
        
        @* Text Lines *@
        @if (TextLines != null && TextLines.Count > 0)
        {
            <div class="text-container">
                @foreach (var line in TextLines)
                {
                    <label style="color: @line.TextColor.Hex; font-size: @(line.FontSize)px;">
                        @line.Text
                    </label>
                }
            </div>
        }
    </div>

</root>

@code {
    // === CUSTOM DATA CLASS FOR TEXT LINES ===
    
    public class SplashTextLine
    {
        [Property, Description("The text to display.")] 
        public string Text { get; set; } = "NEW LINE";

        [Property, Description("Text color for this specific line.")] 
        public Color TextColor { get; set; } = Color.White;

        [Property, Description("Font size for this specific line.")] 
        public float FontSize { get; set; } = 80f;
    }


    // === IMAGE SETTINGS ===
    
    [Property, ImageAssetPath, Group("Images"), Description("Supports .png and .jpg. If empty, the background will be black.")] 
    public string BackgroundImage { get; set; }

    [Property, ImageAssetPath, Group("Images"), Description("Main logo image (.png / .jpg). Appears above the text if both are set.")] 
    public string LogoImage { get; set; }


    // === TEXT SETTINGS ===

    [Property, Group("Text"), Description("Add text lines with individual settings (color, size).")]
    public List<SplashTextLine> TextLines { get; set; } = new();


    // === AUDIO & SCENE SETTINGS ===

    [Property, Group("Audio"), Description("Select a Sound Event (.sound) that contains your .mp3 or .ogg file.")] 
    public SoundEvent SplashSound { get; set; }

    [Property, Group("Scene"), Description("The scene to load after the splash screen finishes.")] 
    public SceneFile NextScene { get; set; }


    // === LOGIC ===
    
    public bool IsFadingOut { get; set; } = false;

    protected override void OnStart()
    {
        base.OnStart();
        
        // Start the asynchronous sequence
        _ = RunSplashSequence();
    }

    private async Task RunSplashSequence()
    {
        // 1. Wait half a second before starting to avoid stuttering during load
        await Task.Delay(500);

        // 2. Play the assigned sound (.mp3 / .ogg via Sound Event)
        if (SplashSound != null)
        {
            Sound.Play(SplashSound);
        }

        // 3. Wait while the logo/text is visible on the screen (3 seconds)
        await Task.Delay(3000);

        // 4. Trigger the fade-out animation
        IsFadingOut = true;
        StateHasChanged(); // Notify the UI to update CSS classes

        // 5. Wait for the fade-out animation to finish (matches the CSS transition time)
        await Task.Delay(2000);

        // 6. Load the next scene
        if (NextScene != null)
        {
            Scene.Load(NextScene);
        }
        else
        {
            Log.Warning("Next Scene is not assigned in the Splash Screen component!");
            GameObject.Destroy(); // Destroy the component if no scene is assigned
        }
    }
}
using System;
using Sandbox;

namespace Goo.Animation;

public record struct SmoothVector2
{
    public Vector2 Current;
    public Vector2 Target;
    public Vector2 Velocity;
    public float SmoothTime;

    public SmoothVector2(Vector2 initial, float smoothTime)
    {
        Current = initial;
        Target = initial;
        Velocity = default;
        SmoothTime = smoothTime;
    }

    public void Update(float dt)
    {
        float vx = Velocity.x, vy = Velocity.y;
        Current = new Vector2(
            MathX.SmoothDamp(Current.x, Target.x, ref vx, SmoothTime, dt),
            MathX.SmoothDamp(Current.y, Target.y, ref vy, SmoothTime, dt));
        Velocity = new Vector2(vx, vy);
    }

    public bool IsSettled =>
        MathF.Abs(Target.x - Current.x) < 0.0001f &&
        MathF.Abs(Target.y - Current.y) < 0.0001f &&
        MathF.Abs(Velocity.x) < 0.0001f &&
        MathF.Abs(Velocity.y) < 0.0001f;

    /// <summary>Advances by dt and returns true while still moving; chain calls with | (not ||) so every damper advances each frame.</summary>
    public bool Tick(float dt) { Update(dt); return !IsSettled; }
}
using System;

namespace Goo.Animation;

public readonly record struct Tween
{
    public Sandbox.Utility.Easing.Function Easing { get; init; }
    public float Duration   { get; init; }
    public float Delay      { get; init; }
    public float SpeedScale { get; init; }
    public int   Iterations { get; init; }
    public bool  PingPong   { get; init; }
    public bool  Reversed   { get; init; }

    public Tween(Sandbox.Utility.Easing.Function easing, float duration, float delay = 0f)
    {
        Easing     = easing;
        Duration   = duration;
        Delay      = delay;
        SpeedScale = 1f;
        Iterations = 1;
        PingPong   = false;
        Reversed   = false;
    }

    /// <summary>
    /// Bridge a designer-authored Sandbox.Curve (authored over [0, 1]) into a Tween.
    /// Allocates one delegate per call; cache the result in a static readonly field.
    /// </summary>
    public static Tween FromCurve(Sandbox.Curve curve, float duration, float delay = 0f)
        => new Tween(curve.Evaluate, duration, delay);

    public float Eval(float elapsedSec)
    {
        if (Duration <= 0f) return Reversed ? 0f : 1f;

        float t = (elapsedSec - Delay) * SpeedScale;
        if (t <= 0f) return Reversed ? 1f : 0f;

        float cycleDuration = PingPong ? 2f * Duration : Duration;

        if (Iterations > 0 && t >= cycleDuration * Iterations)
        {
            float endLocal = PingPong ? 0f : 1f;
            if (Reversed) endLocal = 1f - endLocal;
            return Easing(endLocal);
        }

        float cycleT = t % cycleDuration;
        float local = PingPong
            ? (cycleT < Duration ? cycleT / Duration : 1f - (cycleT - Duration) / Duration)
            : cycleT / Duration;

        if (Reversed) local = 1f - local;
        return Easing(local);
    }
}

public static class TweenExtensions
{
    public static Tween Loop(this Tween t)               => t with { Iterations = -1 };
    public static Tween Times(this Tween t, int n)       => t with { Iterations = n };
    public static Tween PingPong(this Tween t)           => t with { PingPong = true };
    public static Tween Scale(this Tween t, float speed) => t with { SpeedScale = speed };
    public static Tween WithDelay(this Tween t, float s) => t with { Delay = s };
    public static Tween Reverse(this Tween t)            => t with { Reversed = true };
}
using System;
using System.Collections;

namespace Goo;

public sealed class Children : IEnumerable
{
    internal FrameList? _list;
    internal int _buildId;
    internal Children() { }
    internal int Count { get { EnsureValid(); return _list!.Count; } }
    internal ref Frame this[int i] { get { EnsureValid(); return ref _list![i]; } }

    public void Add<T>(in T child) where T : struct, IBlob
    {
        EnsureValid();
        ref Frame slot = ref _list!.Reserve();
        child.WriteTo(ref slot);
    }

    void EnsureValid()
    {
        var ctx = BuildContext._current;
        if (ctx == null || _list == null || _buildId != ctx._currentBuildId)
            throw new InvalidOperationException(
                "Container reused across rebuilds. The same Container instance cannot survive " +
                "past the Build() it was created in. Build a new one inside Build(), or extract " +
                "a helper function that returns a fresh Container each call.");
    }

    // IEnumerable is required by C# collection-initializer syntax; iteration is not a
    // use case for Children, so this returns an empty enumerator.
    IEnumerator IEnumerable.GetEnumerator() => Array.Empty<object>().GetEnumerator();
}

namespace Goo;

/// <summary>Compile-time constraint for blob struct types. Never use as storage, return, or parameter type (boxes the struct, destroys per-Rebuild allocation profile); only valid in where T : struct, IBlob.</summary>
public interface IBlob
{
    static abstract BlobKind Kind { get; }
    string? Key { get; }
    internal void WriteTo(ref Frame frame);
}

/// <summary>Returns the single root Blob for a GooView build. A named delegate rather than
/// Func&lt;IBlob&gt; because IBlob has a static-abstract member (Kind) and C# bars such interfaces
/// as generic type arguments (CS8920). Consequence for Razor markup: a bare method group cannot
/// bind (its natural type is the illegal Func&lt;IBlob&gt;), so write
/// <c>Build=@(new BlobBuilder(MyBuild))</c>. See docs/site/docs/gotchas.md.</summary>
public delegate IBlob BlobBuilder();
// <auto-generated />
// Generated by tools/StyleFacadeEmit. Do not edit by hand.
// Source of truth: tools/StyleFacadeEmit/style-manifest.json
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

public readonly partial record struct Text
{
    public Length? Width { init => _style = StyleAccumulator.Add(_style, StyleField.Width, value); }
    public Length? Height { init => _style = StyleAccumulator.Add(_style, StyleField.Height, value); }
    public Length? Margin { init => _style = StyleAccumulator.Add(_style, StyleField.Margin, value); }
    public Length? MarginLeft { init => _style = StyleAccumulator.Add(_style, StyleField.MarginLeft, value); }
    public Length? MarginTop { init => _style = StyleAccumulator.Add(_style, StyleField.MarginTop, value); }
    public Length? MarginRight { init => _style = StyleAccumulator.Add(_style, StyleField.MarginRight, value); }
    public Length? MarginBottom { init => _style = StyleAccumulator.Add(_style, StyleField.MarginBottom, value); }
    public Color? BackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundColor, value, StyleValue.FromColor); }
    public Color? BackgroundTint { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundTint, value, StyleValue.FromColor); }
    public Length? FlexBasis { init => _style = StyleAccumulator.Add(_style, StyleField.FlexBasis, value); }
    public float? FlexGrow { init => _style = StyleAccumulator.Add(_style, StyleField.FlexGrow, value); }
    public float? FlexShrink { init => _style = StyleAccumulator.Add(_style, StyleField.FlexShrink, value); }
    public Color? FontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FontColor, value, StyleValue.FromColor); }
    public string? FontFamily { init => _style = StyleAccumulator.Add(_style, StyleField.FontFamily, value); }
    public Length? FontSize { init => _style = StyleAccumulator.Add(_style, StyleField.FontSize, value); }
    public FontSmooth? FontSmooth { init => _style = StyleAccumulator.Add(_style, StyleField.FontSmooth, value, StyleValue.FromFontSmooth); }
    public FontStyle? FontStyle { init => _style = StyleAccumulator.Add(_style, StyleField.FontStyle, value, StyleValue.FromFontStyle); }
    public FontVariantNumeric? FontVariantNumeric { init => _style = StyleAccumulator.Add(_style, StyleField.FontVariantNumeric, value, StyleValue.FromFontVariantNumeric); }
    public int? FontWeight { init => _style = StyleAccumulator.Add(_style, StyleField.FontWeight, value); }
    public Length? LetterSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.LetterSpacing, value); }
    public Length? LineHeight { init => _style = StyleAccumulator.Add(_style, StyleField.LineHeight, value); }
    public Length? MaxHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MaxHeight, value); }
    public Length? MaxWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MaxWidth, value); }
    public Length? MinHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MinHeight, value); }
    public Length? MinWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MinWidth, value); }
    public float? Opacity { init => _style = StyleAccumulator.Add(_style, StyleField.Opacity, value); }
    public TextAlign? TextAlign { init => _style = StyleAccumulator.Add(_style, StyleField.TextAlign, value, StyleValue.FromTextAlign); }
    public Length? TextBackgroundAngle { init => _style = StyleAccumulator.Add(_style, StyleField.TextBackgroundAngle, value); }
    public Color? TextDecorationColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationColor, value, StyleValue.FromColor); }
    public TextDecoration? TextDecorationLine { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationLine, value, StyleValue.FromTextDecoration); }
    public TextSkipInk? TextDecorationSkipInk { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationSkipInk, value, StyleValue.FromTextSkipInk); }
    public TextDecorationStyle? TextDecorationStyle { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationStyle, value, StyleValue.FromTextDecorationStyle); }
    public Length? TextDecorationThickness { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationThickness, value); }
    public FilterMode? TextFilter { init => _style = StyleAccumulator.Add(_style, StyleField.TextFilter, value, StyleValue.FromFilterMode); }
    public Length? TextLineThroughOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextLineThroughOffset, value); }
    public TextOverflow? TextOverflow { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverflow, value, StyleValue.FromTextOverflow); }
    public Length? TextOverlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverlineOffset, value); }
    public Color? TextStrokeColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeColor, value, StyleValue.FromColor); }
    public Length? TextStrokeWidth { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeWidth, value); }
    public TextTransform? TextTransform { init => _style = StyleAccumulator.Add(_style, StyleField.TextTransform, value, StyleValue.FromTextTransform); }
    public Length? TextUnderlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextUnderlineOffset, value); }
    public Goo.PanelTransform? Transform { init => _style = StyleAccumulator.Add(_style, StyleField.Transform, value, StyleValue.FromPanelTransform); }
    public WhiteSpace? WhiteSpace { init => _style = StyleAccumulator.Add(_style, StyleField.WhiteSpace, value, StyleValue.FromWhiteSpace); }
    public WordBreak? WordBreak { init => _style = StyleAccumulator.Add(_style, StyleField.WordBreak, value, StyleValue.FromWordBreak); }
    public Length? WordSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.WordSpacing, value); }
    public Color? HoverBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverBackgroundColor, value, StyleValue.FromColor); }
    public Color? ActiveBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveBackgroundColor, value, StyleValue.FromColor); }
    public Color? FocusBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusBackgroundColor, value, StyleValue.FromColor); }
    public Color? HoverFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverFontColor, value, StyleValue.FromColor); }
    public Color? ActiveFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveFontColor, value, StyleValue.FromColor); }
    public Color? FocusFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusFontColor, value, StyleValue.FromColor); }
    public int? TransitionMs { init => _style = StyleAccumulator.Add(_style, StyleField.TransitionMs, value); }
}
// <auto-generated />
// Generated by tools/StyleFacadeEmit. Do not edit by hand.
// Source of truth: tools/StyleFacadeEmit/style-manifest.json
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

public readonly partial record struct TextEntry
{
    public FlexDirection? FlexDirection { init => _style = StyleAccumulator.Add(_style, StyleField.FlexDirection, value, StyleValue.FromFlexDirection); }
    public Justify? JustifyContent { init => _style = StyleAccumulator.Add(_style, StyleField.JustifyContent, value, StyleValue.FromJustify); }
    public Align? AlignItems { init => _style = StyleAccumulator.Add(_style, StyleField.AlignItems, value, StyleValue.FromAlign); }
    public DisplayMode? Display { init => _style = StyleAccumulator.Add(_style, StyleField.Display, value, StyleValue.FromDisplay); }
    public Length? Width { init => _style = StyleAccumulator.Add(_style, StyleField.Width, value); }
    public Length? Height { init => _style = StyleAccumulator.Add(_style, StyleField.Height, value); }
    public Length? Padding { init => _style = StyleAccumulator.Add(_style, StyleField.Padding, value); }
    public Length? PaddingLeft { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingLeft, value); }
    public Length? PaddingTop { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingTop, value); }
    public Length? PaddingRight { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingRight, value); }
    public Length? PaddingBottom { init => _style = StyleAccumulator.Add(_style, StyleField.PaddingBottom, value); }
    public Length? Margin { init => _style = StyleAccumulator.Add(_style, StyleField.Margin, value); }
    public Length? MarginLeft { init => _style = StyleAccumulator.Add(_style, StyleField.MarginLeft, value); }
    public Length? MarginTop { init => _style = StyleAccumulator.Add(_style, StyleField.MarginTop, value); }
    public Length? MarginRight { init => _style = StyleAccumulator.Add(_style, StyleField.MarginRight, value); }
    public Length? MarginBottom { init => _style = StyleAccumulator.Add(_style, StyleField.MarginBottom, value); }
    public Length? Gap { init => _style = StyleAccumulator.Add(_style, StyleField.Gap, value); }
    public Length? RowGap { init => _style = StyleAccumulator.Add(_style, StyleField.RowGap, value); }
    public Length? ColumnGap { init => _style = StyleAccumulator.Add(_style, StyleField.ColumnGap, value); }
    public Color? BackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundColor, value, StyleValue.FromColor); }
    public Length? BorderRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRadius, value); }
    public Length? BorderTopLeftRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopLeftRadius, value); }
    public Length? BorderTopRightRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopRightRadius, value); }
    public Length? BorderBottomRightRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomRightRadius, value); }
    public Length? BorderBottomLeftRadius { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomLeftRadius, value); }
    public Align? AlignContent { init => _style = StyleAccumulator.Add(_style, StyleField.AlignContent, value, StyleValue.FromAlign); }
    public Align? AlignSelf { init => _style = StyleAccumulator.Add(_style, StyleField.AlignSelf, value, StyleValue.FromAlign); }
    public float? AspectRatio { init => _style = StyleAccumulator.Add(_style, StyleField.AspectRatio, value); }
    public Length? BackdropFilterBlur { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterBlur, value); }
    public Length? BackdropFilterBrightness { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterBrightness, value); }
    public Length? BackdropFilterContrast { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterContrast, value); }
    public Length? BackdropFilterHueRotate { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterHueRotate, value); }
    public Length? BackdropFilterInvert { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterInvert, value); }
    public Length? BackdropFilterSaturate { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterSaturate, value); }
    public Length? BackdropFilterSepia { init => _style = StyleAccumulator.Add(_style, StyleField.BackdropFilterSepia, value); }
    public Length? BackgroundAngle { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundAngle, value); }
    public string? BackgroundBlendMode { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundBlendMode, value); }
    public Texture? BackgroundImage { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundImage, value); }
    public bool? BackgroundPlaybackPaused { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPlaybackPaused, value); }
    public Length? BackgroundPositionX { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPositionX, value); }
    public Length? BackgroundPositionY { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundPositionY, value); }
    public BackgroundRepeat? BackgroundRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundRepeat, value, StyleValue.FromBackgroundRepeat); }
    public Length? BackgroundSizeX { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundSizeX, value); }
    public Length? BackgroundSizeY { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundSizeY, value); }
    public Color? BackgroundTint { init => _style = StyleAccumulator.Add(_style, StyleField.BackgroundTint, value, StyleValue.FromColor); }
    public Color? BorderBottomColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomColor, value, StyleValue.FromColor); }
    public Color? BorderColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderColor, value, StyleValue.FromColor); }
    public Color? BorderLeftColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderLeftColor, value, StyleValue.FromColor); }
    public Color? BorderRightColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRightColor, value, StyleValue.FromColor); }
    public Color? BorderTopColor { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopColor, value, StyleValue.FromColor); }
    public BorderImageFill? BorderImageFill { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageFill, value, StyleValue.FromBorderImageFill); }
    public BorderImageRepeat? BorderImageRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageRepeat, value, StyleValue.FromBorderImageRepeat); }
    public Texture? BorderImageSource { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageSource, value); }
    public Color? BorderImageTint { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageTint, value, StyleValue.FromColor); }
    public Length? BorderImageWidthBottom { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthBottom, value); }
    public Length? BorderImageWidthLeft { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthLeft, value); }
    public Length? BorderImageWidthRight { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthRight, value); }
    public Length? BorderImageWidthTop { init => _style = StyleAccumulator.Add(_style, StyleField.BorderImageWidthTop, value); }
    public Length? BorderBottomWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderBottomWidth, value); }
    public Length? BorderLeftWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderLeftWidth, value); }
    public Length? BorderRightWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderRightWidth, value); }
    public Length? BorderTopWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderTopWidth, value); }
    public Length? BorderWidth { init => _style = StyleAccumulator.Add(_style, StyleField.BorderWidth, value); }
    public Length? Bottom { init => _style = StyleAccumulator.Add(_style, StyleField.Bottom, value); }
    public Length? Left { init => _style = StyleAccumulator.Add(_style, StyleField.Left, value); }
    public Length? Right { init => _style = StyleAccumulator.Add(_style, StyleField.Right, value); }
    public Length? Top { init => _style = StyleAccumulator.Add(_style, StyleField.Top, value); }
    public Color? CaretColor { init => _style = StyleAccumulator.Add(_style, StyleField.CaretColor, value, StyleValue.FromColor); }
    public string? Cursor { init => _style = StyleAccumulator.Add(_style, StyleField.Cursor, value); }
    public Length? FilterBlur { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBlur, value); }
    public Color? FilterBorderColor { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBorderColor, value, StyleValue.FromColor); }
    public Length? FilterBorderWidth { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBorderWidth, value); }
    public Length? FilterBrightness { init => _style = StyleAccumulator.Add(_style, StyleField.FilterBrightness, value); }
    public Length? FilterContrast { init => _style = StyleAccumulator.Add(_style, StyleField.FilterContrast, value); }
    public Length? FilterHueRotate { init => _style = StyleAccumulator.Add(_style, StyleField.FilterHueRotate, value); }
    public Length? FilterInvert { init => _style = StyleAccumulator.Add(_style, StyleField.FilterInvert, value); }
    public Length? FilterSaturate { init => _style = StyleAccumulator.Add(_style, StyleField.FilterSaturate, value); }
    public Length? FilterSepia { init => _style = StyleAccumulator.Add(_style, StyleField.FilterSepia, value); }
    public Color? FilterTint { init => _style = StyleAccumulator.Add(_style, StyleField.FilterTint, value, StyleValue.FromColor); }
    public Length? FlexBasis { init => _style = StyleAccumulator.Add(_style, StyleField.FlexBasis, value); }
    public float? FlexGrow { init => _style = StyleAccumulator.Add(_style, StyleField.FlexGrow, value); }
    public float? FlexShrink { init => _style = StyleAccumulator.Add(_style, StyleField.FlexShrink, value); }
    public Wrap? FlexWrap { init => _style = StyleAccumulator.Add(_style, StyleField.FlexWrap, value, StyleValue.FromWrap); }
    public Color? FontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FontColor, value, StyleValue.FromColor); }
    public string? FontFamily { init => _style = StyleAccumulator.Add(_style, StyleField.FontFamily, value); }
    public Length? FontSize { init => _style = StyleAccumulator.Add(_style, StyleField.FontSize, value); }
    public FontSmooth? FontSmooth { init => _style = StyleAccumulator.Add(_style, StyleField.FontSmooth, value, StyleValue.FromFontSmooth); }
    public FontStyle? FontStyle { init => _style = StyleAccumulator.Add(_style, StyleField.FontStyle, value, StyleValue.FromFontStyle); }
    public FontVariantNumeric? FontVariantNumeric { init => _style = StyleAccumulator.Add(_style, StyleField.FontVariantNumeric, value, StyleValue.FromFontVariantNumeric); }
    public int? FontWeight { init => _style = StyleAccumulator.Add(_style, StyleField.FontWeight, value); }
    public ImageRendering? ImageRendering { init => _style = StyleAccumulator.Add(_style, StyleField.ImageRendering, value, StyleValue.FromImageRendering); }
    public Length? LetterSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.LetterSpacing, value); }
    public Length? LineHeight { init => _style = StyleAccumulator.Add(_style, StyleField.LineHeight, value); }
    public Length? MaskAngle { init => _style = StyleAccumulator.Add(_style, StyleField.MaskAngle, value); }
    public Texture? MaskImage { init => _style = StyleAccumulator.Add(_style, StyleField.MaskImage, value); }
    public MaskMode? MaskMode { init => _style = StyleAccumulator.Add(_style, StyleField.MaskMode, value, StyleValue.FromMaskMode); }
    public Length? MaskPositionX { init => _style = StyleAccumulator.Add(_style, StyleField.MaskPositionX, value); }
    public Length? MaskPositionY { init => _style = StyleAccumulator.Add(_style, StyleField.MaskPositionY, value); }
    public BackgroundRepeat? MaskRepeat { init => _style = StyleAccumulator.Add(_style, StyleField.MaskRepeat, value, StyleValue.FromBackgroundRepeat); }
    public MaskScope? MaskScope { init => _style = StyleAccumulator.Add(_style, StyleField.MaskScope, value, StyleValue.FromMaskScope); }
    public Length? MaskSizeX { init => _style = StyleAccumulator.Add(_style, StyleField.MaskSizeX, value); }
    public Length? MaskSizeY { init => _style = StyleAccumulator.Add(_style, StyleField.MaskSizeY, value); }
    public Length? MaxHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MaxHeight, value); }
    public Length? MaxWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MaxWidth, value); }
    public Length? MinHeight { init => _style = StyleAccumulator.Add(_style, StyleField.MinHeight, value); }
    public Length? MinWidth { init => _style = StyleAccumulator.Add(_style, StyleField.MinWidth, value); }
    public string? MixBlendMode { init => _style = StyleAccumulator.Add(_style, StyleField.MixBlendMode, value); }
    public float? Opacity { init => _style = StyleAccumulator.Add(_style, StyleField.Opacity, value); }
    public int? Order { init => _style = StyleAccumulator.Add(_style, StyleField.Order, value); }
    public Color? OutlineColor { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineColor, value, StyleValue.FromColor); }
    public Length? OutlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineOffset, value); }
    public Length? OutlineWidth { init => _style = StyleAccumulator.Add(_style, StyleField.OutlineWidth, value); }
    public OverflowMode? Overflow { init => _style = StyleAccumulator.Add(_style, StyleField.Overflow, value, StyleValue.FromOverflowMode); }
    public OverflowMode? OverflowX { init => _style = StyleAccumulator.Add(_style, StyleField.OverflowX, value, StyleValue.FromOverflowMode); }
    public OverflowMode? OverflowY { init => _style = StyleAccumulator.Add(_style, StyleField.OverflowY, value, StyleValue.FromOverflowMode); }
    public Length? PerspectiveOriginX { init => _style = StyleAccumulator.Add(_style, StyleField.PerspectiveOriginX, value); }
    public Length? PerspectiveOriginY { init => _style = StyleAccumulator.Add(_style, StyleField.PerspectiveOriginY, value); }
    public PointerEvents? PointerEvents { init => _style = StyleAccumulator.Add(_style, StyleField.PointerEvents, value, StyleValue.FromPointerEvents); }
    public PositionMode? Position { init => _style = StyleAccumulator.Add(_style, StyleField.Position, value, StyleValue.FromPositionMode); }
    public string? SoundIn { init => _style = StyleAccumulator.Add(_style, StyleField.SoundIn, value); }
    public string? SoundOut { init => _style = StyleAccumulator.Add(_style, StyleField.SoundOut, value); }
    public TextAlign? TextAlign { init => _style = StyleAccumulator.Add(_style, StyleField.TextAlign, value, StyleValue.FromTextAlign); }
    public Length? TextBackgroundAngle { init => _style = StyleAccumulator.Add(_style, StyleField.TextBackgroundAngle, value); }
    public Color? TextDecorationColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationColor, value, StyleValue.FromColor); }
    public TextDecoration? TextDecorationLine { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationLine, value, StyleValue.FromTextDecoration); }
    public TextSkipInk? TextDecorationSkipInk { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationSkipInk, value, StyleValue.FromTextSkipInk); }
    public TextDecorationStyle? TextDecorationStyle { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationStyle, value, StyleValue.FromTextDecorationStyle); }
    public Length? TextDecorationThickness { init => _style = StyleAccumulator.Add(_style, StyleField.TextDecorationThickness, value); }
    public FilterMode? TextFilter { init => _style = StyleAccumulator.Add(_style, StyleField.TextFilter, value, StyleValue.FromFilterMode); }
    public Length? TextLineThroughOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextLineThroughOffset, value); }
    public TextOverflow? TextOverflow { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverflow, value, StyleValue.FromTextOverflow); }
    public Length? TextOverlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextOverlineOffset, value); }
    public Color? TextStrokeColor { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeColor, value, StyleValue.FromColor); }
    public Length? TextStrokeWidth { init => _style = StyleAccumulator.Add(_style, StyleField.TextStrokeWidth, value); }
    public TextTransform? TextTransform { init => _style = StyleAccumulator.Add(_style, StyleField.TextTransform, value, StyleValue.FromTextTransform); }
    public Length? TextUnderlineOffset { init => _style = StyleAccumulator.Add(_style, StyleField.TextUnderlineOffset, value); }
    public Goo.PanelTransform? Transform { init => _style = StyleAccumulator.Add(_style, StyleField.Transform, value, StyleValue.FromPanelTransform); }
    public Length? TransformOriginX { init => _style = StyleAccumulator.Add(_style, StyleField.TransformOriginX, value); }
    public Length? TransformOriginY { init => _style = StyleAccumulator.Add(_style, StyleField.TransformOriginY, value); }
    public WhiteSpace? WhiteSpace { init => _style = StyleAccumulator.Add(_style, StyleField.WhiteSpace, value, StyleValue.FromWhiteSpace); }
    public WordBreak? WordBreak { init => _style = StyleAccumulator.Add(_style, StyleField.WordBreak, value, StyleValue.FromWordBreak); }
    public Length? WordSpacing { init => _style = StyleAccumulator.Add(_style, StyleField.WordSpacing, value); }
    public int? ZIndex { init => _style = StyleAccumulator.Add(_style, StyleField.ZIndex, value); }
    public Color? HoverBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverBackgroundColor, value, StyleValue.FromColor); }
    public Color? ActiveBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveBackgroundColor, value, StyleValue.FromColor); }
    public Color? FocusBackgroundColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusBackgroundColor, value, StyleValue.FromColor); }
    public Color? HoverFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.HoverFontColor, value, StyleValue.FromColor); }
    public Color? ActiveFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.ActiveFontColor, value, StyleValue.FromColor); }
    public Color? FocusFontColor { init => _style = StyleAccumulator.Add(_style, StyleField.FocusFontColor, value, StyleValue.FromColor); }
    public int? TransitionMs { init => _style = StyleAccumulator.Add(_style, StyleField.TransitionMs, value); }
}
using Goo;
using Sandbox.UI;

namespace Sandbox;

public class CounterUI : GooPanel<Container>
{
	private int _count;

	protected override Container Build() => new Container
	{
		Padding = 16,
		Width = 128,
		Height = 128,
		BackgroundColor = Color.White,
		BorderRadius = 12,
		FlexDirection = FlexDirection.Row,
		Gap = 12,
		AlignItems = Align.Center,
		Children =
		{
			new Text(_count.ToString()),
			new Container
			{
				Padding = 8,
				BackgroundColor = Color.Orange,
				HoverBackgroundColor = Color.Cyan,
				BorderRadius = 6,
				OnClick = e => { _count++; Rebuild(); },
				Children = { new Text("+") },
			},
		},
	};
}
using System;
using System.Collections.Generic;
using Sandbox;

namespace Goo.Input;

// Polls a curated key catalog once per frame for rising-edge presses + modifier state. ReemitHeldOnModifierRise re-emits a held key when a modifier rises (so "hold W, press Ctrl" reads as a chord).
public sealed class KeyTracker
{
    public bool ReemitHeldOnModifierRise { get; set; } = false;

    public ModifierState Modifiers { get; private set; }
    public IReadOnlyList<KeyDescriptor> JustPressed => _justPressed;

    readonly IReadOnlyList<KeyDescriptor> _catalog;
    readonly HashSet<string>     _downLastFrame = new();
    readonly List<KeyDescriptor> _justPressed   = new();

    public KeyTracker() : this( KnownKeys.All ) { }

    public KeyTracker( IReadOnlyList<KeyDescriptor> catalog )
    {
        _catalog = catalog;
    }

    public void Reset()
    {
        _downLastFrame.Clear();
        _justPressed.Clear();
        Modifiers = default;
    }

    static readonly Func<string, bool> s_engineDown = Sandbox.Input.Keyboard.Down;

    public void Poll() => Poll( s_engineDown );

    // isDown seam exists because engine Input statics throw outside a running engine process.
    public void Poll( Func<string, bool> isDown )
    {
        _justPressed.Clear();
        var prev = Modifiers;

        for ( int i = 0; i < _catalog.Count; i++ )
        {
            var  d    = _catalog[i];
            bool down = isDown( d.EngineName );
            if ( down && !_downLastFrame.Contains( d.EngineName ) )
                _justPressed.Add( d );
            if ( down ) _downLastFrame.Add( d.EngineName );
            else        _downLastFrame.Remove( d.EngineName );
        }

        Modifiers = new ModifierState(
            _downLastFrame.Contains( "ctrl"  ),
            _downLastFrame.Contains( "shift" ),
            _downLastFrame.Contains( "alt"   ),
            _downLastFrame.Contains( "win"   ) );

        if ( !ReemitHeldOnModifierRise ) return;

        bool modRose = ( Modifiers.Ctrl  && !prev.Ctrl  ) || ( Modifiers.Shift && !prev.Shift )
                    || ( Modifiers.Alt   && !prev.Alt   ) || ( Modifiers.Meta  && !prev.Meta  );
        if ( !modRose ) return;

        for ( int i = 0; i < _catalog.Count; i++ )
        {
            var d = _catalog[i];
            if ( d.Class == KeyClass.Modifier ) continue;
            if ( !_downLastFrame.Contains( d.EngineName ) ) continue;
            if ( _justPressed.Contains( d ) ) continue;
            _justPressed.Add( d );
        }
    }

    /// <summary>True if the named key had a rising edge (just-pressed, not held) this frame; call Poll first.</summary>
    public bool Pressed( string engineName )
    {
        for ( int i = 0; i < _justPressed.Count; i++ )
            if ( _justPressed[i].EngineName == engineName ) return true;
        return false;
    }
}
using System;
using Sandbox;
using Sandbox.UI;

namespace Goo.Internal;

internal sealed class StatefulLabel : Label, IStatefulHost, IStatefulEventHost
{
    StateController? _state;

    internal Action<MousePanelEvent>? _onClick;
    internal Action<MousePanelEvent>? _onRightClick;
    internal Action<MousePanelEvent>? _onMiddleClick;
    internal Action<MousePanelEvent>? _onMouseEnter;
    internal Action<MousePanelEvent>? _onMouseLeave;
    internal Action<MousePanelEvent>? _onMouseDown;
    internal Action<MousePanelEvent>? _onMouseUp;
    internal Action<MousePanelEvent>? _onMouseMove;
    internal bool    _userSetPointerEvents;
    internal Action? _requestRebuild;
    public Action? RequestRebuild { set => _requestRebuild = value; }

    public void ApplyStateVariants(
        Color? baseBg,  Color? baseFg,
        Color? hoverBg, Color? activeBg, Color? focusBg,
        Color? hoverFg, Color? activeFg, Color? focusFg,
        int? transitionMs)
    {
        _state ??= new StateController(this);
        _state.ApplyVariants(
            baseBg, baseFg,
            hoverBg, activeBg, focusBg,
            hoverFg, activeFg, focusFg,
            transitionMs);
    }

    public void ClearStateVariants() => _state?.ClearVariants();

    public bool HasActiveStateVariants => _state?.HasActiveVariants ?? false;

    public void ApplyEvents(in BlobEvents events)
    {
        _onClick      = events.OnClick;
        _onRightClick = events.OnRightClick;
        _onMiddleClick = events.OnMiddleClick;
        _onMouseEnter = events.OnMouseEnter;
        _onMouseLeave = events.OnMouseLeave;
        _onMouseDown  = events.OnMouseDown;
        _onMouseUp    = events.OnMouseUp;
        _onMouseMove  = events.OnMouseMove;
    }

    public bool HasEventHandlers =>
        _onClick != null || _onRightClick != null || _onMiddleClick != null || _onMouseEnter != null || _onMouseLeave != null ||
        _onMouseDown != null || _onMouseUp != null || _onMouseMove != null;

    public bool UserSetPointerEvents
    {
        get => _userSetPointerEvents;
        set => _userSetPointerEvents = value;
    }

    protected override void OnClick(MousePanelEvent e)
    {
        base.OnClick(e);
        EventDispatch.Fire(_onClick, e, _requestRebuild);
    }

    protected override void OnRightClick(MousePanelEvent e)
    {
        base.OnRightClick(e);
        EventDispatch.Fire(_onRightClick, e, _requestRebuild);
    }

    protected override void OnMiddleClick(MousePanelEvent e)
    {
        base.OnMiddleClick(e);
        EventDispatch.Fire(_onMiddleClick, e, _requestRebuild);
    }

    protected override void OnMouseOver(MousePanelEvent e)
    {
        base.OnMouseOver(e);
        EventDispatch.Fire(_onMouseEnter, e, _requestRebuild);
    }

    protected override void OnMouseOut(MousePanelEvent e)
    {
        base.OnMouseOut(e);
        EventDispatch.Fire(_onMouseLeave, e, _requestRebuild);
    }

    protected override void OnMouseDown(MousePanelEvent e)
    {
        base.OnMouseDown(e);
        EventDispatch.Fire(_onMouseDown, e, _requestRebuild);
    }

    protected override void OnMouseUp(MousePanelEvent e)
    {
        base.OnMouseUp(e);
        EventDispatch.Fire(_onMouseUp, e, _requestRebuild);
    }

    protected override void OnMouseMove(MousePanelEvent e)
    {
        base.OnMouseMove(e);
        EventDispatch.Fire(_onMouseMove, e, _requestRebuild);
    }
}
using System;
using System.Collections.Generic;
using Sandbox;
using Sandbox.Rendering;
using Sandbox.UI;

namespace Goo;

/// <summary>
/// A custom shader applied to a Blob. Point it at a compiled .shader; it parses the shader
/// source's Attribute() declarations to validate uniform names and to reset uniforms other
/// panels have set (panels share one CommandList attribute namespace), and pushes the uniform
/// bag every frame. Set uniforms with the collection initializer (<c>["Name"] = value</c>);
/// a value may be a literal or a per-frame Func. Subclass and override <see cref="Apply"/>
/// only for bespoke per-frame CPU logic.
/// </summary>
public record ShaderEffect
{
    static readonly Dictionary<object, Material> _materialCache = new();
    static readonly Dictionary<object, ShaderSchemaInfo?> _schemaCache = new();

    // Every uniform name any ShaderEffect has pushed this session, with the last value pushed
    // (its runtime type drives the reset conversion). Panels share one CommandList attribute
    // namespace, so a uniform set by one panel persists into every later panel's draw; an
    // effect that does not set a seen uniform must reset it to its shader's declared default
    // or it inherits the other panel's value (view-5u5m). Touched only from Apply (render thread).
    static readonly Dictionary<string, object> _seenUniforms = new();

    internal sealed class ShaderSchemaInfo
    {
        public required HashSet<string> Names;                       // declared attribute names
        public required Dictionary<string, (Vector4 Floats, Vector4 Ints)> Defaults;
    }

    readonly string? _path;
    readonly Shader? _shader;
    readonly GrabMode _grab;
    readonly Dictionary<string, UniformValue> _bag = new();
    bool _validated;

    /// <summary>For subclasses that supply their own <see cref="Material"/> and <see cref="Apply"/>.</summary>
    protected ShaderEffect() { }

    /// <summary>Apply the shader at <paramref name="shaderPath"/> (e.g. "shaders/ui_dither.shader").</summary>
    public ShaderEffect( string shaderPath, GrabMode grab = GrabMode.None )
    {
        _path = shaderPath;
        _grab = grab;
    }

    /// <summary>Apply a shader resource (drag-droppable in the inspector).</summary>
    public ShaderEffect( Shader shader, GrabMode grab = GrabMode.None )
    {
        _shader = shader;
        _grab = grab;
    }

    /// <summary>The shader asset path, or null when constructed from a <see cref="T:Sandbox.Shader"/> resource.</summary>
    public string? ShaderPath => _path;

    /// <summary>How this effect grabs the framebuffer behind its panel.</summary>
    public GrabMode Grab => _grab;

    /// <summary>Get or set a uniform by its shader attribute name.</summary>
    public UniformValue this[string name]
    {
        get => _bag[name];
        set => _bag[name] = value;
    }

    object CacheKey => (object?)_path ?? _shader!;

    /// <summary>The material this effect draws with, cached so it is excluded from record equality.</summary>
    public virtual Material Material
    {
        get
        {
            var key = CacheKey;
            if ( !_materialCache.TryGetValue( key, out var mat ) )
            {
                mat = _path is not null ? Material.FromShader( _path ) : Material.FromShader( _shader! );
                _materialCache[key] = mat;
            }
            return mat;
        }
    }

    /// <summary>Create the Material now, on the calling thread. Call on the main thread; Draw runs on the render thread and must only read the cache.</summary>
    public void Warm() => _ = Material;

    /// <summary>Set this effect's shader attributes for the frame, then grab the framebuffer per <see cref="Grab"/>.</summary>
    protected internal virtual void Apply( CommandList cl, Rect rect )
    {
        cl.Attributes.Set( "BoxSize", new Vector2( rect.Width, rect.Height ) );

        // Subclass escape hatch: a derived effect calling base.Apply gets only BoxSize.
        if ( _path is null && _shader is null ) return;

        Validate();

        // Reset seen-but-unset uniforms to this shader's declared defaults so another panel's
        // attribute writes do not bleed into this draw (view-5u5m).
        var schema = SchemaFor( CacheKey );
        if ( schema is not null )
        {
            foreach ( var (name, sample) in _seenUniforms )
            {
                if ( _bag.ContainsKey( name ) ) continue;
                if ( !schema.Defaults.TryGetValue( name, out var def ) ) continue;
                if ( ResetValue( sample, def.Floats, def.Ints ) is { } reset )
                    SetAttribute( cl, name, reset );
            }
        }

        foreach ( var (name, value) in _bag )
        {
            var resolved = value.Resolve();
            SetAttribute( cl, name, resolved );
            if ( resolved is not Texture )
                _seenUniforms[name] = resolved;
        }

        switch ( _grab )
        {
            case GrabMode.Sharp:
                cl.Attributes.GrabFrameTexture( "FrameBufferCopyTexture" );
                break;
            case GrabMode.Blurred:
                cl.Attributes.GrabFrameTexture( "FrameBufferCopyTexture", Graphics.DownsampleMethod.GaussianBlur );
                break;
        }
    }

    static void SetAttribute( CommandList cl, string name, object value )
    {
        switch ( value )
        {
            case float f:   cl.Attributes.Set( name, f ); break;
            case bool b:    cl.Attributes.Set( name, b ); break;
            case Vector2 v: cl.Attributes.Set( name, v ); break;
            case Vector3 v: cl.Attributes.Set( name, v ); break;
            case Vector4 v: cl.Attributes.Set( name, v ); break;
            case Color c:   cl.Attributes.Set( name, c ); break;
            case Texture t: cl.Attributes.Set( name, t ); break;
        }
    }

    // Reads the shader's declared attribute names once per instance and warns on uniform names
    // the shader does not declare. Skipped silently when the source is unavailable.
    void Validate()
    {
        if ( _validated ) return;
        _validated = true;

        var valid = SchemaFor( CacheKey )?.Names;
        if ( valid is null || valid.Count == 0 ) return;

        foreach ( var name in _bag.Keys )
            if ( !valid.Contains( name ) )
                Sandbox.Internal.GlobalSystemNamespace.Log.Warning(
                    $"ShaderEffect for \"{_path ?? _shader?.ResourcePath}\": uniform \"{name}\" is not declared by the shader " +
                    $"(valid: {string.Join( ", ", valid )}). Ignored." );
    }

    // Names + defaults come from parsing the .shader SOURCE, which ships with the project and
    // declares every Attribute() with its Default(). The engine's Shader.Schema is editor-only
    // plumbing: at game runtime it throws ("Load must be called on the main thread!" — Apply
    // runs on the render thread) so it is not consulted at all (view-5u5m probe, 2026-06-11).
    // Null when the source is unreadable (e.g. unit tests, published build without raw
    // .shader files); reset and validation both skip then.
    static ShaderSchemaInfo? SchemaFor( object key )
    {
        if ( _schemaCache.TryGetValue( key, out var info ) ) return info;

        info = null;
        if ( (key as string ?? (key as Shader)?.ResourcePath) is { } srcPath )
        {
            try
            {
                info = ParseShaderSource( FileSystem.Mounted.ReadAllText( srcPath ) );
            }
            catch
            {
                info = null;
            }
        }

        _schemaCache[key] = info;
        return info;
    }

    static readonly System.Text.RegularExpressions.Regex _declRegex = new(
        @"\b(?<type>float[234]?|bool|int|Texture2D)\s+\w+\s*<(?<block>[^>]*)>",
        System.Text.RegularExpressions.RegexOptions.Compiled );
    static readonly System.Text.RegularExpressions.Regex _attrRegex = new(
        @"Attribute\s*\(\s*""(?<name>[^""]+)""\s*\)",
        System.Text.RegularExpressions.RegexOptions.Compiled );
    static readonly System.Text.RegularExpressions.Regex _defaultRegex = new(
        @"Default[234]?\s*\(\s*(?<args>[^)]*)\)",
        System.Text.RegularExpressions.RegexOptions.Compiled );

    // Pure: extracts Attribute()-bound uniform declarations and their Default() values from
    // .shader source. Defaults are stored in both vector slots so ResetValue can convert by the
    // bled value's runtime type. Texture declarations contribute a name (for validation) but no
    // default. Only the main file is scanned; #include'd uniforms (BoxSize, DpiScale) are
    // framework-managed and excluded anyway. Missing Default() = zeros, matching the engine's
    // unset-attribute behavior.
    internal static ShaderSchemaInfo? ParseShaderSource( string source )
    {
        var names    = new HashSet<string>();
        var defaults = new Dictionary<string, (Vector4 Floats, Vector4 Ints)>();

        foreach ( System.Text.RegularExpressions.Match m in _declRegex.Matches( source ) )
        {
            var block = m.Groups["block"].Value;
            var attr  = _attrRegex.Match( block );
            if ( !attr.Success ) continue;

            var name = attr.Groups["name"].Value;
            names.Add( name );

            if ( name is "BoxSize" or "DpiScale" ) continue;     // framework-managed per draw
            if ( m.Groups["type"].Value == "Texture2D" ) continue; // no resettable default

            Vector4 v = default;
            var def = _defaultRegex.Match( block );
            if ( def.Success )
            {
                var parts = def.Groups["args"].Value.Split( ',' );
                for ( int i = 0; i < parts.Length && i < 4; i++ )
                {
                    if ( !float.TryParse( parts[i].Trim(), System.Globalization.NumberStyles.Float,
                             System.Globalization.CultureInfo.InvariantCulture, out var f ) ) continue;
                    switch ( i )
                    {
                        case 0: v.x = f; break;
                        case 1: v.y = f; break;
                        case 2: v.z = f; break;
                        case 3: v.w = f; break;
                    }
                }
            }
            defaults[name] = (v, v);
        }

        return names.Count > 0 ? new ShaderSchemaInfo { Names = names, Defaults = defaults } : null;
    }

    // Pure: converts a shader's declared default (schema FloatDefault/IntDefault vectors) to the
    // runtime type of the value that bled in, so the reset lands in the same attribute slot type.
    // Null = no safe reset (unsupported type; textures are never recorded so never reach this).
    internal static object? ResetValue( object sample, Vector4 floats, Vector4 ints ) => sample switch
    {
        float   => floats.x,
        bool    => ints.x != 0f,
        Vector2 => new Vector2( floats.x, floats.y ),
        Vector3 => new Vector3( floats.x, floats.y, floats.z ),
        Color   => new Color( floats.x, floats.y, floats.z, floats.w ),
        Vector4 => floats,
        _       => null,
    };

    public virtual bool Equals( ShaderEffect? other )
    {
        if ( other is null ) return false;
        if ( ReferenceEquals( this, other ) ) return true;
        if ( EqualityContract != other.EqualityContract ) return false;
        return _path == other._path
            && ReferenceEquals( _shader, other._shader )
            && _grab == other._grab
            && BagEquals( _bag, other._bag );
    }

    public override int GetHashCode()
    {
        var hc = new HashCode();
        hc.Add( EqualityContract );
        hc.Add( _path );
        hc.Add( _grab );
        hc.Add( _bag.Count );
        return hc.ToHashCode();
    }

    static bool BagEquals( Dictionary<string, UniformValue> a, Dictionary<string, UniformValue> b )
    {
        if ( a.Count != b.Count ) return false;
        foreach ( var (k, v) in a )
            if ( !b.TryGetValue( k, out var bv ) || !v.Equals( bv ) ) return false;
        return true;
    }
}
using Sandbox.Citizen;

namespace ShrimpleCharacterController;

[Hide]
public sealed class ShrimpleFlyer : Component
{
    [RequireComponent]
    public ShrimpleCharacterController Controller { get; set; }
    public GameObject Camera { get; set; }

    [Property]
    [Range(400f, 1600f)]
    public float WalkSpeed { get; set; } = 800f;

    [Property]
    [Range(800f, 4000f)]
    public float RunSpeed { get; set; } = 2400f;

    public Angles EyeAngles { get; set; }

    protected override void OnStart()
    {
        base.OnStart();

        Camera = new GameObject(true, "Camera");
        Camera.SetParent(GameObject);
        var cameraComponent = Camera.Components.Create<CameraComponent>();
        cameraComponent.ZFar = 32768f;
    }

    protected override void OnFixedUpdate()
    {
        base.OnFixedUpdate();

        var isDucking = Input.Down("Duck");
        var isRunning = Input.Down("Run");
        var ascending = Input.Down("Jump") ? 1f : 0f;
        var descending = Input.Down("Duck") ? -1f : 0f;
        var wishSpeed = isRunning ? RunSpeed : WalkSpeed;
        var wishDirection = (Input.AnalogMove + Vector3.Up * (ascending + descending)).Normal * EyeAngles.ToRotation();

        Controller.WishVelocity = wishDirection * wishSpeed;
        Controller.Move();
    }

    protected override void OnUpdate()
    {
        base.OnUpdate();

        EyeAngles += Input.AnalogLook;
        EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -40f, 40f));

        var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 760f;
        Camera.WorldRotation = EyeAngles.ToRotation();
        Camera.LocalPosition = cameraOffset * Camera.WorldRotation;
    }
}
using System;
using System.Linq;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadIntersectionComponent
{
	[Property, Feature("Terrain", Icon = "landscape", Tint = EditorTint.Green), Hide]
	private Terrain TerrainTarget { get; set; }

	[Property, Feature("Terrain"), Range(0f, 2000f)]
	public float TerrainFalloffRadius { get; set; } = 500f;

	[Property, Feature("Terrain"), Range(-10f, 10f)]
	public float TerrainHeightOffset { get; set; } = 0f;

	[Property, Feature("Terrain"), Range(0f, 100f)]
	public float TerrainRoadInset { get; set; } = 10f;

	[Property, Feature("Terrain"), Group("Texture"), Range(100f, 1000f)]
	public float TerrainEdgeRadius { get; set; } = 500f;

	[Property, Feature("Terrain"), Group("Texture")]
	public TerrainTextureLayer TerrainTargetLayer { get; set; } = TerrainTextureLayer.Overlay;

	[Property, Feature("Terrain"), Group("Texture"), Range(0f, 1f)]
	public float TerrainTextureNoise { get; set; } = 0.2f;

	[Property, Feature("Terrain"), Group("Texture")]
	public TerrainMaterial[] TerrainEdgeMaterials { get; set; } = Array.Empty<TerrainMaterial>();

	[Property, Feature("Terrain"), Group("Texture")]
	public Gradient TerrainEdgeBlendGradient = new Gradient(
		new Gradient.ColorFrame(0, Color.White),
		new Gradient.ColorFrame(1, Color.White.WithAlpha(0f))
	);



	[Button("Apply to the Ground"), Feature("Terrain")]
	private void ApplyTerrainToGround()
	{
		if (!Scene.IsEditor)
			return;

		AdaptTerrainToIntersection();
	}



	public void AdaptTerrainToIntersection()
	{
		if (!TerrainTarget.IsValid())
		{
			// Always take the closest terrain
			TerrainTarget = Scene.GetAllComponents<Terrain>().OrderBy(x => x.WorldPosition.DistanceSquared(WorldPosition)).FirstOrDefault();
		}

		if (!TerrainTarget.IsValid())
		{
			Log.Warning("RoadTool: No Terrain found in scene.");
			return;
		}

		var storage = TerrainTarget.Storage;
		if (storage == null || storage.HeightMap == null) return;

		// 1. Setup Parameters 
		int resolution = storage.Resolution;
		float terrainSize = storage.TerrainSize;
		float terrainMaxHeight = storage.TerrainHeight;
		float halfSize = terrainSize * 0.5f;

		// Calculate bounds including falloff
		float boundSize = (Shape == IntersectionShape.Rectangle ? Math.Max(Width, Length) * 0.5f : Radius) + TerrainFalloffRadius;
		BBox worldBounds = new BBox(WorldPosition - new Vector3(boundSize), WorldPosition + new Vector3(boundSize));

		var heightMap = storage.HeightMap;

		// Capture initial state for Undo
		bool hasModified = false;

		// Initialize buffers for height calculation 
		var updatedHeights = new float[heightMap.Length];
		var bestDistance = new float[heightMap.Length];

		for (int i = 0; i < heightMap.Length; i++)
		{
			// Decode: Map [0..1] ushort to [0 .. MaxHeight] to match RoadComponent
			updatedHeights[i] = (heightMap[i] / (float)ushort.MaxValue) * terrainMaxHeight;
			bestDistance[i] = float.MaxValue;
		}

		BuildRectangleExitCorridors();

		// 2. Grid Traversal
		for (int ix = 0; ix < resolution; ix++)
		{
			for (int iy = 0; iy < resolution; iy++)
			{
				// 1. Adaptive coordinate detection (Center vs Corner) matching RoadComponent
				float nodeLocalX_corner = (ix / (float)(resolution - 1)) * terrainSize;
				float nodeLocalY_corner = (iy / (float)(resolution - 1)) * terrainSize;

				float nodeLocalX = nodeLocalX_corner;
				float nodeLocalY = nodeLocalY_corner;

				// Check if the intersection is in the centered range
				var checkPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);
				if (checkPos.x < 0f || checkPos.x > terrainSize || checkPos.y < 0f || checkPos.y > terrainSize)
				{
					nodeLocalX = nodeLocalX_corner - halfSize;
					nodeLocalY = nodeLocalY_corner - halfSize;
				}

				Vector3 pixelWorldPos = TerrainTarget.Transform.World.PointToWorld(new Vector3(nodeLocalX, nodeLocalY, 0));

				if (!worldBounds.Contains(pixelWorldPos)) continue;

				int index = iy * resolution + ix;

				// 2. Distance to intersection shape 
				Vector3 relativePos = WorldTransform.PointToLocal(pixelWorldPos);
				float distance = GetDistanceToIntersectionShape(relativePos.WithZ(0));

				if (distance > TerrainFalloffRadius) continue;

				// 3. Target height matching RoadComponent (0 to MaxHeight range)
				Vector3 intersectionLocalPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);
				float roadSurfaceHeight = Math.Clamp(intersectionLocalPos.z + TerrainHeightOffset, 0f, terrainMaxHeight);
				float roadInsetHeight = Math.Clamp(roadSurfaceHeight - TerrainRoadInset, 0f, terrainMaxHeight);
				float currentPixelHeight = (heightMap[index] / (float)ushort.MaxValue) * terrainMaxHeight;

				float candidateHeight;
				if (distance <= 0) // Inside the intersection — sink terrain below road to prevent Z-fighting
				{
					candidateHeight = roadInsetHeight;
				}
				else if (SidewalkWidth > 0f && distance <= SidewalkWidth) // Sidewalk ring — flush with road surface
				{
					candidateHeight = roadSurfaceHeight;
				}
				else // Falloff — blend from surface/inset back to original terrain
				{
					float transitionStart = SidewalkWidth > 0f ? SidewalkWidth : 0f;
					float transitionBaseHeight = SidewalkWidth > 0f ? roadSurfaceHeight : roadInsetHeight;
					float t = Math.Clamp((distance - transitionStart) / TerrainFalloffRadius, 0f, 1f);
					float smoothT = t * t * (3f - 2f * t);
					candidateHeight = MathX.Lerp(transitionBaseHeight, currentPixelHeight, smoothT);
				}

				if (distance < bestDistance[index])
				{
					bestDistance[index] = distance;
					updatedHeights[index] = candidateHeight;
					hasModified = true;
				}
			}
		}

		if (hasModified)
		{
			// 4. Final encoding to ushort (Mapping back to 0..1 without the 0.5 offset)
			for (int i = 0; i < heightMap.Length; i++)
			{
				heightMap[i] = (ushort)MathF.Round(Math.Clamp(updatedHeights[i], 0f, terrainMaxHeight) / terrainMaxHeight * ushort.MaxValue);
			}

			storage.HeightMap = heightMap;
			storage.StateHasChanged();
			TerrainTarget.Create();
		}
	}

	public void PaintTerrainToIntersection()
	{
		if (!TerrainTarget.IsValid() || TerrainEdgeMaterials == null || TerrainEdgeMaterials.Length == 0) return;

		var storage = TerrainTarget.Storage;
		if (storage == null || storage.ControlMap == null) return;

		int resolution = storage.Resolution;
		float terrainSize = storage.TerrainSize;
		float halfSize = terrainSize * 0.5f;

		// Identify all material indices in the terrain storage 
		bool materialsAdded = false;
		var materialIndices = new int[TerrainEdgeMaterials.Length];
		for (int m = 0; m < TerrainEdgeMaterials.Length; m++)
		{
			if (TerrainEdgeMaterials[m] == null) continue;

			int idx = storage.Materials.IndexOf(TerrainEdgeMaterials[m]);
			if (idx == -1)
			{
				storage.Materials.Add(TerrainEdgeMaterials[m]);
				idx = storage.Materials.Count - 1;
				materialsAdded = true;
			}

			if (idx > 31)
			{
				Log.Error($"RoadTool: Terrain has too many materials ({idx}). Material '{TerrainEdgeMaterials[m].ResourceName}' cannot be painted.");
				idx = 0;
			}

			materialIndices[m] = idx;
		}

		if (materialsAdded)
		{
			storage.StateHasChanged();
			TerrainTarget.Create();
		}

		float boundSize = (Shape == IntersectionShape.Rectangle ? Math.Max(Width, Length) * 0.5f : Radius) + TerrainEdgeRadius; // This line is unchanged 
		BBox worldBounds = new BBox(WorldPosition - new Vector3(boundSize), WorldPosition + new Vector3(boundSize)); // This line is unchanged

		BuildRectangleExitCorridors();

		var controlMap = storage.ControlMap;
		bool hasModified = false;

		for (int ix = 0; ix < resolution; ix++)
		{
			for (int iy = 0; iy < resolution; iy++)
			{
				float nodeLocalX = (ix / (float)(resolution - 1)) * terrainSize;
				float nodeLocalY = (iy / (float)(resolution - 1)) * terrainSize;

				var checkPos = TerrainTarget.Transform.World.PointToLocal(WorldPosition);
				if (checkPos.x < 0f || checkPos.x > terrainSize || checkPos.y < 0f || checkPos.y > terrainSize)
				{
					nodeLocalX -= halfSize;
					nodeLocalY -= halfSize;
				}

				Vector3 pixelWorldPos = TerrainTarget.Transform.World.PointToWorld(new Vector3(nodeLocalX, nodeLocalY, 0));
				if (!worldBounds.Contains(pixelWorldPos)) continue;

				Vector3 relativePos = WorldTransform.PointToLocal(pixelWorldPos);
				float distance = GetDistanceToIntersectionShape(relativePos.WithZ(0));

				if (distance > TerrainEdgeRadius) continue;

				int index = iy * resolution + ix;
				float t = Math.Clamp(distance / TerrainEdgeRadius, 0f, 1f);
				float blendStrength = TerrainEdgeBlendGradient.Evaluate(t).a;

				if (blendStrength > 0.01f)
				{
					// Add deterministic noise to blend textures together (Dithering)
					float pixelNoise = ((float)((index * 1103515245 + 12345) & 0x7FFFFFFF) / 0x7FFFFFFF) * TerrainTextureNoise - (TerrainTextureNoise * 0.5f);
					float noisyT = Math.Clamp(t + pixelNoise, 0f, 1f);
					float noisyDistance = distance + (pixelNoise * TerrainEdgeRadius);

					int materialIndex;
					if (noisyDistance <= 0)
					{
						materialIndex = materialIndices[0];
					}
					else
					{
						int edgeMatCount = materialIndices.Length - 1;
						// Using noisyT for index selection 
						int edgeIdx = edgeMatCount > 0 ? Math.Clamp((int)(noisyT * edgeMatCount), 0, edgeMatCount - 1) + 1 : 0;
						materialIndex = materialIndices[edgeIdx];
					}

					uint packed = controlMap[index];
					var mat = new CompactTerrainMaterial(packed);

					if (TerrainTargetLayer == TerrainTextureLayer.Base)
					{
						mat.BaseTextureId = (byte)materialIndex;
						mat.BlendFactor = (byte)MathX.Lerp(mat.BlendFactor, 0, blendStrength);
					}
					else
					{
						// Otherwise, we place it in Overlay and increase the BlendFactor to display it
						mat.OverlayTextureId = (byte)materialIndex;
						mat.BlendFactor = (byte)MathX.Lerp(mat.BlendFactor, 255, blendStrength);
					}

					controlMap[index] = mat.Packed;
					hasModified = true;
				}
			}
		}

		if (hasModified)
		{
			storage.ControlMap = controlMap;
			storage.StateHasChanged();
			TerrainTarget.SyncGPUTexture();
		}
	}

	// Per-opening exit corridors in local space, rebuilt once per flatten so the per-pixel distance test stays cheap.
	// Each entry is an opening's road-edge centre, its outward direction, its lateral direction, and half its width.
	private (Vector3 Center, Vector3 Outward, Vector3 Lateral, float Half)[] m_RectangleExitCorridors = Array.Empty<(Vector3, Vector3, Vector3, float)>();

	private void BuildRectangleExitCorridors()
	{
		if (Shape != IntersectionShape.Rectangle)
		{
			m_RectangleExitCorridors = Array.Empty<(Vector3, Vector3, Vector3, float)>();
			return;
		}

		EnsureRectangleExits();

		m_RectangleExitCorridors = Exits
			.Where(exit => exit != null)
			.Select(exit =>
			{
				Transform t = GetRectangleExitLocalTransform(exit.Side, false, exit.Offset);
				return (t.Position, t.Rotation.Forward, t.Rotation.Right, exit.Width * 0.5f);
			})
			.ToArray();
	}



	private float GetDistanceToIntersectionShape(Vector3 localPixelPos)
	{
		if (Shape == IntersectionShape.Rectangle)
		{
			float hl = Length * 0.5f;
			float hw = Width * 0.5f;
			float dx = MathF.Max(MathF.Abs(localPixelPos.x) - hl, 0);
			float dy = MathF.Max(MathF.Abs(localPixelPos.y) - hw, 0);
			float dist = MathF.Sqrt(dx * dx + dy * dy);

			// Treat each open exit's corridor as inside, so terrain doesn't poke up through a road opening. A corridor is
			// the band beyond an opening's road edge (outward) and within that opening's width (lateral) — one per opening.
			if (dist > 0)
			{
				foreach (var corridor in m_RectangleExitCorridors)
				{
					Vector3 toPixel = localPixelPos - corridor.Center;

					if (Vector3.Dot(toPixel, corridor.Outward) >= 0.0f && MathF.Abs(Vector3.Dot(toPixel, corridor.Lateral)) <= corridor.Half)
						return 0;
				}
			}

			return dist;
		}

		// Circle
		float radDist = MathF.Max(localPixelPos.WithZ(0).Length - Radius, 0);

		if (radDist > 0 && CircleExits != null && CircleExits.Length > 0)
		{
			Vector3 pixelDir = localPixelPos.WithZ(0);
			if (pixelDir.LengthSquared > 0.0001f)
				pixelDir = pixelDir.Normal;

			foreach (var exit in CircleExits)
			{
				// Use dot product to stay independent of angle conventions
				Vector3 exitDir = Rotation.FromYaw(exit.AngleDegrees).Forward;
				float cosHalfAngle = MathF.Cos(MathF.Atan(exit.RoadWidth / Radius));
				if (Vector3.Dot(pixelDir, exitDir) >= cosHalfAngle)
					return 0;
			}
		}

		return radDist;
	}
}
using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool;

public partial class RoadIntersectionComponent
{
	/// <summary>
	/// A single drivable exit of an intersection, expressed in world space.
	/// <see cref="Transform"/>.Forward points outward (away from the intersection), matching the snap targets.
	/// </summary>
	public readonly struct TrafficExit
	{
		public Transform Transform { get; init; }
		public float RoadWidth { get; init; }
	}

	/// <summary>
	/// When enabled, this intersection is ignored by the traffic system: vehicles will not route through it.
	/// </summary>
	[Property, Feature("General"), Category("Traffic"), Order(2)] public bool ExcludeTraffic { get; set; } = false;

	/// <summary>Speed limit for traffic crossing this intersection, in km/h.</summary>
	[Property, Feature("General"), Category("Traffic"), Order(2), Range(5.0f, 130.0f)] public float SpeedLimit { get; set; } = 30.0f;



	/// <summary>
	/// Enumerates every active exit of this intersection (rectangle or circle) as a world transform plus road width.
	/// These are the same outer-edge positions that <see cref="SnapNearbyRoads"/> snaps roads to, so the traffic
	/// graph can match road endpoints against them by proximity.
	/// </summary>
	public List<TrafficExit> GetTrafficExits()
	{
		var exits = new List<TrafficExit>();

		if (Shape == IntersectionShape.Rectangle)
		{
			EnsureRectangleExits();

			foreach (var exit in Exits)
			{
				if (exit is null)
					continue;

				exits.Add(new TrafficExit
				{
					Transform = GetRectangleExitTransform(exit.Side, true, exit.Offset),
					RoadWidth = exit.Width
				});
			}
		}
		else
		{
			var circleExits = CircleExits ?? Array.Empty<CircleExit>();

			for (int i = 0; i < circleExits.Length; i++)
			{
				exits.Add(new TrafficExit
				{
					Transform = GetCircleExitTransform(i, true),
					RoadWidth = circleExits[i].RoadWidth
				});
			}
		}

		return exits;
	}
}
using System;

namespace RedSnail.RoadTool;

public partial class RoadIntersectionComponent
{
	// Computes a quadratic Bezier control point at the intersection of the two tangent lines.
	// Returns true if the lines intersect, false if parallel (in which case the midpoint is used as a fallback).
	// The control distance along _StartTan is clamped to the chord length so asymmetric tangents (e.g. an
	// off-grid exit angle whose disc tangent points well past the outer corner) don't drive the bezier past
	// the outer endpoint — overshoot produces samples beyond the endpoint and flips downstream triangle winding.
	private static bool TryBezierControl(Vector3 _Start, Vector3 _StartTan, Vector3 _End, Vector3 _EndTan, out Vector3 _Control)
	{
		float det = _StartTan.x * _EndTan.y - _EndTan.x * _StartTan.y;

		if (MathF.Abs(det) < 0.0001f)
		{
			_Control = (_Start + _End) * 0.5f;
			return false;
		}

		Vector3 d = _End - _Start;
		float r = (d.x * _EndTan.y - _EndTan.x * d.y) / det;
		float rMax = d.Length;
		float rClamped = Math.Clamp(r, 0.0f, rMax);
		_Control = _Start + rClamped * _StartTan;
		return true;
	}

	private static Vector3 SampleQuadBezier(Vector3 _B0, Vector3 _B1, Vector3 _B2, float _T)
	{
		float u = 1.0f - _T;
		return u * u * _B0 + 2.0f * u * _T * _B1 + _T * _T * _B2;
	}
}
using System;
using Sandbox;

namespace RedSnail.RoadTool;

/// <summary>
/// A primitive control surface the traffic AI (<see cref="TrafficVehicle"/>) uses to drive ONE vehicle. It's a plain
/// bag of delegates — there is no interface for a vehicle controller to implement, and your vehicle code never needs
/// to reference this library.
///
/// The seam is filled in by whoever uses both this tool AND a vehicle controller — i.e. your GAME — via
/// <see cref="RoadManager.ResolveVehicleDriver"/>. The game maps whatever its controller looks like onto these few
/// delegates. Any field left null is simply skipped. The demo wires <see cref="DemoCarController"/> automatically.
/// </summary>
public sealed class RoadVehicleDriver
{
	/// <summary>True while a player is at the wheel — the AI then hands this car over for good and never reclaims it.</summary>
	public Func<bool> IsPlayerDriving;

	/// <summary>The vehicle body's world velocity. The brain reads it to chase a target speed and to detect being jammed.</summary>
	public Func<Vector3> Velocity;

	/// <summary>Tell the controller whether the AI is currently driving this vehicle (vs parked / player-driven). Pushed every frame.</summary>
	public Action<bool> SetAiControlled;

	/// <summary>Push the AI's per-frame inputs: throttle and steer in [-1, 1] (steer + = left), plus handbrake.</summary>
	public Action<float, float, bool> Drive;

	/// <summary>Optional: max steering angle in degrees, used to widen the entity look-ahead toward where the car is turning.</summary>
	public Func<float> MaxSteering;
}
global using static Sandbox.Internal.GlobalGameNamespace;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Rendering;
[assembly: global::System.Reflection.AssemblyMetadata( "AddonTitle", "Reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "AddonIdent", "reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "OrgIdent", "igor" )]
[assembly: global::System.Reflection.AssemblyMetadata( "Ident", "igor.reactivity" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineVersion", "26" )]
[assembly: global::System.Reflection.AssemblyMetadata( "EngineMinorVersion", "1" )]

[assembly: System.Runtime.Versioning.TargetFramework( ".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0" )]
[assembly: global::System.Reflection.AssemblyMetadata( "CompileTime", "2026-06-27T21:09:21.7423840Z" )]
[assembly: global::System.Reflection.AssemblyVersion("0.0.111.0")]
[assembly: global::System.Reflection.AssemblyFileVersion("0.0.111.0")]
using System.Runtime.CompilerServices;

namespace Sandbox.Reactivity.Internals.Runtimes;

internal sealed class Runtime : IDisposable
{
	/// <summary>
	/// Effects that are waiting to run due to reactivity changes.
	/// </summary>
	private readonly Queue<Effect> _pendingEffects = new(16);

	/// <summary>
	/// How many effects have run during a flush operation.
	/// </summary>
	private uint _flushDepth;

	/// <summary>
	/// Whether pending effects are currently being run.
	/// </summary>
	private bool _isFlushing;

	/// <summary>
	/// The currently executing effect.
	/// </summary>
	public Effect? CurrentEffect { get; set; }

	/// <summary>
	/// The currently executing reaction.
	/// </summary>
	public IReaction? CurrentReaction { get; set; }

	/// <summary>
	/// A monotonically increasing counter that's incremented when an <see cref="IProducer" /> updates its current
	/// value.
	/// </summary>
	public uint Version { get; set; } = 1;

	/// <summary>
	/// Whether dependency tracking is currently disabled.
	/// </summary>
	public bool IsUntracking { get; set; }

	/// <summary>
	/// Whether a flush was scheduled to run at the end of the frame.
	/// </summary>
	public bool IsFlushScheduled { get; private set; }

	/// <summary>
	/// Whether an effect is currently executing its teardown function. This is used by producers to skip any
	/// recomputation when accessed to ensure the previous value is returned.
	/// </summary>
	public bool IsRunningTeardown { get; set; }

	public void Dispose()
	{
		_pendingEffects.Clear();
		CurrentEffect = null;
		CurrentReaction = null;
		Version = uint.MaxValue;
		IsUntracking = true;
		IsFlushScheduled = false;
		_isFlushing = false;
	}

	/// <summary>
	/// Returns the currently executing effect.
	/// </summary>
	/// <exception cref="InvalidOperationException">
	/// Thrown if there is no effect that is currently executing.
	/// </exception>
	public Effect EnsureCurrentEffect([CallerMemberName] string name = "Effect")
	{
		return CurrentEffect ?? throw new InvalidOperationException(name + " must be created in an effect root");
	}

	public void ScheduleEffect(Effect effect)
	{
		_pendingEffects.Enqueue(effect);

		if (!IsFlushScheduled && !_isFlushing)
		{
			IsFlushScheduled = true;
		}
	}

	/// <summary>
	/// Empties the queue of effects that are scheduled to run due to one of their dependencies changing. This should
	/// only be run when you want an effect to re-run immediately after changing a reactive value.
	/// </summary>
	public void Flush()
	{
		if (_isFlushing)
		{
			return;
		}

		_isFlushing = true;
		IsFlushScheduled = false;

		try
		{
			while (_pendingEffects.TryDequeue(out var effect))
			{
				if (_flushDepth++ > 1000)
				{
					_pendingEffects.Clear();
					_pendingEffects.TrimExcess(16);

#if DEBUG
					var exception = new InfiniteLoopException(_effectExecutions);
					OnFlushInfiniteLoop?.Invoke(exception);

					throw exception;
#else
					throw new InfiniteLoopException();
#endif
				}

				if (effect.ShouldRun)
				{
					effect.Run();
#if DEBUG
					_effectExecutions[effect] = _effectExecutions.GetValueOrDefault(effect) + 1;
#endif
				}
			}
		}
		finally
		{
			_flushDepth = 0;
			_isFlushing = false;
#if DEBUG
			_effectExecutions.Clear();
#endif
		}
	}

#if DEBUG
	/// <summary>
	/// Which effects have executing during the current flush, and how many times they've executed.
	/// </summary>
	private readonly Dictionary<Effect, int> _effectExecutions = [];

	/// <summary>
	/// Called when an infinite loop occurred during a flush.
	/// </summary>
	public static event Action<InfiniteLoopException>? OnFlushInfiniteLoop;
#endif
}
#if SANDBOX
using System.Diagnostics;
using Sandbox.Reactivity.Internals;
using Sandbox.UI;
using static Sandbox.Reactivity.Reactive;
#if JETBRAINS_ANNOTATIONS
using JetBrains.Annotations;
#endif

namespace Sandbox.Reactivity;

/// <summary>
/// The reactive counterpart to <see cref="Panel" /> that allows usage of reactive properties.
/// </summary>
/// <remarks>
/// Make sure you set up an effect root using <see cref="PanelRoot" /> at the top of your razor markup:
/// <code>
/// @{ using var _ = PanelRoot(); }
/// </code>
/// Engine limitations prevent this from being done automatically.
/// </remarks>
#if JETBRAINS_ANNOTATIONS
[PublicAPI]
#endif
public class ReactivePanel : Panel, IReactivePropertyContainer, IReactivePanel
{
	private Effect? _effectRoot;

	private Effect? _renderEffectRoot;

	private int _version;

	public ReactivePanel()
	{
		var parent = Runtime.CurrentEffect;

		_effectRoot = new Effect([StackTraceHidden] [DebuggerStepThrough]() =>
			{
				OnActivate();
				return null;
			},
			parent,
			false);

		_effectRoot.SetDebugInfo(DisplayInfo.For(this).Name,
			DisplayInfo.For(this).Icon,
			new CallLocation(GetType(), nameof(OnActivate)),
			parent ?? (object?)this);

		_effectRoot.Run();
	}

	Effect? IReactivePanel.RenderEffectRoot
	{
		get => _renderEffectRoot;
		set => _renderEffectRoot = value;
	}

	int IReactivePanel.Version
	{
		get => _version;
		set => _version = value;
	}

	Dictionary<int, IProducer> IReactivePropertyContainer.Producers { get; } = [];

	protected ReactivePanelScope PanelRoot()
	{
		return new ReactivePanelScope(this);
	}

	public sealed override void Delete(bool immediate = false)
	{
		_renderEffectRoot?.Dispose();
		_renderEffectRoot = null;

		_effectRoot?.Dispose();
		_effectRoot = null;

		base.Delete(immediate);
	}

	protected sealed override int BuildHash()
	{
		return _version;
	}

	/// <summary>
	/// Called inside an effect root when this panel is instantiated, allowing for effects to be created. When this
	/// panel is deleted, the effect root (and all of its descendants) are disposed.
	/// </summary>
	protected virtual void OnActivate()
	{
	}
}
#endif
#if SANDBOX
using Sandbox.Reactivity.Internals;
#if JETBRAINS_ANNOTATIONS
using JetBrains.Annotations;
#endif

// we can't wrap the BuildRenderTree method for razor components, so we need something that can set up the proper
// scope inside the markup itself

namespace Sandbox.Reactivity;

/// <summary>
/// A disposable that's used to enable reactivity for a <see cref="ReactivePanelComponent" /> or
/// <see cref="ReactivePanel" /> during rendering.
/// </summary>
#if JETBRAINS_ANNOTATIONS
[PublicAPI]
#endif
public readonly ref struct ReactivePanelScope : IDisposable
{
	private readonly Effect.ExecutionScope _executionScope;

	internal ReactivePanelScope(IReactivePanel panel)
	{
		if (panel.RenderEffectRoot is { } previousRoot)
		{
			// don't teardown previous root since we're already building the render tree by this point
			previousRoot.Dispose(false);
		}

		// nested panels don't render immediately when a containing panel's tree is rendering, so the parent is
		// always null anyway
		var effectRoot = new Effect(null, null, true, () => panel.Version++);
		effectRoot.SetDebugInfo(panel.GetType().ToSimpleString(false) + " (Render)",
			panel is ReactivePanel ? "view_quilt" : "monitor",
			new CallLocation(2),
			panel is ReactivePanel reactive ? reactive.GameObject?.GetComponent<IReactivePanel>() : panel);

		panel.RenderEffectRoot = effectRoot;
		_executionScope = new Effect.ExecutionScope(effectRoot);
	}

	public void Dispose()
	{
		_executionScope.Dispose();
	}
}
#endif
#if SANDBOX
namespace Sandbox.Reactivity.Internals;

/// <summary>
/// Maintains a list of types that are assignable to the given type.
/// </summary>
internal static class TypeHierarchy<T>
{
	/// <summary>
	/// All types that are assignable to <typeparamref name="T"/>.
	/// </summary>
	[SkipHotload]
	// ReSharper disable once StaticMemberInGenericType
	public static readonly IEnumerable<Type> Types;

	static TypeHierarchy()
	{
		// since this is most likely going to be used for simple event types, we're going to assume that the hierarchy
		// won't be very large and that checking a list would be faster than hashing for a set
		var next = typeof(T);
		var hierarchy = new List<Type>();

		while (next != null)
		{
			hierarchy.Add(next);

			foreach (var type in next.GetInterfaces())
			{
				if (!hierarchy.Contains(type))
				{
					hierarchy.Add(type);
				}
			}

			next = next.BaseType;

			if (next == typeof(object))
			{
				break;
			}
		}

		Types = hierarchy;
	}
}
#endif
#if JETBRAINS_ANNOTATIONS
#endif

namespace Sandbox.Reactivity;

/// <summary>
/// An object that contains a reactive value. Reading the value inside an effect will cause it to re-run when it
/// changes.
/// </summary>
/// <typeparam name="T">The type of value this object contains.</typeparam>
/// <remarks>
/// This can be used to abstract over a <see cref="State{T}" /> or <see cref="Derived{T}" /> as needed.
/// </remarks>
#if JETBRAINS_ANNOTATIONS
#endif
public interface IState<T>
{
	/// <summary>
	/// The current value.
	/// </summary>
	T Value { get; set; }
}

/// <inheritdoc cref="IState{T}" />
#if JETBRAINS_ANNOTATIONS
#endif
public interface IReadOnlyState<out T>
{
	/// <inheritdoc cref="IState{T}.Value" />
	T Value { get; }
}
using Sandbox;
using Sandbox.Volumes;

namespace RedSnail.WaterTool;

/// <summary>
/// Calms the water inside a volume: wave displacement (and the surface normals that
/// come from it) smoothly fade to flat. Affects every water surface — WaterQuad,
/// WaterBodyRenderer and WaterFlow — so it's the clean way to blend two of them
/// together. The classic use is a river mouth meeting an ocean: drop a calm volume
/// over the junction, set both surfaces to the same height there, and the wave
/// mismatch (ocean chop poking above the river, seams) disappears.
///
/// Purely visual — it doesn't touch buoyancy, swimming or the flow current.
/// </summary>
[Title("Water Calm Volume")]
[Category("Volumes")]
[Icon("water")]
public sealed class WaterCalmVolume : VolumeComponent, Component.ExecuteInEditor
{
	// 0 = no effect, 1 = perfectly flat at the core. Lets a volume only partially
	// settle the water if you want some residual motion.
	[Property, Range(0.0f, 1.0f)] public float Strength { get; set; } = 1.0f;

	// Fraction of the volume (from each face inward) over which the calming ramps in.
	// 0 = hard edge (a visible crease), 1 = ramps all the way from the center.
	[Property, Range(0.05f, 1.0f)] public float Falloff { get; set; } = 0.4f;
	
	
	
	protected override void OnEnabled()
	{
		WaterManager.Current?.RefreshWaterCalmVolumesList();
	}
	
	protected override void OnDisabled()
	{
		WaterManager.Current?.RefreshWaterCalmVolumesList();
	}

	protected override void DrawGizmos()
	{
		base.DrawGizmos();

		if (!Gizmo.IsSelected)
			return;

		// Faint fill so calm volumes read differently from exclusion volumes
		BBox box = SceneVolume.GetBounds();

		Gizmo.Draw.Color = Color.Cyan.WithAlpha(0.06f);
		Gizmo.Draw.SolidBox(box);
	}

	public (Vector3 Center, Vector3 Forward, Vector3 Up, Vector3 HalfExtents) GetWorldOBB()
	{
		BBox local = SceneVolume.GetBounds();
		Vector3 center = WorldTransform.PointToWorld(local.Center);
		Vector3 halfExtents = local.Size * 0.5f;

		return (center, WorldRotation.Forward, WorldTransform.Up, halfExtents);
	}
}
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
using Sandbox.Rendering;
using RenderStage = Sandbox.Rendering.Stage;

namespace RedSnail.WaterTool;

[Title("Water Manager")]
public partial class WaterManager : Component, Component.ExecuteInEditor, Component.DontExecuteOnServer, IHotloadManaged
{
	private SceneCustomObject m_SceneObject;
	
	[SkipHotload] public static WaterManager Current { get; private set; } = null;
	
	[Property(Title = "Ocean"), Group("Profile"), Order(0)] public WaterDefinition OceanWaveProfile { get; set; }
	[Property(Title = "Lake"), Group("Profile")] public WaterDefinition LakeWaveProfile { get; set; }
	[Property(Title = "River"), Group("Profile")] public WaterDefinition RiverWaveProfile { get; set; }
	[Property(Title = "Pool"), Group("Profile")] public WaterDefinition PoolWaveProfile { get; set; }
	[Property(Title = "Custom"), Group("Profile")] public WaterDefinition CustomWaveProfile { get; set; }

	[Property(Title = "Underwater Volume"), Group("Post Processing")] public PostProcessVolume UnderwaterPostProcessVolume { get; set; }

	private ComputeShader m_ComputeShader;

	// Double-buffered command lists. We BUILD into the disabled "back" list on the main
	// thread (FinishUpdate); the camera EXECUTES the enabled "front" list on a render
	// worker thread. Because the recorded list and the executing list are never the same
	// instance in a frame, the engine never iterates a list while we're resetting it -
	// which is the multithreaded "CommandList was null" crash. Both stay attached to the
	// camera for its lifetime; each frame we just flip which one is Enabled.
	private CommandList m_CommandList = new("Water Quads");
	private Vector3 m_CameraPosition;
	private WaterDefinition m_DefaultProfile;

	private List<WaterQuad> Quads { get; } = [];
	private List<WaterBodyRenderer> QuadRenderers { get; } = [];
	public List<WaterBody> Bodies { get; } = [];
	public List<WaterFlow> Flows { get; } = [];
	public List<WaterExclusionVolume> ExclusionVolumes { get; } = [];
	public List<HullWaterExclusionVolume> HullExclusionVolumes { get; } = [];
	
	
	
	protected override void OnAwake()
	{
		Current = Scene.Get<WaterManager>();
		
		m_ComputeShader = new ComputeShader("water_clipmap_cs");

		m_DefaultProfile = new WaterDefinition();
	}
	
	
	
	protected override void OnEnabled()
	{
		m_SceneObject = new SceneCustomObject(Scene.SceneWorld)
		{
			RenderOverride = RenderAll,
			Transform = new Transform(Vector3.Zero, Rotation.Identity),
			Flags =
			{
				IsOpaque = false,
				IsTranslucent = true,
				WantsFrameBufferCopy = false,
				WantsPrePass = false
			}
		};
		
		Scene.Camera?.AddCommandList(m_CommandList, RenderStage.AfterTransparent);
		
		RefreshWaterQuadsList();
		RefreshWaterBodyRenderersList();
		RefreshWaterBodiesList();
		RefreshWaterExclusionVolumesList();
		RefreshWaterHullExclusionVolumesList();
	}
	
	
	
	protected override void OnDisabled()
	{
		m_SceneObject?.Delete();
		m_SceneObject = null;

		m_RippleBuffer?.Dispose();
		m_RippleBuffer = null;
	
		ClearCalmVolumes();
		
		Scene.Camera?.RemoveCommandList(m_CommandList);
	}
	
	
	
	void IHotloadManaged.Destroyed(Dictionary<string, object> _State)
	{
		_State["IsActive"] = Current == this;
	}



	void IHotloadManaged.Created(IReadOnlyDictionary<string, object> _State)
	{
		if (_State.GetValueOrDefault("IsActive") is true)
			Current = this;
	}
	
	
	
	private void RenderAll(SceneObject _)
	{
		if (Graphics.LayerType != SceneLayerType.Translucent)
			return;
		
		m_CommandList.Reset();

		bool hasAnythingToRender = false;

		foreach (var renderer in QuadRenderers)
		{
			if (!renderer.IsValid() || !renderer.ParticipatesInRendering)
				continue;

			hasAnythingToRender = true;
			renderer.RecordCompute(m_CommandList, m_ComputeShader, m_CameraPosition);
		}
		
		foreach (var quad in Quads)
		{
			if (!quad.IsValid() || !quad.ParticipatesInRendering)
				continue;

			hasAnythingToRender = true;
			quad.RecordCompute(m_CommandList, m_ComputeShader, m_CameraPosition);
		}
		
		// Flows build their mesh on the CPU (no compute pass or barrier needed)
		foreach (var flow in Flows)
		{
			if (!flow.IsValid() || !flow.ParticipatesInRendering)
				continue;

			hasAnythingToRender = true;
		}

		if (hasAnythingToRender)
		{
			foreach (var renderer in QuadRenderers)
			{
				if (!renderer.IsValid() || !renderer.ParticipatesInRendering)
					continue;

				renderer.BarrierTransition(m_CommandList);
			}

			foreach (var quad in Quads)
			{
				if (!quad.IsValid() || !quad.ParticipatesInRendering)
					continue;

				quad.BarrierTransition(m_CommandList);
			}

			m_CommandList.Attributes.GrabFrameTexture("FrameBufferCopyTexture");

			foreach (var renderer in QuadRenderers)
			{
				if (!renderer.IsValid() || !renderer.ParticipatesInRendering)
					continue;

				renderer.Draw(m_CommandList);
			}
			
			foreach (var quad in Quads)
			{
				if (!quad.IsValid() || !quad.ParticipatesInRendering)
					continue;

				quad.Draw(m_CommandList);
			}
			
			foreach (var flow in Flows)
			{
				if (!flow.IsValid() || !flow.ParticipatesInRendering)
					continue;

				flow.Draw(m_CommandList);
			}
		}
	}
	
	
	
	protected override void OnUpdate()
	{
		// We've to make sure it's always correct while in the editor
		// (S&box is a complete mess when it comes to managing a singleton properly on a component that execute in the editor, bcs its reference get constantly swapped between
		// gameplay and editor, we've to do this non sense !)
		if (Scene.IsEditor)
			Current = Scene.Get<WaterManager>();
		
		if (Game.IsPlaying)
		{
			m_CameraPosition = Scene.Camera?.WorldPosition ?? Vector3.Zero;
		}
		else
		{
			m_CameraPosition = Application.Editor.Camera.WorldPosition;
		}

		if (UnderwaterPostProcessVolume.IsValid())
			UnderwaterPostProcessVolume.Enabled = IsPositionInsideAny(m_CameraPosition);

		UpdateRipples();
		UpdateCalmVolumes();
	}

	/// <summary>
	/// We have to do all this non sense bcs using a Register/Unregister logic with OnEnabled/OnDisabled is a complete
	/// mess to manage when we enter play mode/stop play mode in the editor, the references get duplicated etc... Otherwise we've to check by gameobject id...
	/// It's just way too annoying, refreshing the whole list is safer and we're always sure to have the proper count of components
	/// </summary>
	public void RefreshWaterQuadsList()
	{
		if (!Scene.IsValid()) // S&box make this null while stopping play mode and entering back the editor mode (We need to guard this)
			return;
		
		Quads.Clear();
		Quads.AddRange(Scene.GetAll<WaterQuad>());
	}

	public void RefreshWaterBodyRenderersList()
	{
		if (!Scene.IsValid()) // S&box make this null while stopping play mode and entering back the editor mode (We need to guard this)
			return;
		
		QuadRenderers.Clear();
		QuadRenderers.AddRange(Scene.GetAll<WaterBodyRenderer>());
	}

	public void RefreshWaterBodiesList()
	{
		if (!Scene.IsValid()) // S&box make this null while stopping play mode and entering back the editor mode (We need to guard this)
			return;
		
		Bodies.Clear();
		Bodies.AddRange(Scene.GetAll<WaterBody>());
	}
	
	public void RefreshWaterFlowsList()
	{
		if (!Scene.IsValid()) // S&box make this null while stopping play mode and entering back the editor mode (We need to guard this)
			return;
		
		Flows.Clear();
		Flows.AddRange(Scene.GetAll<WaterFlow>());
	}

	public void RefreshWaterExclusionVolumesList()
	{
		if (!Scene.IsValid()) // S&box make this null while stopping play mode and entering back the editor mode (We need to guard this)
			return;
		
		ExclusionVolumes.Clear();
		ExclusionVolumes.AddRange(Scene.GetAll<WaterExclusionVolume>());
	}

	public void RefreshWaterHullExclusionVolumesList()
	{
		if (!Scene.IsValid()) // S&box make this null while stopping play mode and entering back the editor mode (We need to guard this)
			return;
		
		HullExclusionVolumes.Clear();
		HullExclusionVolumes.AddRange(Scene.GetAll<HullWaterExclusionVolume>());
	}

	private WaterDefinition GetWaveProfileForType(WaterBodyType waterType) => waterType switch
	{
		WaterBodyType.Ocean => OceanWaveProfile,
		WaterBodyType.Lake => LakeWaveProfile,
		WaterBodyType.River => RiverWaveProfile,
		WaterBodyType.Pool => PoolWaveProfile,
		_ => CustomWaveProfile
	};

	public static WaterDefinition GetWaveProfile(WaterBodyType _WaterType)
	{
		if (Current == null)
			return null;

		WaterDefinition profile = Current.GetWaveProfileForType(_WaterType);

		if (profile.IsValid())
			return profile;

		Log.Warning("[WaterTool] No water profile found in the 'Water Manager', please add a water profile for the specified water type ! (Project Settings > Water Manager > 'Assign the profiles')");

		return Current.m_DefaultProfile;
	}
}
namespace AutoRig.Dl.Nn;

using AutoRig.Dl;

/// <summary>
/// Transformer building blocks over <see cref="Tensor"/> ([N, C] rows), matching
/// PyTorch inference semantics (eps defaults, tanh-approx GELU where models use it).
/// Foundation for the UniRig-family ports (plan 8).
/// </summary>
public static class TransformerOps
{
    /// <summary>LayerNorm over the last dim: (x - mean) / sqrt(var + eps) * g + b.</summary>
    public static Tensor LayerNorm( Tensor x, Tensor gamma, Tensor beta, float eps = 1e-5f )
    {
        var (rows, cols) = (x.Shape[0], x.Shape[1]);
        var result = new float[x.Count];
        for ( var r = 0; r < rows; r++ )
        {
            float mean = 0;
            for ( var c = 0; c < cols; c++ )
                mean += x.Data[r * cols + c];
            mean /= cols;
            float variance = 0;
            for ( var c = 0; c < cols; c++ )
            {
                var d = x.Data[r * cols + c] - mean;
                variance += d * d;
            }
            variance /= cols;
            var inv = 1f / MathF.Sqrt( variance + eps );
            for ( var c = 0; c < cols; c++ )
                result[r * cols + c] =
                    (x.Data[r * cols + c] - mean) * inv * (gamma?.Data[c] ?? 1f)
                    + (beta?.Data[c] ?? 0f);   // null = no affine (DiT final norm)
        }
        return Tensor.From( result, rows, cols );
    }

    /// <summary>RMSNorm: x / rms(x) * g (LLaMA-style, no mean subtraction).</summary>
    public static Tensor RmsNorm( Tensor x, Tensor gamma, float eps = 1e-6f )
    {
        var (rows, cols) = (x.Shape[0], x.Shape[1]);
        var result = new float[x.Count];
        for ( var r = 0; r < rows; r++ )
        {
            float meanSquare = 0;
            for ( var c = 0; c < cols; c++ )
            {
                var v = x.Data[r * cols + c];
                meanSquare += v * v;
            }
            meanSquare /= cols;
            var inv = 1f / MathF.Sqrt( meanSquare + eps );
            for ( var c = 0; c < cols; c++ )
                result[r * cols + c] = x.Data[r * cols + c] * inv * gamma.Data[c];
        }
        return Tensor.From( result, rows, cols );
    }

    /// <summary>Exact GELU: x · Φ(x) via erf (PyTorch nn.GELU default).</summary>
    public static Tensor Gelu( Tensor x )
    {
        var result = new float[x.Count];
        for ( var i = 0; i < x.Count; i++ )
            result[i] = x.Data[i] * 0.5f * (1f + Erf( x.Data[i] * 0.70710678f ));
        return Tensor.From( result, x.Shape );
    }

    /// <summary>Tanh-approximated GELU (PyTorch approximate="tanh" / GPT-style).</summary>
    public static Tensor GeluTanh( Tensor x )
    {
        var result = new float[x.Count];
        for ( var i = 0; i < x.Count; i++ )
        {
            var v = x.Data[i];
            result[i] = 0.5f * v * (1f + MathF.Tanh(
                0.7978845608f * (v + 0.044715f * v * v * v) ));
        }
        return Tensor.From( result, x.Shape );
    }

    /// <summary>SiLU / swish: x · sigmoid(x).</summary>
    public static Tensor Silu( Tensor x )
    {
        var result = new float[x.Count];
        for ( var i = 0; i < x.Count; i++ )
            result[i] = x.Data[i] / (1f + MathF.Exp( -x.Data[i] ));
        return Tensor.From( result, x.Shape );
    }

    /// <summary>
    /// Multi-head scaled-dot-product attention. q: [Nq, H·D], k/v: [Nk, H·D]
    /// (already projected). Causal masks query i from keys j &gt; i + (Nk − Nq)
    /// (standard KV-cache offset). Returns [Nq, H·D].
    /// </summary>
    public static Tensor Attention( Tensor q, Tensor k, Tensor v, int heads, bool causal )
    {
        var nq = q.Shape[0];
        var nk = k.Shape[0];
        var headDim = q.Shape[1] / heads;
        var scale = 1f / MathF.Sqrt( headDim );
        var offset = nk - nq;
        var result = new float[nq * heads * headDim];

        void Head( int h )
        {
            var scores = new float[nk];   // per-worker buffer
            var headBase = h * headDim;
            for ( var i = 0; i < nq; i++ )
            {
                var limit = causal ? Math.Min( nk, i + offset + 1 ) : nk;
                var max = float.MinValue;
                for ( var j = 0; j < limit; j++ )
                {
                    float dot = 0;
                    for ( var d = 0; d < headDim; d++ )
                        dot += q.Data[i * heads * headDim + headBase + d]
                             * k.Data[j * heads * headDim + headBase + d];
                    scores[j] = dot * scale;
                    if ( scores[j] > max )
                        max = scores[j];
                }
                float total = 0;
                for ( var j = 0; j < limit; j++ )
                {
                    scores[j] = MathF.Exp( scores[j] - max );
                    total += scores[j];
                }
                for ( var j = 0; j < limit; j++ )
                {
                    var weight = scores[j] / total;
                    if ( weight == 0f )
                        continue;
                    for ( var d = 0; d < headDim; d++ )
                        result[i * heads * headDim + headBase + d] +=
                            weight * v.Data[j * heads * headDim + headBase + d];
                }
            }
        }

        // Heads write disjoint column ranges — safe to parallelize; skip the
        // overhead for tiny single-token decode steps.
        if ( (long)nq * nk * headDim >= 1 << 16 )
            Concurrency.For( 0, heads, Head );
        else
            for ( var h = 0; h < heads; h++ )
                Head( h );
        return Tensor.From( result, nq, heads * headDim );
    }

    /// <summary>
    /// Rotary position embedding (LLaMA/Qwen half-split convention): for each head,
    /// x' = x·cos(θ) + rotate_half(x)·sin(θ) with rotate_half = (−x₂, x₁) over the
    /// half-dim split; θ_i = pos · base^(−2i/d).
    /// </summary>
    public static Tensor Rope( Tensor x, int heads, int[] positions, float thetaBase = 1e6f )
    {
        var rows = x.Shape[0];
        var headDim = x.Shape[1] / heads;
        var half = headDim / 2;
        var result = new float[x.Count];
        var invFreq = new float[half];
        for ( var i = 0; i < half; i++ )
            invFreq[i] = MathF.Pow( thetaBase, -2f * i / headDim );

        for ( var r = 0; r < rows; r++ )
            for ( var h = 0; h < heads; h++ )
            {
                var at = r * heads * headDim + h * headDim;
                for ( var i = 0; i < half; i++ )
                {
                    var angle = positions[r] * invFreq[i];
                    var (sin, cos) = (MathF.Sin( angle ), MathF.Cos( angle ));
                    var a = x.Data[at + i];
                    var b = x.Data[at + half + i];
                    result[at + i] = a * cos - b * sin;
                    result[at + half + i] = b * cos + a * sin;
                }
            }
        return Tensor.From( result, rows, heads * headDim );
    }

    /// <summary>
    /// Grouped-query attention: q has <paramref name="qHeads"/>, k/v have
    /// <paramref name="kvHeads"/> (each kv head serves qHeads/kvHeads queries).
    /// Same causal/KV-cache-offset semantics as <see cref="Attention"/>.
    /// </summary>
    public static Tensor AttentionGqa(
        Tensor q, Tensor k, Tensor v, int qHeads, int kvHeads, bool causal )
    {
        var nq = q.Shape[0];
        var nk = k.Shape[0];
        var headDim = q.Shape[1] / qHeads;
        var scale = 1f / MathF.Sqrt( headDim );
        var offset = nk - nq;
        var group = qHeads / kvHeads;
        var result = new float[nq * qHeads * headDim];

        void Head( int h )
        {
            var scores = new float[nk];
            var kvHead = h / group;
            var qBase = h * headDim;
            var kvBase = kvHead * headDim;
            for ( var i = 0; i < nq; i++ )
            {
                var limit = causal ? Math.Min( nk, i + offset + 1 ) : nk;
                var max = float.MinValue;
                for ( var j = 0; j < limit; j++ )
                {
                    float dot = 0;
                    for ( var d = 0; d < headDim; d++ )
                        dot += q.Data[i * qHeads * headDim + qBase + d]
                             * k.Data[j * kvHeads * headDim + kvBase + d];
                    scores[j] = dot * scale;
                    if ( scores[j] > max )
                        max = scores[j];
                }
                float total = 0;
                for ( var j = 0; j < limit; j++ )
                {
                    scores[j] = MathF.Exp( scores[j] - max );
                    total += scores[j];
                }
                for ( var j = 0; j < limit; j++ )
                {
                    var weight = scores[j] / total;
                    if ( weight == 0f )
                        continue;
                    for ( var d = 0; d < headDim; d++ )
                        result[i * qHeads * headDim + qBase + d] +=
                            weight * v.Data[j * kvHeads * headDim + kvBase + d];
                }
            }
        }

        if ( (long)nq * nk * headDim >= 1 << 16 )
            Concurrency.For( 0, qHeads, Head );
        else
            for ( var h = 0; h < qHeads; h++ )
                Head( h );
        return Tensor.From( result, nq, qHeads * headDim );
    }

    /// <summary>Per-head RMSNorm over each head's slice (Qwen3 q_norm/k_norm).</summary>
    public static Tensor RmsNormPerHead( Tensor x, Tensor gamma, int heads, float eps = 1e-6f )
    {
        var rows = x.Shape[0];
        var headDim = x.Shape[1] / heads;
        var result = new float[x.Count];
        for ( var r = 0; r < rows; r++ )
            for ( var h = 0; h < heads; h++ )
            {
                var at = r * heads * headDim + h * headDim;
                float meanSquare = 0;
                for ( var d = 0; d < headDim; d++ )
                    meanSquare += x.Data[at + d] * x.Data[at + d];
                var inv = 1f / MathF.Sqrt( meanSquare / headDim + eps );
                for ( var d = 0; d < headDim; d++ )
                    result[at + d] = x.Data[at + d] * inv * gamma.Data[d];
            }
        return Tensor.From( result, rows, heads * headDim );
    }

    /// <summary>SwiGLU feed-forward core: silu(gate) ⊙ up (caller applies down proj).</summary>
    public static Tensor SwiGlu( Tensor gate, Tensor up )
    {
        var result = new float[gate.Count];
        for ( var i = 0; i < gate.Count; i++ )
            result[i] = gate.Data[i] / (1f + MathF.Exp( -gate.Data[i] )) * up.Data[i];
        return Tensor.From( result, gate.Shape );
    }

    /// <summary>Embedding lookup: ids → rows of the [V, C] table.</summary>
    public static Tensor Embed( Tensor table, int[] ids )
    {
        var cols = table.Shape[1];
        var result = new float[ids.Length * cols];
        for ( var i = 0; i < ids.Length; i++ )
            Array.Copy( table.Data, ids[i] * cols, result, i * cols, cols );
        return Tensor.From( result, ids.Length, cols );
    }

    /// <summary>Greedy decode step: argmax of the final row of logits.</summary>
    public static int Argmax( Tensor logits )
    {
        var cols = logits.Shape[1];
        var lastRow = (logits.Shape[0] - 1) * cols;
        var best = 0;
        for ( var c = 1; c < cols; c++ )
            if ( logits.Data[lastRow + c] > logits.Data[lastRow + best] )
                best = c;
        return best;
    }

    /// <summary>Abramowitz–Stegun erf (max error ~1.5e-7, plenty for fp32 goldens).</summary>
    internal static float Erf( float x )
    {
        var sign = x < 0 ? -1f : 1f;
        x = MathF.Abs( x );
        var t = 1f / (1f + 0.3275911f * x);
        var y = 1f - (((((1.061405429f * t - 1.453152027f) * t) + 1.421413741f) * t
            - 0.284496736f) * t + 0.254829592f) * t * MathF.Exp( -x * x );
        return sign * y;
    }
}
namespace AutoRig.Dl.Puppeteer;

using AutoRig.Dl;

/// <summary>Binds PartField from the weights embedded in Puppeteer's skin
/// checkpoint (point_embed.model.*, verified in skin_keys.json).</summary>
public static class PartFieldLoader
{
    public static PartFieldModel Load( IReadOnlyDictionary<string, Tensor> tensors, string prefix )
    {
        var pf = $"{prefix}point_embed.model.";
        var enc = $"{pf}pvcnn.pc_encoder.encoder.0.";
        var unet = $"{pf}pvcnn.unet_encoder.";
        var tri = $"{pf}triplane_transformer.";

        PartFieldModel.UnetBlock Block( string b, int cin, int cout ) => new()
        {
            Norm1G = Get( tensors, $"{b}norm1.weight" ),
            Norm1B = Get( tensors, $"{b}norm1.bias" ),
            Conv1W = Get( tensors, $"{b}conv1.weight" ),
            Conv1B = Get( tensors, $"{b}conv1.bias" ),
            NormMidG = Get( tensors, $"{b}norm_mid.weight" ),
            NormMidB = Get( tensors, $"{b}norm_mid.bias" ),
            AwareW = new[]
            {
                Get( tensors, $"{b}conv_3daware.plane_convs.0.weight" ),
                Get( tensors, $"{b}conv_3daware.plane_convs.1.weight" ),
                Get( tensors, $"{b}conv_3daware.plane_convs.2.weight" ),
            },
            AwareB = new[]
            {
                Get( tensors, $"{b}conv_3daware.plane_convs.0.bias" ),
                Get( tensors, $"{b}conv_3daware.plane_convs.1.bias" ),
                Get( tensors, $"{b}conv_3daware.plane_convs.2.bias" ),
            },
            Norm2G = Get( tensors, $"{b}norm2.weight" ),
            Norm2B = Get( tensors, $"{b}norm2.bias" ),
            Conv2W = Get( tensors, $"{b}conv2.weight" ),
            Conv2B = Get( tensors, $"{b}conv2.bias" ),
            ShortcutW = tensors.TryGetValue( $"{b}nin_shortcut.weight", out var sw ) ? sw : null,
            ShortcutB = tensors.TryGetValue( $"{b}nin_shortcut.bias", out var sb ) ? sb : null,
            CIn = cin,
            COut = cout,
        };

        var layers = new List<PartFieldModel.TriLayer>();
        while ( tensors.ContainsKey( $"{tri}transformer.layers.{layers.Count}.norm1.weight" ) )
        {
            var l = $"{tri}transformer.layers.{layers.Count}.";
            layers.Add( new PartFieldModel.TriLayer
            {
                Norm1G = Get( tensors, $"{l}norm1.weight" ),
                Norm1B = Get( tensors, $"{l}norm1.bias" ),
                InProj = Get( tensors, $"{l}self_attn.in_proj_weight" ),
                OutProj = Get( tensors, $"{l}self_attn.out_proj.weight" ),
                Norm2G = Get( tensors, $"{l}norm2.weight" ),
                Norm2B = Get( tensors, $"{l}norm2.bias" ),
                Mlp0W = Get( tensors, $"{l}mlp.0.weight" ),
                Mlp0B = Get( tensors, $"{l}mlp.0.bias" ),
                Mlp3W = Get( tensors, $"{l}mlp.3.weight" ),
                Mlp3B = Get( tensors, $"{l}mlp.3.bias" ),
            } );
        }
        if ( layers.Count == 0 )
            throw new FormatException( "PartField: no triplane transformer layers found." );

        var posEmbed = Get( tensors, $"{tri}pos_embed" );
        return new PartFieldModel
        {
            VoxConv0W = Get( tensors, $"{enc}voxel_layers.0.weight" ),
            VoxConv0B = Get( tensors, $"{enc}voxel_layers.0.bias" ),
            VoxConv3W = Get( tensors, $"{enc}voxel_layers.3.weight" ),
            VoxConv3B = Get( tensors, $"{enc}voxel_layers.3.bias" ),
            PointMlpW = Get( tensors, $"{enc}point_features.layers.0.weight" ),
            PointMlpB = Get( tensors, $"{enc}point_features.layers.0.bias" ),
            DownBlocks = new[]
            {
                Block( $"{unet}down_convs.0.block.", 256, 32 ),
                Block( $"{unet}down_convs.1.block.", 32, 64 ),
                Block( $"{unet}down_convs.2.block.", 64, 128 ),
            },
            UpBlocks = new[]
            {
                Block( $"{unet}up_convs.0.block.", 128 + 64, 64 ),
                Block( $"{unet}up_convs.1.block.", 64 + 32, 32 ),
            },
            UpNorm1G = new[]
            {
                Get( tensors, $"{unet}up_convs.0.norm1.weight" ),
                Get( tensors, $"{unet}up_convs.1.norm1.weight" ),
            },
            UpNorm1B = new[]
            {
                Get( tensors, $"{unet}up_convs.0.norm1.bias" ),
                Get( tensors, $"{unet}up_convs.1.norm1.bias" ),
            },
            UnetNormOutG = Get( tensors, $"{unet}norm_out.weight" ),
            UnetNormOutB = Get( tensors, $"{unet}norm_out.bias" ),
            UnetFinalW = Get( tensors, $"{unet}conv_final.weight" ),
            UnetFinalB = Get( tensors, $"{unet}conv_final.bias" ),
            Down0W = Get( tensors, $"{tri}downsampler.0.weight" ),
            Down0B = Get( tensors, $"{tri}downsampler.0.bias" ),
            Down3W = Get( tensors, $"{tri}downsampler.3.weight" ),
            Down3B = Get( tensors, $"{tri}downsampler.3.bias" ),
            PosEmbed = Tensor.From( posEmbed.Data, posEmbed.Data.Length / 1024, 1024 ),
            TriLayers = layers.ToArray(),
            TriNormG = Get( tensors, $"{tri}transformer.norm.weight" ),
            TriNormB = Get( tensors, $"{tri}transformer.norm.bias" ),
            UpsamplerW = Get( tensors, $"{tri}upsampler.weight" ),
            UpsamplerB = Get( tensors, $"{tri}upsampler.bias" ),
            Mlp0W = Get( tensors, $"{tri}mlp.0.weight" ),
            Mlp0B = Get( tensors, $"{tri}mlp.0.bias" ),
            Mlp2W = Get( tensors, $"{tri}mlp.2.weight" ),
            Mlp2B = Get( tensors, $"{tri}mlp.2.bias" ),
        };
    }

    internal static Tensor Get( IReadOnlyDictionary<string, Tensor> tensors, string key )
        => tensors.TryGetValue( key, out var tensor )
            ? tensor
            : throw new FormatException( $"Puppeteer skin checkpoint is missing tensor '{key}'." );
}
using AutoRig.Dl.RigNet;
using AutoRig.Mesh;

namespace AutoRig.Dl.UniRig;

using AutoRig.Dl;
using Vector3 = System.Numerics.Vector3;

/// <summary>
/// SkinTokens' input pipeline (checkpoint hparams predict_transform + the
/// encoder's eval path): bbox-normalize to [-1,1]³, SamplerMix a 54000-point
/// cloud (16384 vertex picks + area-weighted surface samples), then the
/// perceiver reduction with use_full_input=TRUE — the constant seed-0
/// 2048-subsample is fps'd down to 512 queries, but those queries cross-attend
/// against the WHOLE 54000-point cloud (unlike UniRig, which attends its
/// 4096-point subsample).
/// </summary>
public static class SkinTokensInput
{
    public const int NumSamples = 54000;
    public const int VertexSamples = 16384;
    public const int PreCount = 2048;    // token_num 512 · 4
    public const int LatentCount = 512;  // fps ratio 1/4 of the pre-cloud

    public sealed class Prepared
    {
        /// <summary>The FULL 54000-point cloud (perceiver "data", use_full_input).</summary>
        public required Vector3[] Points;
        public required Vector3[] Normals;

        /// <summary>fps picks (512) as indices into Points (perceiver queries).</summary>
        public required int[] SampledIndices;

        /// <summary>Undo the normalization: world = p * Scale + Center.</summary>
        public required Vector3 Center;
        public required float Scale;
    }

    public static Prepared Prepare( RigMesh mesh )
    {
        ArgumentNullException.ThrowIfNull( mesh );

        var (points, normals, center, scale) =
            UniRigInput.SampleCloud( mesh, NumSamples, VertexSamples );

        // fps runs over the seed-0 pre-subsample; its picks map back into the
        // full cloud (queries are literal rows of the data in the reference too).
        var pre = new float[PreCount * 3];
        for ( var i = 0; i < PreCount; i++ )
        {
            var p = points[SkinTokensSubsample.Indices[i]];
            pre[i * 3 + 0] = p.X;
            pre[i * 3 + 1] = p.Y;
            pre[i * 3 + 2] = p.Z;
        }
        var fps = PointNet.FarthestPointSample( Tensor.From( pre, PreCount, 3 ), ratio: 0.25f );
        var sampled = new int[fps.Length];
        for ( var i = 0; i < fps.Length; i++ )
            sampled[i] = SkinTokensSubsample.Indices[fps[i]];

        return new Prepared
        {
            Points = points,
            Normals = normals,
            SampledIndices = sampled,
            Center = center,
            Scale = scale,
        };
    }
}
namespace AutoRig.Dl.UniRig;

using Vector3 = System.Numerics.Vector3;

/// <summary>Decoded skeleton from a UniRig token sequence.</summary>
public sealed class UniRigSkeleton
{
    public required Vector3[] Joints;
    public required int[] Parents;       // -1 = root
    public required Vector3[] Tails;     // per-joint tail (leaf/branch tails extruded)
}

/// <summary>
/// UniRig's skeleton tokenizer (transcribed from src/tokenizer/tokenizer_part.py +
/// spec.make_skeleton, config tokenizer_parts_articulationxl_256): 256 coordinate
/// bins in [-1,1], BOS/EOS/branch/pad/spring/part/class tokens, the constrained-
/// decode state machine, and detokenization to joints/parents/tails.
/// </summary>
public sealed class UniRigTokenizer
{
    public const int NumDiscrete = 256;
    public const float RangeLo = -1f, RangeHi = 1f;

    public const int TokenBranch = NumDiscrete + 0;   // 256
    public const int TokenBos = NumDiscrete + 1;      // 257
    public const int TokenEos = NumDiscrete + 2;      // 258
    public const int TokenPad = NumDiscrete + 3;      // 259
    public const int TokenSpring = NumDiscrete + 4;   // 260
    public const int TokenPartBody = NumDiscrete + 5; // 261 (parts: body 0, hand 1)
    public const int TokenPartHand = NumDiscrete + 6; // 262
    public const int TokenClsNone = NumDiscrete + 7;  // 263
    public const int TokenClsVroid = NumDiscrete + 8;         // 264
    public const int TokenClsMixamo = NumDiscrete + 9;        // 265
    public const int TokenClsArticulationXl = NumDiscrete + 10; // 266
    public const int VocabSize = NumDiscrete + 11;    // 267

    public static int Discretize( float value )
    {
        var t = (value - RangeLo) / (RangeHi - RangeLo) * NumDiscrete;
        return Math.Clamp( (int)MathF.Round( t ), 0, NumDiscrete - 1 );
    }

    public static float Undiscretize( int bin )
        => (bin + 0.5f) / NumDiscrete * (RangeHi - RangeLo) + RangeLo;

    enum State
    {
        ExpectBos, ExpectClsOrPartOrJoint, ExpectPartOrJoint,
        ExpectJoint, ExpectJoint2, ExpectJoint3, ExpectBranchOrPartOrJoint,
    }

    static bool IsCls( int id ) => id is >= TokenClsVroid and <= TokenClsArticulationXl;
    static bool IsPart( int id ) => id is TokenPartBody or TokenPartHand;

    static State Advance( State state, int id ) => state switch
    {
        State.ExpectBos => State.ExpectClsOrPartOrJoint,
        State.ExpectClsOrPartOrJoint => id < NumDiscrete
            ? State.ExpectJoint2
            : id == TokenClsNone || IsCls( id )
                ? State.ExpectPartOrJoint
                : State.ExpectJoint,
        State.ExpectPartOrJoint => id < NumDiscrete ? State.ExpectJoint2 : State.ExpectPartOrJoint,
        State.ExpectJoint2 => State.ExpectJoint3,
        State.ExpectJoint3 => State.ExpectBranchOrPartOrJoint,
        State.ExpectBranchOrPartOrJoint => id == TokenBranch
            ? State.ExpectJoint
            : id < NumDiscrete
                ? State.ExpectJoint2
                : State.ExpectJoint,
        State.ExpectJoint => State.ExpectJoint2,
        _ => throw new FormatException( $"UniRig tokenizer: bad state {state}" ),
    };

    /// <summary>
    /// Allowed next tokens given the sequence so far — the constrained-decode mask
    /// (next_posible_token). Greedy argmax over logits restricted to this set
    /// reproduces UniRig's LogitsProcessor deterministically.
    /// </summary>
    public static List<int> NextPossibleTokens( IReadOnlyList<int> ids )
    {
        if ( ids.Count == 0 )
            return new List<int> { TokenBos };

        var state = State.ExpectBos;
        foreach ( var id in ids )
        {
            if ( state == State.ExpectBos && id != TokenBos )
                throw new FormatException( "UniRig tokenizer: sequence does not start with BOS." );
            state = Advance( state, id );
        }

        var allowed = new List<int>();
        void AddCls()
        {
            allowed.Add( TokenClsNone );
            allowed.Add( TokenClsVroid );
            allowed.Add( TokenClsMixamo );
            allowed.Add( TokenClsArticulationXl );
        }
        void AddPart()
        {
            allowed.Add( TokenSpring );
            allowed.Add( TokenPartBody );
            allowed.Add( TokenPartHand );
        }
        void AddJoint()
        {
            for ( var i = 0; i < NumDiscrete; i++ )
                allowed.Add( i );
        }

        switch ( state )
        {
            case State.ExpectClsOrPartOrJoint:
                AddCls(); AddPart(); AddJoint();
                break;
            case State.ExpectPartOrJoint:
                AddPart(); AddJoint(); allowed.Add( TokenEos );
                break;
            case State.ExpectJoint or State.ExpectJoint2 or State.ExpectJoint3:
                AddJoint();
                break;
            case State.ExpectBranchOrPartOrJoint:
                AddJoint(); AddPart(); allowed.Add( TokenBranch ); allowed.Add( TokenEos );
                break;
            default:
                throw new FormatException( $"UniRig tokenizer: bad decode state {state}" );
        }
        return allowed;
    }

    /// <summary>Completed bones in a (possibly partial) sequence — the reference's
    /// bones_in_sequence: a bone completes on its 3rd coordinate token, but a
    /// branch's FIRST triple (the re-stated parent position) does NOT count, so
    /// the result equals the number of joints.</summary>
    public static int BonesInSequence( IReadOnlyList<int> ids )
    {
        var bones = 0;
        var isBranch = false;
        var state = State.ExpectBos;
        foreach ( var id in ids )
        {
            if ( state == State.ExpectBos && id != TokenBos )
                throw new FormatException( "UniRig tokenizer: sequence does not start with BOS." );
            if ( state == State.ExpectBranchOrPartOrJoint && id == TokenBranch )
                isBranch = true;
            var previous = state;
            state = Advance( state, id );
            if ( previous == State.ExpectJoint3 )
            {
                if ( !isBranch )
                    bones++;
                isBranch = false;
            }
            if ( id == TokenEos )
                break;
        }
        return bones;
    }

    /// <summary>detokenize + make_skeleton: token sequence → joints/parents/tails.</summary>
    public static UniRigSkeleton Detokenize( IReadOnlyList<int> ids )
    {
        if ( ids.Count < 2 || ids[0] != TokenBos )
            throw new FormatException( "UniRig tokenizer: sequence must start with BOS." );
        var end = ids.Count;
        while ( end > 0 && ids[end - 1] == TokenPad )
            end--;
        if ( ids[end - 1] != TokenEos )
            throw new FormatException( "UniRig tokenizer: sequence must end with EOS." );

        var joints = new List<Vector3>();
        var parentPositions = new List<Vector3>();
        var tailsByBone = new Dictionary<int, Vector3>();
        var isBranch = false;
        Vector3? lastJoint = null;

        Vector3 Coords( int at ) => new(
            Undiscretize( ids[at] ), Undiscretize( ids[at + 1] ), Undiscretize( ids[at + 2] ) );

        var i = 1;
        while ( i < end - 1 )
        {
            var id = ids[i];
            if ( id < NumDiscrete )
            {
                Vector3 current;
                if ( isBranch )
                {
                    parentPositions.Add( Coords( i ) );
                    current = Coords( i + 3 );
                    joints.Add( current );
                    i += 6;
                }
                else
                {
                    current = Coords( i );
                    joints.Add( current );
                    parentPositions.Add( lastJoint ?? current );   // root parents itself
                    i += 3;
                }
                if ( lastJoint is not null )
                    tailsByBone[joints.Count - 2] = current;
                lastJoint = current;
                isBranch = false;
            }
            else if ( id == TokenBranch )
            {
                isBranch = true;
                lastJoint = null;
                i++;
            }
            else if ( id == TokenSpring || IsPart( id ) || IsCls( id ) || id == TokenClsNone )
            {
                i++;   // parts/class recorded upstream; geometry unaffected
            }
            else
            {
                throw new FormatException( $"UniRig tokenizer: unexpected token {id}." );
            }
        }

        // make_skeleton: parent of joint i = earlier bone whose joint is nearest to
        // the recorded parent position (scan i-1 → 0, strictly-smaller wins, so the
        // LATER index wins ties — reversed scan order, matching the reference).
        var count = joints.Count;
        var parents = new int[count];
        for ( var j = 0; j < count; j++ )
        {
            if ( j == 0 )
            {
                parents[0] = -1;
                continue;
            }
            var best = -1;
            var bestDistance = float.MaxValue;
            for ( var k = j - 1; k >= 0; k-- )
            {
                var d = Vector3.DistanceSquared( joints[k], parentPositions[j] );
                if ( d < bestDistance )
                {
                    bestDistance = d;
                    best = k;
                }
            }
            parents[j] = best;
        }

        // Tails: recorded child position, else extruded for leaves/branches
        // (extrude_scale 0.5 along the bone direction; z-up fallback).
        var childCount = new int[count];
        foreach ( var p in parents )
            if ( p >= 0 )
                childCount[p]++;
        var tails = new Vector3[count];
        for ( var j = 0; j < count; j++ )
        {
            if ( childCount[j] == 0 )   // leaf
            {
                var direction = parents[j] >= 0 ? joints[j] - joints[parents[j]] : default;
                if ( direction.LengthSquared() <= 1e-18f )
                    direction = new Vector3( 0f, 0f, 1f );
                tails[j] = joints[j] + direction * 0.5f;
            }
            else if ( childCount[j] > 1 )   // branch
            {
                Vector3 direction;
                if ( parents[j] < 0 )
                {
                    float averageLength = 0;
                    for ( var c = 0; c < count; c++ )
                        if ( parents[c] == j )
                            averageLength += Vector3.Distance( joints[j], joints[c] );
                    averageLength /= childCount[j];
                    tails[j] = joints[j] + new Vector3( 0f, 0f, 0.5f * averageLength );
                    continue;
                }
                direction = joints[j] - joints[parents[j]];
                if ( direction.LengthSquared() <= 1e-18f )
                    direction = new Vector3( 0f, 0f, 1f );
                tails[j] = joints[j] + direction * 0.5f;
            }
            else
            {
                tails[j] = tailsByBone.TryGetValue( j, out var tail ) ? tail : joints[j];
            }
        }

        return new UniRigSkeleton
        {
            Joints = joints.ToArray(),
            Parents = parents,
            Tails = tails,
        };
    }
}
using AutoRig.Analyze;
using AutoRig.Rig;

namespace AutoRig.Solve;

/// <summary>Which solver family to use. Auto lets the classifier route.</summary>
public enum RigMode
{
    Auto,
    Mechanical,
    Organic,
    Floor,
    /// <summary>Neural rigging; needs an installed model (see <see cref="RigNetBundle"/>).</summary>
    DeepLearning,
    /// <summary>Copy skeleton + weights from a rigged donor model (see <see cref="Rig.DonorRig"/>).</summary>
    Transfer,
}

/// <summary>
/// The solver façade: routes an analyzed mesh to the right solver and guarantees a
/// valid result for any mesh that passed <see cref="Mesh.RigMesh.Validate"/> — solver
/// failures degrade to the floor rig instead of throwing.
/// </summary>
public static class AutoRigger
{
    /// <param name="deepLearning">Loaded neural model; required for
    /// <see cref="RigMode.DeepLearning"/>. Without one that mode degrades to the
    /// geometric route (marked Degraded with an explanation).</param>
    /// <param name="donor">Rigged donor; required for <see cref="RigMode.Transfer"/>
    /// (same degrade rule).</param>
    public static RigResult Rig(
        AnalysisResult analysis, RigMode mode = RigMode.Auto,
        RigNetBundle deepLearning = null, Rig.DonorRig donor = null )
    {
        ArgumentNullException.ThrowIfNull( analysis );

        if ( (mode == RigMode.DeepLearning && deepLearning is null)
            || (mode == RigMode.Transfer && donor is null) )
        {
            var geometric = Rig( analysis, RigMode.Auto );
            return new RigResult
            {
                Skeleton = geometric.Skeleton,
                Weights = geometric.Weights,
                SolverName = geometric.SolverName,
                Degraded = true,
                Explanation = (mode == RigMode.Transfer
                        ? "No donor model was picked - used the geometric solver instead. "
                        : "No deep-learning model is installed - used the geometric solver instead. ")
                    + geometric.Explanation,
            };
        }

        try
        {
            var result = mode switch
            {
                RigMode.Mechanical => MechanicalSolver.Rig( analysis ),
                RigMode.Organic => OrganicSolver.Rig( analysis ),
                RigMode.Floor => FloorSolver.Rig( analysis ),
                RigMode.DeepLearning => DeepLearningSolver.Rig( analysis, deepLearning ),
                RigMode.Transfer => TransferSolver.Rig( analysis, donor ),
                _ => analysis.Classification.Kind == MeshKind.Mechanical
                    ? MechanicalSolver.Rig( analysis )
                    : OrganicSolver.Rig( analysis ),
            };
            result.Skeleton.Validate();
            result.Weights.Validate( analysis.Mesh, result.Skeleton );
            return result;
        }
        catch ( Exception e )
        {
            // Failed neural/transfer rigs fall back to the geometric route first.
            if ( mode is RigMode.DeepLearning or RigMode.Transfer )
            {
                var geometric = Rig( analysis, RigMode.Auto );
                return new RigResult
                {
                    Skeleton = geometric.Skeleton,
                    Weights = geometric.Weights,
                    SolverName = geometric.SolverName,
                    Degraded = true,
                    Explanation = $"The {mode} solver failed ({FirstLine( e.Message )}) - "
                        + "used the geometric solver instead.",
                };
            }
            var floor = FloorSolver.Rig( analysis );
            return new RigResult
            {
                Skeleton = floor.Skeleton,
                Weights = floor.Weights,
                SolverName = floor.SolverName,
                Degraded = true,
                Explanation = $"The {mode} solver failed ({FirstLine( e.Message )}) - "
                    + "produced a single-bone rig instead.",
            };
        }
    }

    static string FirstLine( string text )
    {
        var i = text.IndexOfAny( [ '\r', '\n' ] );
        return i < 0 ? text : text[..i];
    }
}
using AutoRig.Rig;

namespace AutoRig.Solve.Organic;

/// <summary>
/// Builds a generic rig straight from the curve skeleton: one joint per graph node,
/// rooted at the core, chains named per branch ("limb1_01", …). Used for organic
/// categories without a dedicated template.
/// </summary>
public static class GraphSkeletonBuilder
{
    public static RigSkeleton Build( SkeletonGraph graph )
    {
        ArgumentNullException.ThrowIfNull( graph );

        var skeleton = new RigSkeleton();
        if ( graph.Nodes.Count == 0 )
            throw new FormatException( "Curve skeleton has no nodes." );

        var jointOf = new int[graph.Nodes.Count];
        Array.Fill( jointOf, -1 );

        // BFS from core so parents precede children.
        var queue = new Queue<int>();
        queue.Enqueue( graph.Core );
        jointOf[graph.Core] = 0;
        skeleton.Joints.Add( new RigJoint
        {
            Name = "root",
            Parent = -1,
            Position = graph.Nodes[graph.Core].Position,
        } );

        var branchCounter = 0;
        var branchOf = new int[graph.Nodes.Count];        // branch id per node
        var linkOf = new int[graph.Nodes.Count];          // index within its branch chain
        branchOf[graph.Core] = -1;

        while ( queue.Count > 0 )
        {
            var current = queue.Dequeue();
            foreach ( var next in graph.Nodes[current].Neighbors.OrderBy( n => n ) )
            {
                if ( jointOf[next] >= 0 )
                    continue;

                // New branch starts when leaving the core or a junction.
                int branch, link;
                if ( current == graph.Core || graph.Nodes[current].Neighbors.Count >= 3 )
                {
                    branch = ++branchCounter;
                    link = 1;
                }
                else
                {
                    branch = branchOf[current];
                    link = linkOf[current] + 1;
                }
                branchOf[next] = branch;
                linkOf[next] = link;

                jointOf[next] = skeleton.Joints.Count;
                skeleton.Joints.Add( new RigJoint
                {
                    Name = $"limb{branch}_{link:00}",
                    Parent = jointOf[current],
                    Position = graph.Nodes[next].Position,
                } );
                queue.Enqueue( next );
            }
        }

        skeleton.Validate();
        return skeleton;
    }
}
using AutoRig.Analyze;
using AutoRig.Dl.RigNet;
using AutoRig.Rig;
using AutoRig.Voxel;

namespace AutoRig.Solve;

using Vector3 = System.Numerics.Vector3;

/// <summary>
/// The transfer solver (spec §5.4): fits a rigged donor's skeleton into the target
/// mesh and carries its skin weights across. Both models are normalized into the
/// same unit space, the donor is stretched per-axis to the target's proportions,
/// joints are snapped into the target's solid, and each target vertex blends the
/// weights of its nearest donor vertices (inverse-squared distance, then one-ring
/// smoothing).
/// </summary>
public static class TransferSolver
{
    const int NearestDonorVertices = 4;
    const float SmoothingBlend = 0.5f;

    public static RigResult Rig( AnalysisResult analysis, DonorRig donor )
    {
        ArgumentNullException.ThrowIfNull( analysis );
        ArgumentNullException.ThrowIfNull( donor );
        var target = analysis.Mesh;

        // ---- shared unit space + per-axis affine donor → target ----
        var (targetNormalized, targetPivot, targetScale) = RigNetInput.Normalize( target.Positions );
        var (donorNormalized, donorPivot, donorScale) = RigNetInput.Normalize( donor.Mesh.Positions );

        var targetBounds = Mesh.Aabb3.FromPoints( targetNormalized );
        var donorBounds = Mesh.Aabb3.FromPoints( donorNormalized );
        var stretch = new Vector3(
            SafeRatio( targetBounds.Size.X, donorBounds.Size.X ),
            SafeRatio( targetBounds.Size.Y, donorBounds.Size.Y ),
            SafeRatio( targetBounds.Size.Z, donorBounds.Size.Z ) );
        var offset = targetBounds.Min - donorBounds.Min * stretch;

        Vector3 Fit( Vector3 donorPoint ) => donorPoint * stretch + offset;

        var donorVertices = new Vector3[donorNormalized.Length];
        for ( var v = 0; v < donorNormalized.Length; v++ )
            donorVertices[v] = Fit( donorNormalized[v] );

        // Donor joints through the same normalize (mesh-derived pivot/scale) + fit.
        var joints = new Vector3[donor.Skeleton.Joints.Count];
        for ( var j = 0; j < joints.Length; j++ )
            joints[j] = Fit( (donor.Skeleton.Joints[j].Position - donorPivot) * donorScale );

        // ---- snap joints into the target's solid ----
        var voxels = BuildVoxels( target, targetNormalized );
        if ( voxels is not null )
            for ( var j = 0; j < joints.Length; j++ )
                joints[j] = SnapInside( joints[j], voxels );

        // ---- weights: inverse-squared-distance blend of nearest donor vertices ----
        var jointCount = donor.Skeleton.Joints.Count;
        var raw = new float[targetNormalized.Length][];
        var nearest = new int[NearestDonorVertices];
        var nearestDistance = new float[NearestDonorVertices];
        for ( var v = 0; v < targetNormalized.Length; v++ )
        {
            var count = 0;
            for ( var d = 0; d < donorVertices.Length; d++ )
            {
                var distance = Vector3.DistanceSquared( targetNormalized[v], donorVertices[d] );
                if ( count < NearestDonorVertices )
                {
                    nearest[count] = d;
                    nearestDistance[count] = distance;
                    count++;
                }
                else
                {
                    var worst = 0;
                    for ( var k = 1; k < NearestDonorVertices; k++ )
                        if ( nearestDistance[k] > nearestDistance[worst] )
                            worst = k;
                    if ( distance < nearestDistance[worst] )
                    {
                        nearest[worst] = d;
                        nearestDistance[worst] = distance;
                    }
                }
            }

            var row = new float[jointCount];
            float weightSum = 0;
            for ( var k = 0; k < count; k++ )
                weightSum += 1f / MathF.Max( nearestDistance[k], 1e-12f );
            for ( var k = 0; k < count; k++ )
            {
                var blend = 1f / MathF.Max( nearestDistance[k], 1e-12f ) / weightSum;
                var donorVertex = nearest[k];
                for ( var slot = 0; slot < 4; slot++ )
                {
                    var weight = donor.Weights.Weights[donorVertex * 4 + slot];
                    if ( weight > 0f )
                        row[donor.Weights.BoneIndices[donorVertex * 4 + slot]] += blend * weight;
                }
            }
            raw[v] = row;
        }

        SmoothOneRing( raw, target.Triangles );

        // ---- assemble ----
        var skeleton = new RigSkeleton();
        for ( var j = 0; j < jointCount; j++ )
        {
            skeleton.Joints.Add( new RigJoint
            {
                Name = donor.Skeleton.Joints[j].Name,
                Parent = donor.Skeleton.Joints[j].Parent,
                Position = joints[j] / targetScale + targetPivot,
            } );
        }

        var indices = new int[targetNormalized.Length * 4];
        var weights = new float[targetNormalized.Length * 4];
        var top = new int[4];
        for ( var v = 0; v < targetNormalized.Length; v++ )
        {
            var row = raw[v];
            var count = 0;
            for ( var j = 0; j < jointCount; j++ )
            {
                if ( row[j] <= 0f )
                    continue;
                if ( count < 4 )
                    top[count++] = j;
                else
                {
                    var weakest = 0;
                    for ( var k = 1; k < 4; k++ )
                        if ( row[top[k]] < row[top[weakest]] )
                            weakest = k;
                    if ( row[j] > row[top[weakest]] )
                        top[weakest] = j;
                }
            }
            float total = 0;
            for ( var k = 0; k < count; k++ )
                total += row[top[k]];
            if ( count == 0 || total <= 0f )
            {
                indices[v * 4] = NearestJoint( joints, targetNormalized[v] );
                weights[v * 4] = 1f;
                continue;
            }
            for ( var k = 0; k < count; k++ )
            {
                indices[v * 4 + k] = top[k];
                weights[v * 4 + k] = row[top[k]] / total;
            }
        }

        return new RigResult
        {
            Skeleton = skeleton,
            Weights = new SkinWeights { BoneIndices = indices, Weights = weights },
            SolverName = "transfer",
            Degraded = false,
            Explanation = $"Transferred {jointCount} joints and their skin weights "
                + $"from '{donor.Mesh.SourceName}'.",
        };
    }

    static float SafeRatio( float target, float donor )
        => donor > 1e-6f ? target / donor : 1f;

    static VoxelGrid BuildVoxels( Mesh.RigMesh target, Vector3[] normalizedPositions )
    {
        try
        {
            var proxy = new Mesh.RigMesh
            {
                SourceName = target.SourceName,
                Positions = normalizedPositions,
                Normals = target.Normals,
                Uvs = target.Uvs,
                Triangles = target.Triangles,
                TriangleTags = target.TriangleTags,
                Tags = target.Tags,
            };
            return VoxelGrid.Build( proxy, 64 );
        }
        catch ( FormatException )
        {
            return null;   // degenerate target — skip snapping
        }
    }

    /// <summary>Nearest solid voxel center by outward ring search (unchanged when
    /// the joint is already inside).</summary>
    static Vector3 SnapInside( Vector3 point, VoxelGrid voxels )
    {
        var (x, y, z) = voxels.VoxelOf( point );
        if ( voxels.IsSolid( x, y, z ) )
            return point;

        var maxRadius = Math.Max( voxels.SizeX, Math.Max( voxels.SizeY, voxels.SizeZ ) );
        for ( var radius = 1; radius <= maxRadius; radius++ )
        {
            var best = point;
            var bestDistance = float.MaxValue;
            for ( var dx = -radius; dx <= radius; dx++ )
                for ( var dy = -radius; dy <= radius; dy++ )
                    for ( var dz = -radius; dz <= radius; dz++ )
                    {
                        if ( Math.Max( Math.Abs( dx ), Math.Max( Math.Abs( dy ), Math.Abs( dz ) ) ) != radius )
                            continue;   // shell only
                        if ( !voxels.IsSolid( x + dx, y + dy, z + dz ) )
                            continue;
                        var center = voxels.CenterOf( x + dx, y + dy, z + dz );
                        var distance = Vector3.DistanceSquared( center, point );
                        if ( distance < bestDistance )
                        {
                            bestDistance = distance;
                            best = center;
                        }
                    }
            if ( bestDistance < float.MaxValue )
                return best;
        }
        return point;
    }

    /// <summary>Blend each vertex row toward its one-ring neighborhood mean.</summary>
    static void SmoothOneRing( float[][] rows, int[] triangles )
    {
        var vertexCount = rows.Length;
        var neighbors = new HashSet<int>[vertexCount];
        for ( var v = 0; v < vertexCount; v++ )
            neighbors[v] = new HashSet<int>();
        for ( var t = 0; t < triangles.Length; t += 3 )
        {
            int a = triangles[t], b = triangles[t + 1], c = triangles[t + 2];
            neighbors[a].Add( b ); neighbors[a].Add( c );
            neighbors[b].Add( a ); neighbors[b].Add( c );
            neighbors[c].Add( a ); neighbors[c].Add( b );
        }

        var jointCount = rows[0].Length;
        var smoothed = new float[vertexCount][];
        for ( var v = 0; v < vertexCount; v++ )
        {
            if ( neighbors[v].Count == 0 )
            {
                smoothed[v] = rows[v];
                continue;
            }
            var mean = new float[jointCount];
            foreach ( var n in neighbors[v] )
                for ( var j = 0; j < jointCount; j++ )
                    mean[j] += rows[n][j];
            var row = new float[jointCount];
            for ( var j = 0; j < jointCount; j++ )
                row[j] = rows[v][j] * (1f - SmoothingBlend)
                    + mean[j] / neighbors[v].Count * SmoothingBlend;
            smoothed[v] = row;
        }
        for ( var v = 0; v < vertexCount; v++ )
            rows[v] = smoothed[v];
    }

    static int NearestJoint( Vector3[] joints, Vector3 point )
    {
        var best = 0;
        for ( var j = 1; j < joints.Length; j++ )
            if ( Vector3.DistanceSquared( joints[j], point )
                < Vector3.DistanceSquared( joints[best], point ) )
                best = j;
        return best;
    }
}
using AutoRig.Analyze;
using AutoRig.Mesh;

namespace AutoRig.Voxel;

// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector3 = System.Numerics.Vector3;

/// <summary>
/// A solid voxelization of a mesh: conservative surface rasterization plus interior
/// fill (exterior flood from the padded border; whatever the flood cannot reach and
/// is not surface is interior). Solid = surface ∪ interior.
/// </summary>
public sealed class VoxelGrid
{
    public int SizeX { get; private init; }
    public int SizeY { get; private init; }
    public int SizeZ { get; private init; }
    public float CellSize { get; private init; }

    /// <summary>World position of voxel (0,0,0)'s min corner.</summary>
    public Vector3 Origin { get; private init; }

    public int SolidCount { get; private set; }

    bool[] _solid = [];

    public bool IsSolid( int x, int y, int z )
        => x >= 0 && x < SizeX && y >= 0 && y < SizeY && z >= 0 && z < SizeZ
            && _solid[Index( x, y, z )];

    public Vector3 CenterOf( int x, int y, int z )
        => Origin + new Vector3( (x + 0.5f) * CellSize, (y + 0.5f) * CellSize, (z + 0.5f) * CellSize );

    public (int X, int Y, int Z) VoxelOf( Vector3 world )
    {
        var p = (world - Origin) / CellSize;
        return (Math.Clamp( (int)p.X, 0, SizeX - 1 ),
                Math.Clamp( (int)p.Y, 0, SizeY - 1 ),
                Math.Clamp( (int)p.Z, 0, SizeZ - 1 ));
    }

    public int Index( int x, int y, int z ) => (z * SizeY + y) * SizeX + x;

    /// <exception cref="FormatException">Empty mesh or degenerate bounds.</exception>
    public static VoxelGrid Build( RigMesh mesh, int maxDimension )
    {
        ArgumentNullException.ThrowIfNull( mesh );
        if ( mesh.Positions.Length == 0 || mesh.TriangleCount == 0 )
            throw new FormatException( "Cannot voxelize an empty mesh." );
        if ( maxDimension < 4 )
            throw new FormatException( $"Voxel maxDimension {maxDimension} too small (need >= 4)." );

        var bounds = mesh.ComputeBounds();
        var size = bounds.Size;
        var longest = MathF.Max( size.X, MathF.Max( size.Y, size.Z ) );
        if ( longest <= 0f )
            throw new FormatException( "Cannot voxelize a mesh with zero extent." );

        const int padding = 2;
        var cell = longest / maxDimension;
        var sizeX = Math.Max( 2, (int)MathF.Ceiling( size.X / cell ) ) + padding * 2;
        var sizeY = Math.Max( 2, (int)MathF.Ceiling( size.Y / cell ) ) + padding * 2;
        var sizeZ = Math.Max( 2, (int)MathF.Ceiling( size.Z / cell ) ) + padding * 2;

        var grid = new VoxelGrid
        {
            SizeX = sizeX,
            SizeY = sizeY,
            SizeZ = sizeZ,
            CellSize = cell,
            Origin = bounds.Min - new Vector3( padding * cell ),
        };
        grid._solid = new bool[sizeX * sizeY * sizeZ];
        var surface = grid._solid; // filled as surface first

        // ---- conservative surface rasterization ----
        var reach = cell * 0.87f; // ~half the cell diagonal: cell centers within this of a triangle are surface
        for ( var t = 0; t < mesh.Triangles.Length; t += 3 )
        {
            var a = mesh.Positions[mesh.Triangles[t]];
            var b = mesh.Positions[mesh.Triangles[t + 1]];
            var c = mesh.Positions[mesh.Triangles[t + 2]];

            var min = Vector3.Min( a, Vector3.Min( b, c ) ) - new Vector3( reach );
            var max = Vector3.Max( a, Vector3.Max( b, c ) ) + new Vector3( reach );
            var (x0, y0, z0) = grid.VoxelOf( min );
            var (x1, y1, z1) = grid.VoxelOf( max );

            for ( var z = z0; z <= z1; z++ )
                for ( var y = y0; y <= y1; y++ )
                    for ( var x = x0; x <= x1; x++ )
                    {
                        var i = grid.Index( x, y, z );
                        if ( surface[i] )
                            continue;
                        var center = grid.CenterOf( x, y, z );
                        var closest = PartContacts.ClosestPointOnTriangle( center, a, b, c );
                        if ( Vector3.DistanceSquared( center, closest ) <= reach * reach )
                            surface[i] = true;
                    }
        }

        // ---- exterior flood (6-connectivity) from every border voxel ----
        var exterior = new bool[surface.Length];
        var queue = new Queue<(int X, int Y, int Z)>();
        void Push( int x, int y, int z )
        {
            var i = grid.Index( x, y, z );
            if ( exterior[i] || surface[i] )
                return;
            exterior[i] = true;
            queue.Enqueue( (x, y, z) );
        }
        for ( var x = 0; x < sizeX; x++ )
            for ( var y = 0; y < sizeY; y++ )
            {
                Push( x, y, 0 );
                Push( x, y, sizeZ - 1 );
            }
        for ( var x = 0; x < sizeX; x++ )
            for ( var z = 0; z < sizeZ; z++ )
            {
                Push( x, 0, z );
                Push( x, sizeY - 1, z );
            }
        for ( var y = 0; y < sizeY; y++ )
            for ( var z = 0; z < sizeZ; z++ )
            {
                Push( 0, y, z );
                Push( sizeX - 1, y, z );
            }
        while ( queue.Count > 0 )
        {
            var (x, y, z) = queue.Dequeue();
            if ( x > 0 ) Push( x - 1, y, z );
            if ( x < sizeX - 1 ) Push( x + 1, y, z );
            if ( y > 0 ) Push( x, y - 1, z );
            if ( y < sizeY - 1 ) Push( x, y + 1, z );
            if ( z > 0 ) Push( x, y, z - 1 );
            if ( z < sizeZ - 1 ) Push( x, y, z + 1 );
        }

        // ---- solid = surface ∪ interior (not exterior) ----
        var solidCount = 0;
        for ( var i = 0; i < surface.Length; i++ )
        {
            surface[i] = surface[i] || !exterior[i];
            if ( surface[i] )
                solidCount++;
        }
        grid.SolidCount = solidCount;
        return grid;
    }
}
using System.Numerics;

namespace AutoRig.Analyze;

// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;


/// <summary>A detected bilateral symmetry plane.</summary>
public readonly record struct SymmetryPlane( Vector3 Origin, Vector3 Normal, float Score );
using AutoRig.Mesh;

namespace AutoRig.Dl.RigNet;

using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;

/// <summary>
/// Builds RigNet's network inputs from a RigMesh, following quick_start's
/// preparation: normalize to a unit-height space, reduce to a proxy mesh of a few
/// thousand vertices (vertex clustering instead of quadric decimation — weights
/// transfer back by nearest proxy vertex, exactly like the original's remesh flow),
/// one-ring topology edges and geodesic-ball edges over a sampled surface
/// geodesic. Randomized reference steps (poisson sampling, random ball picks) are
/// replaced with deterministic equivalents.
/// </summary>
public static class RigNetInput
{
    /// <summary>normalize_obj: scale 1/longest-dim; pivot (center x, min y, center z).</summary>
    public static (Vector3[] Positions, Vector3 Pivot, float Scale) Normalize( Vector3[] positions )
    {
        var bounds = Aabb3.FromPoints( positions );
        var size = bounds.Size;
        var scale = 1f / MathF.Max( size.X, MathF.Max( size.Y, size.Z ) );
        var pivot = new Vector3(
            (bounds.Min.X + bounds.Max.X) * 0.5f, bounds.Min.Y, (bounds.Min.Z + bounds.Max.Z) * 0.5f );
        var result = new Vector3[positions.Length];
        for ( var i = 0; i < positions.Length; i++ )
            result[i] = (positions[i] - pivot) * scale;
        return (result, pivot, scale);
    }

    /// <summary>
    /// Vertex-clustering decimation: bucket vertices on a uniform grid sized so the
    /// proxy stays at or under <paramref name="targetVertices"/>, average each
    /// cluster, drop collapsed triangles. Returns the proxy plus each original
    /// vertex's proxy index (for weight transfer back).
    /// </summary>
    public static (RigMesh Proxy, int[] VertexMap) Decimate( RigMesh mesh, int targetVertices )
    {
        ArgumentNullException.ThrowIfNull( mesh );
        if ( mesh.Positions.Length <= targetVertices )
            return (mesh, Enumerable.Range( 0, mesh.Positions.Length ).ToArray());

        var bounds = mesh.ComputeBounds();
        var size = bounds.Size;
        var longest = MathF.Max( size.X, MathF.Max( size.Y, size.Z ) );

        // Shrink the grid until the cluster count fits the budget.
        var resolution = (int)MathF.Ceiling( MathF.Cbrt( targetVertices ) ) * 2;
        int[] map;
        Vector3[] proxyPositions;
        while ( true )
        {
            var cell = longest / resolution;
            var clusters = new Dictionary<(int, int, int), int>();
            map = new int[mesh.Positions.Length];
            var sums = new List<Vector3>();
            var counts = new List<int>();
            for ( var v = 0; v < mesh.Positions.Length; v++ )
            {
                var p = (mesh.Positions[v] - bounds.Min) / cell;
                var key = ((int)p.X, (int)p.Y, (int)p.Z);
                if ( !clusters.TryGetValue( key, out var id ) )
                {
                    id = sums.Count;
                    clusters[key] = id;
                    sums.Add( Vector3.Zero );
                    counts.Add( 0 );
                }
                map[v] = id;
                sums[id] += mesh.Positions[v];
                counts[id]++;
            }
            if ( sums.Count <= targetVertices || resolution <= 4 )
            {
                proxyPositions = new Vector3[sums.Count];
                for ( var i = 0; i < sums.Count; i++ )
                    proxyPositions[i] = sums[i] / counts[i];
                break;
            }
            resolution = (int)(resolution * 0.8f);
        }

        var triangles = new List<int>();
        for ( var t = 0; t < mesh.TriangleCount; t++ )
        {
            int a = map[mesh.Triangles[t * 3]], b = map[mesh.Triangles[t * 3 + 1]],
                c = map[mesh.Triangles[t * 3 + 2]];
            if ( a != b && b != c && a != c )
            {
                triangles.Add( a );
                triangles.Add( b );
                triangles.Add( c );
            }
        }

        var proxy = new RigMesh
        {
            SourceName = mesh.SourceName,
            Positions = proxyPositions,
            Normals = new Vector3[proxyPositions.Length],
            Uvs = new Vector2[proxyPositions.Length],
            Triangles = triangles.ToArray(),
            TriangleTags = new int[triangles.Count / 3],
            Tags = mesh.Tags,
        };
        return (proxy, map);
    }

    /// <summary>get_tpl_edges: one-ring mesh edges, both directions, no self-loops.</summary>
    public static (int From, int To)[] TopologyEdges( int vertexCount, int[] triangles )
    {
        var neighbors = new HashSet<int>[vertexCount];
        for ( var v = 0; v < vertexCount; v++ )
            neighbors[v] = new HashSet<int>();
        for ( var t = 0; t < triangles.Length; t += 3 )
        {
            int a = triangles[t], b = triangles[t + 1], c = triangles[t + 2];
            neighbors[a].Add( b ); neighbors[a].Add( c );
            neighbors[b].Add( a ); neighbors[b].Add( c );
            neighbors[c].Add( a ); neighbors[c].Add( b );
        }
        var edges = new List<(int, int)>();
        for ( var v = 0; v < vertexCount; v++ )
            foreach ( var n in neighbors[v].OrderBy( n => n ) )
                edges.Add( (v, n) );
        return edges.ToArray();
    }

    /// <summary>
    /// get_geo_edges: for each vertex, edges to vertices within surface-geodesic
    /// distance 0.06 (up to 10, evenly strided instead of the original's random
    /// sample), both stored as (from vertex, to neighbor).
    /// </summary>
    public static (int From, int To)[] GeodesicEdges( SurfaceGeodesic geodesic, int vertexCount )
    {
        var edges = new List<(int, int)>();
        var ball = new List<int>();
        for ( var v = 0; v < vertexCount; v++ )
        {
            ball.Clear();
            for ( var u = 0; u < vertexCount; u++ )
                if ( u != v && geodesic.Distance( v, u ) <= 0.06f )
                    ball.Add( u );
            if ( ball.Count > 10 )
            {
                var strided = new List<int>( 10 );
                for ( var i = 0; i < 10; i++ )
                    strided.Add( ball[i * ball.Count / 10] );
                ball = strided;
            }
            foreach ( var u in ball )
                edges.Add( (v, u) );
        }
        return edges.ToArray();
    }
}

/// <summary>
/// Sampled surface geodesic distances (calc_surface_geodesic): deterministic
/// area-weighted surface samples, a 5-NN graph filtered by normal agreement
/// (cos &gt; -0.5), all-pairs Dijkstra, vertices mapped to their nearest sample.
/// Disconnected pairs fall back to 8 + euclidean, like the reference.
/// </summary>
public sealed class SurfaceGeodesic
{
    readonly float[] _sampleDistances;   // [S*S]
    readonly int[] _vertexSample;        // vertex → sample id
    readonly Vector3[] _samplePositions;
    readonly int _sampleCount;

    public float Distance( int v0, int v1 )
    {
        int s0 = _vertexSample[v0], s1 = _vertexSample[v1];
        var d = _sampleDistances[s0 * _sampleCount + s1];
        return float.IsInfinity( d )
            ? 8f + Vector3.Distance( _samplePositions[s0], _samplePositions[s1] )
            : d;
    }

    SurfaceGeodesic( float[] sampleDistances, int[] vertexSample, Vector3[] samplePositions )
    {
        _sampleDistances = sampleDistances;
        _vertexSample = vertexSample;
        _samplePositions = samplePositions;
        _sampleCount = samplePositions.Length;
    }

    public static SurfaceGeodesic Build( RigMesh mesh, int sampleCount = 1024 )
    {
        ArgumentNullException.ThrowIfNull( mesh );
        var (samples, normals) = SampleSurface( mesh, sampleCount );
        var s = samples.Length;

        // 5-NN graph, keeping neighbors whose normals do not oppose (cos > -0.5).
        var adjacency = new List<(int To, float Weight)>[s];
        for ( var i = 0; i < s; i++ )
            adjacency[i] = new List<(int, float)>();
        var order = new int[s];
        var distances = new float[s];
        for ( var i = 0; i < s; i++ )
        {
            for ( var j = 0; j < s; j++ )
            {
                order[j] = j;
                distances[j] = Vector3.DistanceSquared( samples[i], samples[j] );
            }
            Array.Sort( distances, order );
            for ( var pick = 1; pick <= 5 && pick < s; pick++ )
            {
                var j = order[pick];
                var cos = Vector3.Dot( normals[i], normals[j] )
                    / (normals[i].Length() * normals[j].Length() + 1e-10f);
                if ( cos > -0.5f )
                {
                    var weight = MathF.Sqrt( distances[pick] );
                    adjacency[i].Add( (j, weight) );
                    adjacency[j].Add( (i, weight) );
                }
            }
        }

        // All-pairs Dijkstra over the sample graph (inline binary heap — no
        // PriorityQueue, it is not proven against the s&box whitelist).
        var all = new float[s * s];
        var heap = new MinHeap( s * 8 );
        for ( var source = 0; source < s; source++ )
        {
            var dist = new float[s];
            Array.Fill( dist, float.PositiveInfinity );
            dist[source] = 0f;
            heap.Count = 0;
            heap.Push( source, 0f );
            while ( heap.TryPop( out var u, out var du ) )
            {
                if ( du > dist[u] )
                    continue;
                foreach ( var (to, weight) in adjacency[u] )
                {
                    var candidate = du + weight;
                    if ( candidate < dist[to] )
                    {
                        dist[to] = candidate;
                        heap.Push( to, candidate );
                    }
                }
            }
            Array.Copy( dist, 0, all, source * s, s );
        }

        // Vertex → nearest sample.
        var vertexSample = new int[mesh.Positions.Length];
        for ( var v = 0; v < mesh.Positions.Length; v++ )
        {
            var best = 0;
            var bestDistance = float.MaxValue;
            for ( var i = 0; i < s; i++ )
            {
                var d = Vector3.DistanceSquared( mesh.Positions[v], samples[i] );
                if ( d < bestDistance )
                {
                    bestDistance = d;
                    best = i;
                }
            }
            vertexSample[v] = best;
        }
        return new SurfaceGeodesic( all, vertexSample, samples );
    }

    /// <summary>A minimal (id, key) binary min-heap for Dijkstra.</summary>
    sealed class MinHeap
    {
        int[] _ids;
        float[] _keys;
        public int Count;

        public MinHeap( int capacity )
        {
            _ids = new int[Math.Max( capacity, 16 )];
            _keys = new float[Math.Max( capacity, 16 )];
        }

        public void Push( int id, float key )
        {
            if ( Count == _ids.Length )
            {
                Array.Resize( ref _ids, Count * 2 );
                Array.Resize( ref _keys, Count * 2 );
            }
            var i = Count++;
            while ( i > 0 )
            {
                var parent = (i - 1) / 2;
                if ( _keys[parent] <= key )
                    break;
                _ids[i] = _ids[parent];
                _keys[i] = _keys[parent];
                i = parent;
            }
            _ids[i] = id;
            _keys[i] = key;
        }

        public bool TryPop( out int id, out float key )
        {
            if ( Count == 0 )
            {
                id = 0;
                key = 0f;
                return false;
            }
            id = _ids[0];
            key = _keys[0];
            Count--;
            var lastId = _ids[Count];
            var lastKey = _keys[Count];
            var i = 0;
            while ( true )
            {
                var child = i * 2 + 1;
                if ( child >= Count )
                    break;
                if ( child + 1 < Count && _keys[child + 1] < _keys[child] )
                    child++;
                if ( _keys[child] >= lastKey )
                    break;
                _ids[i] = _ids[child];
                _keys[i] = _keys[child];
                i = child;
            }
            _ids[i] = lastId;
            _keys[i] = lastKey;
            return true;
        }
    }

    /// <summary>Deterministic area-weighted surface sampling with low-discrepancy
    /// barycentrics (stands in for poisson-disk sampling).</summary>
    static (Vector3[] Points, Vector3[] Normals) SampleSurface( RigMesh mesh, int sampleCount )
    {
        var triangleCount = mesh.TriangleCount;
        var cumulative = new float[triangleCount];
        float total = 0f;
        for ( var t = 0; t < triangleCount; t++ )
        {
            var a = mesh.Positions[mesh.Triangles[t * 3]];
            var b = mesh.Positions[mesh.Triangles[t * 3 + 1]];
            var c = mesh.Positions[mesh.Triangles[t * 3 + 2]];
            total += Vector3.Cross( b - a, c - a ).Length() * 0.5f;
            cumulative[t] = total;
        }
        if ( total <= 0f )
            throw new FormatException( "Cannot sample a mesh with zero surface area." );

        var points = new Vector3[sampleCount];
        var normals = new Vector3[sampleCount];
        for ( var i = 0; i < sampleCount; i++ )
        {
            var target = (i + 0.5f) / sampleCount * total;
            var t = Array.BinarySearch( cumulative, target );
            if ( t < 0 )
                t = ~t;
            t = Math.Min( t, triangleCount - 1 );

            var a = mesh.Positions[mesh.Triangles[t * 3]];
            var b = mesh.Positions[mesh.Triangles[t * 3 + 1]];
            var c = mesh.Positions[mesh.Triangles[t * 3 + 2]];

            // Low-discrepancy barycentric from the sample index (plastic constants).
            var r1 = (i * 0.7548776662466927f) % 1f;
            var r2 = (i * 0.5698402909980532f) % 1f;
            if ( r1 + r2 > 1f )
            {
                r1 = 1f - r1;
                r2 = 1f - r2;
            }
            points[i] = a + (b - a) * r1 + (c - a) * r2;
            var normal = Vector3.Cross( b - a, c - a );
            normals[i] = normal.Length() > 1e-12f ? Vector3.Normalize( normal ) : Vector3.UnitY;
        }
        return (points, normals);
    }
}
using System.IO.Compression;
using System.Text;

namespace AutoRig.Dl;

/// <summary>
/// Reads PyTorch checkpoints (both the zip format and the pre-1.6 LEGACY stream
/// format) into named float32 tensors. The embedded pickles run on a RESTRICTED
/// stack machine: only the opcodes a tensor state dict uses, and only three
/// resolvable globals (torch._utils._rebuild_tensor_v2, torch.FloatStorage,
/// collections.OrderedDict). Anything else throws FormatException - arbitrary
/// pickle code can never run.
/// </summary>
public static class TorchCheckpoint
{
    /// <exception cref="FormatException">Unsupported container, pickle content, or
    /// inconsistent tensor data.</exception>
    public static IReadOnlyDictionary<string, Tensor> Parse( byte[] data )
    {
        ArgumentNullException.ThrowIfNull( data );
        if ( data.Length > 2 && data[0] == 0x50 && data[1] == 0x4b )
            return ParseZip( data );
        if ( data.Length > 2 && data[0] == 0x80 )
            return ParseLegacy( data );
        throw new FormatException( "torch checkpoint: neither a zip archive nor a legacy pickle stream." );
    }

    static IReadOnlyDictionary<string, Tensor> ParseZip( byte[] data )
    {
        var entries = ReadZip( data );

        var pickleEntry = entries.Keys.FirstOrDefault( k => k.EndsWith( "/data.pkl", StringComparison.Ordinal ) )
            ?? throw new FormatException( "torch checkpoint: no data.pkl entry found." );
        var prefix = pickleEntry[..^"data.pkl".Length];

        var unpickler = new Unpickler( entries[pickleEntry], 0 );
        var result = unpickler.Run( out _ );
        return Materialize( result, key =>
        {
            if ( !entries.TryGetValue( $"{prefix}data/{key}", out var bytes ) )
                throw new FormatException( $"torch checkpoint: storage '{key}' missing." );
            var type = unpickler.Storages.TryGetValue( key, out var info )
                ? info.Type : "FloatStorage";
            return ToFloat32Bytes( bytes, type );
        } );
    }

    /// <summary>
    /// Streams a zip-format checkpoint — required for files past the 2GB array
    /// limit (e.g. MagicArticulate's 4.4GB training checkpoint, which is also
    /// ZIP64). The stream must be seekable. <paramref name="keepTensor"/> filters
    /// by full dotted name BEFORE storage bytes are read, so skipping e.g.
    /// optimizer state costs no memory or IO.
    /// </summary>
    public static IReadOnlyDictionary<string, Tensor> Parse(
        Stream file, Func<string, bool> keepTensor = null )
    {
        ArgumentNullException.ThrowIfNull( file );
        var entries = ReadZipIndex( file );

        var pickleEntry = entries.Keys.FirstOrDefault( k => k.EndsWith( "/data.pkl", StringComparison.Ordinal ) )
            ?? throw new FormatException( "torch checkpoint: no data.pkl entry found." );
        var prefix = pickleEntry[..^"data.pkl".Length];

        var unpickler = new Unpickler( ReadZipEntry( file, entries[pickleEntry] ), 0 );
        var result = unpickler.Run( out _ );
        return Materialize( result, key =>
        {
            if ( !entries.TryGetValue( $"{prefix}data/{key}", out var entry ) )
                throw new FormatException( $"torch checkpoint: storage '{key}' missing." );
            var type = unpickler.Storages.TryGetValue( key, out var info )
                ? info.Type : "FloatStorage";
            return ToFloat32Bytes( ReadZipEntry( file, entry ), type );
        }, keepTensor );
    }

    /// <summary>Normalizes accepted storage payloads to raw float32 bytes (bf16/f16
    /// checkpoints — e.g. SkinTokens' bfloat16 model — widen at load).</summary>
    internal static byte[] ToFloat32Bytes( byte[] bytes, string storageType )
    {
        switch ( storageType )
        {
            case "BFloat16Storage":
            {
                var result = new byte[bytes.Length * 2];
                for ( var i = 0; i < bytes.Length / 2; i++ )
                {
                    // bf16 = the high 16 bits of an f32 (little-endian layout).
                    result[i * 4 + 2] = bytes[i * 2];
                    result[i * 4 + 3] = bytes[i * 2 + 1];
                }
                return result;
            }
            case "HalfStorage":
            {
                var result = new byte[bytes.Length * 2];
                for ( var i = 0; i < bytes.Length / 2; i++ )
                {
                    var value = (float)BitConverter.ToHalf( bytes, i * 2 );
                    BitConverter.TryWriteBytes( result.AsSpan( i * 4, 4 ), value );
                }
                return result;
            }
            default:
                return bytes;   // FloatStorage — already f32
        }
    }

    /// <summary>
    /// Legacy stream: four consecutive pickles (magic, protocol, sys_info, object),
    /// then a pickle listing storage keys, then per key an i64 element count followed
    /// by the raw float32 storage bytes.
    /// </summary>
    static IReadOnlyDictionary<string, Tensor> ParseLegacy( byte[] data )
    {
        var offset = 0;
        object result = null;
        Unpickler objectUnpickler = null;
        for ( var p = 0; p < 4; p++ )
        {
            objectUnpickler = new Unpickler( data, offset );
            result = objectUnpickler.Run( out offset );
        }

        var keysObject = new Unpickler( data, offset ).Run( out offset );
        if ( keysObject is not List<object> keyList )
            throw new FormatException( "torch checkpoint: legacy storage-key list missing." );

        var storages = new Dictionary<string, byte[]>( StringComparer.Ordinal );
        foreach ( var keyObject in keyList )
        {
            if ( keyObject is not string key )
                throw new FormatException( "torch checkpoint: legacy storage key is not a string." );
            if ( offset + 8 > data.Length )
                throw new FormatException( "torch checkpoint: legacy stream truncated at storage sizes." );
            var numel = BitConverter.ToInt64( data, offset );
            offset += 8;

            // Element width comes from the storage TYPE recorded at its persistent id
            // (optimizer state mixes Long/Int storages between the float tensors).
            var width = objectUnpickler.Storages.TryGetValue( key, out var info ) ? info.Width : 4;
            var byteCount = numel * width;
            if ( numel < 0 || offset + byteCount > data.Length )
                throw new FormatException( $"torch checkpoint: legacy storage '{key}' overruns the file." );

            if ( info.Type is "FloatStorage" or "BFloat16Storage" or "HalfStorage" )
            {
                var bytes = new byte[byteCount];
                Array.Copy( data, offset, bytes, 0, byteCount );
                storages[key] = ToFloat32Bytes( bytes, info.Type );
            }
            offset += (int)byteCount;
        }

        return Materialize( result, key =>
            storages.TryGetValue( key, out var bytes )
                ? bytes
                : throw new FormatException( $"torch checkpoint: legacy storage '{key}' missing." ) );
    }

    /// <summary>Converts the unpickled graph's tensor stubs into tensors.</summary>
    static IReadOnlyDictionary<string, Tensor> Materialize(
        object root, Func<string, byte[]> storageBytes,
        Func<string, bool> keep = null, string prefix = "" )
    {
        if ( root is not Dictionary<object, object> dict )
            throw new FormatException( "torch checkpoint: top-level pickle value is not a dict." );

        var tensors = new Dictionary<string, Tensor>( StringComparer.Ordinal );
        foreach ( var (key, value) in dict )
        {
            if ( key is not string name )
                continue;
            var full = prefix + name;
            if ( value is TensorStub stub )
            {
                if ( keep is not null && !keep( full ) )
                    continue;
                var bytes = storageBytes( stub.StorageKey );
                long count = 1;
                foreach ( var d in stub.Shape )
                    count *= d;
                if ( (stub.StorageOffset + count) * 4L > bytes.Length )
                    throw new FormatException(
                        $"torch checkpoint: storage '{stub.StorageKey}' too small for shape "
                        + $"[{string.Join( ",", stub.Shape )}] at offset {stub.StorageOffset}." );
                var values = new float[count];
                Buffer.BlockCopy( bytes, stub.StorageOffset * 4, values, 0, (int)count * 4 );
                tensors[full] = stub.Shape.Length == 0
                    ? Tensor.From( values, 1 )
                    : Tensor.From( values, stub.Shape );
            }
            else if ( value is Dictionary<object, object> nested )
            {
                // Nested dicts ("state_dict"/"model" wrappers in training checkpoints).
                foreach ( var (innerKey, innerValue) in Materialize( nested, storageBytes, keep, $"{full}." ) )
                    tensors[innerKey] = innerValue;
            }
        }
        return tensors;
    }

    // ================================================================== zip

    /// <summary>Reads a plain zip archive (entry name → bytes). Public so model
    /// bundles (e.g. the RigNet checkpoints zip) can be read without extraction.</summary>
    public static IReadOnlyDictionary<string, byte[]> ReadArchive( byte[] data ) => ReadZip( data );

    // ---- streaming zip index (ZIP64-aware) — for checkpoints past 2GB ----

    readonly record struct ZipIndexEntry( long LocalHeaderOffset, long CompressedSize, ushort Method );

    static Dictionary<string, ZipIndexEntry> ReadZipIndex( Stream file )
    {
        // EOCD scan over the file tail (comment can pad up to 64KB).
        var tailLength = (int)Math.Min( file.Length, 66_000 );
        var tail = ReadAt( file, file.Length - tailLength, tailLength );
        var eocd = -1;
        for ( var i = tailLength - 22; i >= 0; i-- )
        {
            if ( tail[i] == 0x50 && tail[i + 1] == 0x4b && tail[i + 2] == 0x05 && tail[i + 3] == 0x06 )
            {
                eocd = i;
                break;
            }
        }
        if ( eocd < 0 )
            throw new FormatException( "torch checkpoint: not a zip archive (no end-of-central-directory)." );

        long count = BitConverter.ToUInt16( tail, eocd + 10 );
        long cdOffset = BitConverter.ToUInt32( tail, eocd + 16 );

        if ( count == 0xFFFF || cdOffset == 0xFFFFFFFF )
        {
            // ZIP64: locator sits 20 bytes before the EOCD.
            var locatorAt = eocd - 20;
            if ( locatorAt < 0 || BitConverter.ToUInt32( tail, locatorAt ) != 0x07064b50 )
                throw new FormatException( "torch checkpoint: ZIP64 locator missing." );
            var eocd64Offset = BitConverter.ToInt64( tail, locatorAt + 8 );
            var eocd64 = ReadAt( file, eocd64Offset, 56 );
            if ( BitConverter.ToUInt32( eocd64, 0 ) != 0x06064b50 )
                throw new FormatException( "torch checkpoint: corrupt ZIP64 end-of-central-directory." );
            count = BitConverter.ToInt64( eocd64, 32 );
            cdOffset = BitConverter.ToInt64( eocd64, 48 );
        }

        // Central directory can itself be large — read it in one buffer (it is
        // tiny relative to the payload: ~100 bytes per entry).
        var cdLength = file.Length - cdOffset;
        if ( cdLength > int.MaxValue )
            throw new FormatException( "torch checkpoint: central directory too large." );
        var cd = ReadAt( file, cdOffset, (int)cdLength );

        var entries = new Dictionary<string, ZipIndexEntry>( StringComparer.Ordinal );
        var at = 0;
        for ( long e = 0; e < count; e++ )
        {
            if ( at + 46 > cd.Length || BitConverter.ToUInt32( cd, at ) != 0x02014b50 )
                throw new FormatException( "torch checkpoint: corrupt central directory." );
            var method = BitConverter.ToUInt16( cd, at + 10 );
            long compressedSize = BitConverter.ToUInt32( cd, at + 20 );
            long uncompressedSize = BitConverter.ToUInt32( cd, at + 24 );
            var nameLength = BitConverter.ToUInt16( cd, at + 28 );
            var extraLength = BitConverter.ToUInt16( cd, at + 30 );
            var commentLength = BitConverter.ToUInt16( cd, at + 32 );
            long localOffset = BitConverter.ToUInt32( cd, at + 42 );
            var name = Encoding.UTF8.GetString( cd, at + 46, nameLength );

            // ZIP64 extra field 0x0001: u64 values, in order, only for the
            // fixed fields that overflowed to 0xFFFFFFFF.
            var extraAt = at + 46 + nameLength;
            var extraEnd = extraAt + extraLength;
            while ( extraAt + 4 <= extraEnd )
            {
                var id = BitConverter.ToUInt16( cd, extraAt );
                var size = BitConverter.ToUInt16( cd, extraAt + 2 );
                if ( id == 0x0001 )
                {
                    var v = extraAt + 4;
                    if ( uncompressedSize == 0xFFFFFFFF )
                    {
                        uncompressedSize = BitConverter.ToInt64( cd, v );
                        v += 8;
                    }
                    if ( compressedSize == 0xFFFFFFFF )
                    {
                        compressedSize = BitConverter.ToInt64( cd, v );
                        v += 8;
                    }
                    if ( localOffset == 0xFFFFFFFF )
                        localOffset = BitConverter.ToInt64( cd, v );
                }
                extraAt += 4 + size;
            }

            entries[name] = new ZipIndexEntry( localOffset, compressedSize, method );
            at += 46 + nameLength + extraLength + commentLength;
        }
        return entries;
    }

    static byte[] ReadZipEntry( Stream file, ZipIndexEntry entry )
    {
        var header = ReadAt( file, entry.LocalHeaderOffset, 30 );
        if ( BitConverter.ToUInt32( header, 0 ) != 0x04034b50 )
            throw new FormatException( "torch checkpoint: corrupt local header." );
        var nameLength = BitConverter.ToUInt16( header, 26 );
        var extraLength = BitConverter.ToUInt16( header, 28 );
        var dataOffset = entry.LocalHeaderOffset + 30 + nameLength + extraLength;

        if ( entry.CompressedSize > int.MaxValue )
            throw new FormatException( "torch checkpoint: single zip entry past 2GB is not supported." );
        var raw = ReadAt( file, dataOffset, (int)entry.CompressedSize );
        if ( entry.Method == 0 )
            return raw;
        if ( entry.Method != 8 )
            throw new FormatException( $"torch checkpoint: unsupported compression method {entry.Method}." );

        using var inflater = new DeflateStream( new MemoryStream( raw ), CompressionMode.Decompress );
        using var output = new MemoryStream();
        inflater.CopyTo( output );
        return output.ToArray();
    }

    static byte[] ReadAt( Stream file, long offset, int count )
    {
        file.Seek( offset, SeekOrigin.Begin );
        var buffer = new byte[count];
        file.ReadExactly( buffer );
        return buffer;
    }

    /// <summary>Central-directory zip walk (stored + deflate).</summary>
    static Dictionary<string, byte[]> ReadZip( byte[] data )
    {
        // EOCD scan from the end.
        var eocd = -1;
        for ( var i = data.Length - 22; i >= 0; i-- )
        {
            if ( data[i] == 0x50 && data[i + 1] == 0x4b && data[i + 2] == 0x05 && data[i + 3] == 0x06 )
            {
                eocd = i;
                break;
            }
        }
        if ( eocd < 0 )
            throw new FormatException( "torch checkpoint: not a zip archive (no end-of-central-directory)." );

        var count = BitConverter.ToUInt16( data, eocd + 10 );
        var cdOffset = BitConverter.ToUInt32( data, eocd + 16 );

        var entries = new Dictionary<string, byte[]>( StringComparer.Ordinal );
        var at = (int)cdOffset;
        for ( var e = 0; e < count; e++ )
        {
            if ( at + 46 > data.Length || BitConverter.ToUInt32( data, at ) != 0x02014b50 )
                throw new FormatException( "torch checkpoint: corrupt central directory." );
            var method = BitConverter.ToUInt16( data, at + 10 );
            var compressedSize = BitConverter.ToUInt32( data, at + 20 );
            var uncompressedSize = BitConverter.ToUInt32( data, at + 24 );
            var nameLength = BitConverter.ToUInt16( data, at + 28 );
            var extraLength = BitConverter.ToUInt16( data, at + 30 );
            var commentLength = BitConverter.ToUInt16( data, at + 32 );
            var localOffset = BitConverter.ToUInt32( data, at + 42 );
            var name = Encoding.UTF8.GetString( data, at + 46, nameLength );
            at += 46 + nameLength + extraLength + commentLength;

            // Local header carries its own name/extra lengths.
            var lh = (int)localOffset;
            if ( lh + 30 > data.Length || BitConverter.ToUInt32( data, lh ) != 0x04034b50 )
                throw new FormatException( $"torch checkpoint: corrupt local header for '{name}'." );
            var localNameLength = BitConverter.ToUInt16( data, lh + 26 );
            var localExtraLength = BitConverter.ToUInt16( data, lh + 28 );
            var dataStart = lh + 30 + localNameLength + localExtraLength;
            if ( dataStart + compressedSize > data.Length )
                throw new FormatException( $"torch checkpoint: entry '{name}' overruns the file." );

            byte[] content;
            if ( method == 0 )
            {
                content = new byte[compressedSize];
                Array.Copy( data, dataStart, content, 0, (int)compressedSize );
            }
            else if ( method == 8 )
            {
                content = new byte[uncompressedSize];
                using var ms = new MemoryStream( data, dataStart, (int)compressedSize );
                using var deflate = new DeflateStream( ms, CompressionMode.Decompress );
                var read = 0;
                while ( read < content.Length )
                {
                    var n = deflate.Read( content, read, content.Length - read );
                    if ( n <= 0 )
                        throw new FormatException( $"torch checkpoint: truncated deflate data in '{name}'." );
                    read += n;
                }
            }
            else
            {
                throw new FormatException( $"torch checkpoint: unsupported zip method {method} for '{name}'." );
            }
            entries[name] = content;
        }
        return entries;
    }

    // ================================================================== pickle

    sealed class StorageRef
    {
        public required string Key;
        public required long Numel;
    }

    sealed class GlobalRef
    {
        public required string Module;
        public required string Name;
    }

    /// <summary>An unknown global resolved as INERT DATA - nothing is ever executed.</summary>
    sealed class OpaqueRef
    {
        public required string What;
    }

    /// <summary>A tensor recorded during unpickling, materialized once storages are read.</summary>
    sealed class TensorStub
    {
        public required string StorageKey;
        public required int StorageOffset;
        public required int[] Shape;
    }

    sealed class Unpickler
    {
        readonly byte[] _data;
        readonly List<object> _stack = new();
        readonly Dictionary<int, object> _memo = new();
        int _pos;

        /// <summary>Every storage seen via persistent ids: key → (type name, element width).</summary>
        public Dictionary<string, (string Type, int Width)> Storages { get; } = new( StringComparer.Ordinal );

        static readonly object MarkSentinel = new();

        public Unpickler( byte[] pickle, int startOffset )
        {
            _data = pickle;
            _pos = startOffset;
        }

        byte Next()
        {
            if ( _pos >= _data.Length )
                throw new FormatException( "torch checkpoint: pickle stream truncated." );
            return _data[_pos++];
        }

        byte[] NextBytes( int count )
        {
            if ( _pos + count > _data.Length )
                throw new FormatException( "torch checkpoint: pickle stream truncated." );
            var span = new byte[count];
            Array.Copy( _data, _pos, span, 0, count );
            _pos += count;
            return span;
        }

        string ReadLine()
        {
            var start = _pos;
            while ( _pos < _data.Length && _data[_pos] != (byte)'\n' )
                _pos++;
            if ( _pos >= _data.Length )
                throw new FormatException( "torch checkpoint: unterminated pickle text line." );
            var line = Encoding.ASCII.GetString( _data, start, _pos - start );
            _pos++;
            return line;
        }

        void Push( object value ) => _stack.Add( value );

        object Pop()
        {
            if ( _stack.Count == 0 )
                throw new FormatException( "torch checkpoint: pickle stack underflow." );
            var value = _stack[^1];
            _stack.RemoveAt( _stack.Count - 1 );
            return value;
        }

        List<object> PopToMark()
        {
            var items = new List<object>();
            while ( true )
            {
                var value = Pop();
                if ( ReferenceEquals( value, MarkSentinel ) )
                    break;
                items.Add( value );
            }
            items.Reverse();
            return items;
        }

        public object Run( out int endOffset )
        {
            var result = RunInner();
            endOffset = _pos;
            return result;
        }

        object RunInner()
        {
            while ( true )
            {
                var op = Next();
                switch ( op )
                {
                    case 0x80: Next(); break;                             // PROTO n
                    case 0x95: NextBytes( 8 ); break;                      // FRAME len
                    case (byte)'}': Push( new Dictionary<object, object>() ); break; // EMPTY_DICT
                    case (byte)']': Push( new List<object>() ); break;     // EMPTY_LIST
                    case (byte)'(': Push( MarkSentinel ); break;           // MARK
                    case (byte)'N': Push( null ); break;                   // NONE
                    case 0x88: Push( true ); break;                        // NEWTRUE
                    case 0x89: Push( false ); break;                       // NEWFALSE

                    case (byte)'X':                                        // BINUNICODE
                    {
                        var length = BitConverter.ToInt32( NextBytes( 4 ), 0 );
                        Push( Encoding.UTF8.GetString( NextBytes( length ) ) );
                        break;
                    }
                    case 0x8c:                                             // SHORT_BINUNICODE
                    {
                        int length = Next();
                        Push( Encoding.UTF8.GetString( NextBytes( length ) ) );
                        break;
                    }
                    case (byte)'U':                                        // SHORT_BINSTRING (legacy sys_info)
                    {
                        int length = Next();
                        Push( Encoding.Latin1.GetString( NextBytes( length ) ) );
                        break;
                    }
                    case (byte)'T':                                        // BINSTRING
                    {
                        var length = BitConverter.ToInt32( NextBytes( 4 ), 0 );
                        Push( Encoding.Latin1.GetString( NextBytes( length ) ) );
                        break;
                    }

                    case (byte)'J': Push( BitConverter.ToInt32( NextBytes( 4 ), 0 ) ); break; // BININT
                    case (byte)'K': Push( (int)Next() ); break;            // BININT1
                    case (byte)'M': Push( (int)BitConverter.ToUInt16( NextBytes( 2 ), 0 ) ); break; // BININT2
                    case 0x8a:                                             // LONG1
                    {
                        int length = Next();
                        var bytes = NextBytes( length );
                        long value = 0;
                        for ( var i = length - 1; i >= 0; i-- )
                            value = (value << 8) | bytes[i];
                        // little-endian two's complement; small values only in practice
                        Push( (int)value );
                        break;
                    }

                    case (byte)'q': _memo[Next()] = Peek(); break;         // BINPUT
                    case (byte)'r': _memo[BitConverter.ToInt32( NextBytes( 4 ), 0 )] = Peek(); break; // LONG_BINPUT
                    case 0x94: _memo[_memo.Count] = Peek(); break;         // MEMOIZE
                    case (byte)'h': Push( Memo( Next() ) ); break;         // BINGET
                    case (byte)'j': Push( Memo( BitConverter.ToInt32( NextBytes( 4 ), 0 ) ) ); break; // LONG_BINGET

                    case (byte)'t': Push( PopToMark().ToArray() ); break;  // TUPLE
                    case (byte)')': Push( Array.Empty<object>() ); break;  // EMPTY_TUPLE
                    case 0x85: Push( new[] { Pop() } ); break;             // TUPLE1
                    case 0x86:                                             // TUPLE2
                    {
                        var b = Pop(); var a = Pop();
                        Push( new[] { a, b } );
                        break;
                    }
                    case 0x87:                                             // TUPLE3
                    {
                        var c = Pop(); var b = Pop(); var a = Pop();
                        Push( new[] { a, b, c } );
                        break;
                    }

                    case (byte)'c':                                        // GLOBAL
                    {
                        var module = ReadLine();
                        var name = ReadLine();
                        Push( ResolveGlobal( module, name ) );
                        break;
                    }
                    case 0x93:                                             // STACK_GLOBAL
                    {
                        var name = Pop() as string ?? throw Bad( "STACK_GLOBAL name" );
                        var module = Pop() as string ?? throw Bad( "STACK_GLOBAL module" );
                        Push( ResolveGlobal( module, name ) );
                        break;
                    }

                    case (byte)'Q':                                        // BINPERSID
                    {
                        Push( ResolvePersistentId( Pop() ) );
                        break;
                    }

                    case (byte)'R':                                        // REDUCE
                    {
                        var args = Pop() as object[] ?? throw Bad( "REDUCE args" );
                        var callable = Pop();
                        Push( Invoke( callable, args ) );
                        break;
                    }

                    case (byte)'s':                                        // SETITEM
                    {
                        var value = Pop();
                        var key = Pop();
                        (Peek() as Dictionary<object, object> ?? throw Bad( "SETITEM target" ))[key] = value;
                        break;
                    }
                    case (byte)'u':                                        // SETITEMS
                    {
                        var items = PopToMark();
                        var dict = Peek() as Dictionary<object, object> ?? throw Bad( "SETITEMS target" );
                        for ( var i = 0; i + 1 < items.Count; i += 2 )
                            dict[items[i]] = items[i + 1];
                        break;
                    }
                    case (byte)'a':                                        // APPEND (single)
                    {
                        var item = Pop();
                        (Peek() as List<object> ?? throw Bad( "APPEND target" )).Add( item );
                        break;
                    }
                    case (byte)'0': Pop(); break;                          // POP
                    case (byte)'e':                                        // APPENDS
                    {
                        var items = PopToMark();
                        var list = Peek() as List<object> ?? throw Bad( "APPENDS target" );
                        list.AddRange( items );
                        break;
                    }

                    case (byte)'G':                                        // BINFLOAT (big-endian f64)
                    {
                        var raw = NextBytes( 8 );
                        var swapped = new byte[8];
                        for ( var i = 0; i < 8; i++ )
                            swapped[i] = raw[7 - i];
                        Push( BitConverter.ToDouble( swapped, 0 ) );
                        break;
                    }

                    case 0x81:                                             // NEWOBJ: cls(*args) - inert
                    {
                        Pop(); // args
                        var cls = Pop();
                        Push( cls is GlobalRef g
                            ? Invoke( g, Array.Empty<object>() )
                            : new OpaqueRef { What = "NEWOBJ instance" } );
                        break;
                    }

                    case (byte)'b':                                        // BUILD: apply state - inert merge
                    {
                        var state = Pop();
                        if ( Peek() is Dictionary<object, object> targetDict
                            && state is Dictionary<object, object> stateDict )
                        {
                            foreach ( var (k, v) in stateDict )
                                targetDict[k] = v;
                        }
                        // Opaque targets: state discarded, nothing executed.
                        break;
                    }

                    case (byte)'.':                                        // STOP
                        return Pop();

                    default:
                        throw new FormatException(
                            $"torch checkpoint: unsupported pickle opcode 0x{op:X2} at {_pos - 1} "
                            + "(the restricted reader accepts tensor state dicts only)." );
                }
            }
        }

        object Peek() => _stack.Count > 0 ? _stack[^1] : throw Bad( "empty stack" );

        object Memo( int slot )
            => _memo.TryGetValue( slot, out var value ) ? value : throw Bad( $"memo slot {slot}" );

        static FormatException Bad( string what )
            => new( $"torch checkpoint: malformed pickle ({what})." );

        static object ResolveGlobal( string module, string name ) => (module, name) switch
        {
            ("torch._utils", "_rebuild_tensor_v2") => new GlobalRef { Module = module, Name = name },
            ("torch", "FloatStorage") => new GlobalRef { Module = module, Name = name },
            ("collections", "OrderedDict") => new GlobalRef { Module = module, Name = name },
            // Anything else becomes INERT DATA: nothing is looked up or executed, and
            // values built from it are dropped at materialization. Training
            // checkpoints carry optimizer state and framework metadata we must
            // tolerate without running.
            _ => new OpaqueRef { What = $"{module}.{name}" },
        };

        static int StorageWidth( string typeName ) => typeName switch
        {
            "DoubleStorage" or "LongStorage" => 8,
            "FloatStorage" or "IntStorage" => 4,
            "HalfStorage" or "ShortStorage" or "BFloat16Storage" => 2,
            _ => 1, // Byte/Char/Bool
        };

        object ResolvePersistentId( object pid )
        {
            if ( pid is not object[] tuple || tuple.Length < 5
                || tuple[0] is not string kind || kind != "storage"
                || tuple[2] is not string key )
                throw Bad( "persistent id" );

            var typeName = tuple[1] switch
            {
                GlobalRef g => g.Name,
                OpaqueRef o => o.What.Contains( '.' ) ? o.What[(o.What.LastIndexOf( '.' ) + 1)..] : o.What,
                _ => throw Bad( "storage type" ),
            };
            var numel = tuple[4] switch { int i => (long)i, long l => l, _ => throw Bad( "storage numel" ) };
            Storages[key] = (typeName, StorageWidth( typeName ));

            if ( typeName is not ("FloatStorage" or "BFloat16Storage" or "HalfStorage") )
                return new OpaqueRef { What = $"non-float storage {typeName}" };
            return new StorageRef { Key = key, Numel = numel };
        }

        object Invoke( object callable, object[] args )
        {
            if ( callable is OpaqueRef opaque )
                return opaque; // inert: nothing executed, value dropped later

            if ( callable is not GlobalRef global )
                throw Bad( "REDUCE callable" );

            if ( global is { Module: "collections", Name: "OrderedDict" } )
                return new Dictionary<object, object>();

            if ( global is { Module: "torch._utils", Name: "_rebuild_tensor_v2" } )
            {
                if ( args.Length < 4
                    || args[1] is not int storageOffset
                    || args[2] is not object[] sizeTuple
                    || args[3] is not object[] strideTuple )
                    throw Bad( "_rebuild_tensor_v2 args" );
                if ( args[0] is OpaqueRef )
                    return new OpaqueRef { What = "tensor on unsupported storage" };
                if ( args[0] is not StorageRef storage )
                    throw Bad( "_rebuild_tensor_v2 storage" );

                var shape = sizeTuple
                    .Select( s => s is int v ? v : throw Bad( "size" ) ).ToArray();
                var stride = strideTuple
                    .Select( s => s is int v ? v : throw Bad( "stride" ) ).ToArray();

                // Contiguous row-major only.
                var expected = 1;
                for ( var d = shape.Length - 1; d >= 0; d-- )
                {
                    if ( stride[d] != expected )
                        throw new FormatException(
                            "torch checkpoint: non-contiguous tensor storage is not supported." );
                    expected *= shape[d];
                }

                return new TensorStub
                {
                    StorageKey = storage.Key,
                    StorageOffset = storageOffset,
                    Shape = shape,
                };
            }

            throw Bad( $"REDUCE of {global.Module}.{global.Name}" );
        }
    }
}
using AutoRig.Mesh;
using AutoRig.Rig;

namespace AutoRig.Export;

/// <summary>Everything a caller needs to write a rigged model to disk.</summary>
public sealed class ExportBundle
{
    public required byte[] Fbx { get; init; }
    public required string Vmdl { get; init; }
    public required string FbxFileName { get; init; }
    public required string VmdlFileName { get; init; }

    /// <summary>Companion files (texture image + generated .vmat), written into the
    /// same folder as the fbx/vmdl. Empty when the source had no textures.</summary>
    public IReadOnlyList<(string FileName, byte[] Bytes)> ExtraFiles { get; init; }
        = Array.Empty<(string, byte[])>();
}

/// <summary>
/// Export façade: RigResult → binary FBX bytes + vmdl text. No file IO here (Code/
/// discipline) — the editor layer writes the files where the user chose.
/// </summary>
public static class RigExporter
{
    /// <param name="assetFolder">Project-relative folder the caller will write the
    /// bundle into (used for texture/vmat references inside the generated files).</param>
    public static ExportBundle Export(
        RigMesh mesh, RigResult rig, string modelName, string assetFolder = "models/autorig" )
    {
        ArgumentNullException.ThrowIfNull( mesh );
        ArgumentNullException.ThrowIfNull( rig );
        ArgumentNullException.ThrowIfNull( modelName );

        var name = NameUtil.Sanitize( modelName );
        var fbxFileName = $"{name}.fbx";
        var folder = assetFolder.Replace( '\\', '/' ).Trim( '/' );

        // Place the model where ModelDoc expects it: centered on the origin's
        // ground plane. Off-origin sources otherwise appear shoved to one side
        // (X/Z offset) or sunk through the floor (Y offset). Center the footprint
        // (X, Z at the bbox center → 0) and floor-snap so the lowest point sits at
        // y = 0. Mesh AND joints move together so the rig stays aligned.
        var bounds = mesh.ComputeBounds();
        var center = bounds.Center;
        var lift = new System.Numerics.Vector3( -center.X, -bounds.Min.Y, -center.Z );
        if ( lift.Length() > 1e-4f )
        {
            var lifted = new Mesh.RigMesh
            {
                SourceName = mesh.SourceName,
                Positions = mesh.Positions.Select( p => p + lift ).ToArray(),
                Normals = mesh.Normals,
                Uvs = mesh.Uvs,
                Triangles = mesh.Triangles,
                TriangleTags = mesh.TriangleTags,
                Tags = mesh.Tags,
                Materials = mesh.Materials,
                TriangleMaterials = mesh.TriangleMaterials,
            };
            var liftedSkeleton = new Rig.RigSkeleton();
            foreach ( var j in rig.Skeleton.Joints )
                liftedSkeleton.Joints.Add( new Rig.RigJoint
                {
                    Name = j.Name,
                    Parent = j.Parent,
                    Position = j.Position + lift,
                    HingeAxis = j.HingeAxis,
                } );
            mesh = lifted;
            rig = new Rig.RigResult
            {
                Skeleton = liftedSkeleton,
                Weights = rig.Weights,
                SolverName = rig.SolverName,
                Degraded = rig.Degraded,
                Explanation = rig.Explanation,
            };
        }

        // Texture passthrough (v1: whole model bound to the first textured source
        // material — the FBX carries a single material named "{name}_mat").
        var extras = new List<(string, byte[])>();
        string remapFrom = null, remapTo = null;
        var source = mesh.Materials.FirstOrDefault( m => m.BaseColorImage is not null )
            ?? mesh.Materials.FirstOrDefault();
        if ( source is not null )
        {
            var vmat = $"// generated by auto_rig from '{mesh.SourceName}'\nLayer0\n{{\n"
                + "\tshader \"shaders/complex.shader\"\n";
            if ( source.BaseColorImage is { } image )
            {
                var extension = image.Length > 2 && image[0] == 0xFF && image[1] == 0xD8
                    ? "jpg" : "png";
                var imageFileName = $"{name}_color.{extension}";
                extras.Add( (imageFileName, image) );
                vmat += $"\tTextureColor \"{folder}/{imageFileName}\"\n";
            }
            if ( source.Tint != new System.Numerics.Vector3( 1f, 1f, 1f ) )
                vmat += $"\tg_vColorTint \"[{source.Tint.X:0.###} {source.Tint.Y:0.###} {source.Tint.Z:0.###}]\"\n";
            vmat += "}\n";

            var vmatFileName = $"{name}_mat.vmat";
            extras.Add( (vmatFileName, System.Text.Encoding.UTF8.GetBytes( vmat )) );
            remapFrom = $"{name}_mat.vmat";                 // FBX material name
            remapTo = $"{folder}/{vmatFileName}";
        }

        return new ExportBundle
        {
            Fbx = FbxRigWriter.Write( mesh, rig, name ),
            Vmdl = VmdlGenerator.Generate( fbxFileName, name, remapFrom, remapTo ),
            FbxFileName = fbxFileName,
            VmdlFileName = $"{name}.vmdl",
            ExtraFiles = extras,
        };
    }
}