Code/Spray.Placement.cs
using Sandbox;
using Sandbox.Utility;
using System.Collections.Generic;
namespace badandbest.Sprays;

public static partial class Spray
{
	public static GameObject LocalSpray;

	/// <summary>
	/// Places an image on a surface.
	/// </summary>
	public static void Place()
	{
		const float RANGE = 128;// Range in GMOD.

		var ray = Game.ActiveScene.Camera.Transform.World.ForwardRay;
		var trace = Game.SceneTrace.Ray( ray, RANGE );

		Place( trace );
	}

	/// <summary>
	/// Places an image on a surface.
	/// </summary>
	/// <param name="trace">The trace to use.</param>
	public static void Place( SceneTrace trace )
	{
		// We only want to hit static bodies. ( maps, etc )
		if ( trace.Run() is not { Body.BodyType: PhysicsBodyType.Static } tr )
			return;

		var config = new CloneConfig
		{
			Name = $"Spray - {Steam.PersonaName}",
			Transform = new Transform( tr.HitPosition, Rotation.LookAt( tr.Normal ) ),
			PrefabVariables = new Dictionary<string, object>
			{
				{ "Image", Cookie.Get( "spray.url", "materials/decals/default.png" ) },
				{ "Placer", Steam.PersonaName },
			}
		};

		LocalSpray?.Destroy();
		LocalSpray = GameObject.Clone( "prefabs/spray.prefab", config );

		LocalSpray.NetworkSpawn(); // NetworkSpawn breaks the prefab
		LocalSpray.SetPrefabSource( "prefabs/spray.prefab" );
	}
}

[Title( "Spray Renderer" ), Icon( "imagesearch_roller" )]
internal class SprayRenderer : Renderer
{
	[Property] internal DecalRenderer _decal { get; set; }
	[Property] internal TextRenderer _text { get; set; }

	[Property, ImageAssetPath]
	public string Image { get; set; }

	[Button( "Remove Spray", Icon = "clear" )]
	public void Remove() => GameObject.Destroy();
	
	public virtual void UpdateObject()
	{
		_decal.Enabled = !Spray.DisableRendering;
		_text.Enabled = Spray.EnableDebug;
	}

	protected override async void OnAwake()
	{
		UpdateObject();

		var texture = await Texture.LoadAsync( FileSystem.Mounted, Image );

		_decal.Material = Material.Load( "materials/spray.vmat" ).CreateCopy();
		_decal.Material.Set( "g_tColor", texture );
	}
}