editor/AdvancedMappingTools/Editing/PropWorkshopExpansion.cs

Editor-side prop workshop helpers for AdvancedMappingTools. Defines physics presets (static, crate, metal, rubber, floating), saves and loads material templates as JSON under Documents/DavelixConstruct/PropMaterials, and builds a temporary Scene to compute collision boxes, wireframe edges and mass center for a viewport gizmo preview drawn by PropWorkshopEditor.

File Access
using System.IO;

namespace AdvancedMappingTools;
public enum PropPhysicsTemplate
{
	StaticScenery,
	WoodenCrate,
	HeavyMetal,
	Rubber,
	Floating
}

public sealed partial class PropWorkshopSettings
{
	[Property, Group( "Physics" ), WideMode]
	public PropPhysicsTemplate PhysicsTemplate { get; set; }

	[Property, Group( "Materials" ), WideMode]
	public string MaterialTemplateName { get; set; } = "My Materials";

	[Property, Group( "Preview" ), WideMode]
	public bool ShowCollisionPreview { get; set; } = true;

	[Property, Group( "Preview" ), WideMode]
	public bool ShowMassCenter { get; set; } = true;
}

public static partial class PropWorkshop
{
	public sealed record PreviewData( List<BBox> Boxes, List<(Vector3 A, Vector3 B)> Edges, Vector3 Center, bool Physical );
	public static void ApplyPhysicsTemplate( PropWorkshopSettings s )
	{
		s.Collision = PropCollision.ConvexParts;
		s.Gravity = true;
		s.GravityScale = 1;
		s.Motion = PropMotion.Dynamic;
		s.LinearDamping = .1f;
		s.AngularDamping = .1f;
		s.Elasticity = 0;
		s.Friction = .5f;
		s.Mass = 10;
		s.StartAsleep = false;
		s.LockX = s.LockY = s.LockZ = s.LockPitch = s.LockYaw = s.LockRoll = false;
		switch ( s.PhysicsTemplate )
		{
			case PropPhysicsTemplate.StaticScenery:
				s.Motion = PropMotion.Static;
				s.Collision = PropCollision.ExactStatic;
				break;
			case PropPhysicsTemplate.WoodenCrate:
				s.Mass = 20;
				s.Friction = .7f;
				break;
			case PropPhysicsTemplate.HeavyMetal:
				s.Mass = 150;
				s.Friction = .4f;
				break;
			case PropPhysicsTemplate.Rubber:
				s.Mass = 5;
				s.Elasticity = .8f;
				s.Friction = .9f;
				break;
			case PropPhysicsTemplate.Floating:
				s.Gravity = false;
				s.LinearDamping = .5f;
				break;
		}
	}

	public sealed class MaterialTemplate
	{
		public string Material { get; set; }
		public string Replace { get; set; }
		public Color Tint { get; set; } = Color.White;
		public bool Preserve { get; set; } = true;
	}

	static string TemplatePath( string name )
	{
		if ( string.IsNullOrWhiteSpace( name ) )
			throw new ArgumentException( "Give the material template a name." );
		var safe = new string ( name.Select( c => Path.GetInvalidFileNameChars().Contains( c ) ? '_' : c ).ToArray() ).Trim();
		if ( safe.Length > 80 )
			safe = safe[..80];
		return Path.Combine( Environment.GetFolderPath( Environment.SpecialFolder.MyDocuments ), "DavelixConstruct", "PropMaterials", safe + ".json" );
	}

	public static void SaveMaterialTemplate( PropWorkshopSettings s ) => StoreSafety.Write( TemplatePath( s.MaterialTemplateName ), System.Text.Json.JsonSerializer.Serialize( new MaterialTemplate { Material = s.Material?.ResourcePath, Replace = s.ReplaceMaterial?.ResourcePath, Tint = s.Tint, Preserve = s.PreserveColors } ) );
	public static void LoadMaterialTemplate( PropWorkshopSettings s )
	{
		var value = StoreSafety.Read<MaterialTemplate>( TemplatePath( s.MaterialTemplateName ) ) ?? throw new InvalidOperationException( "Material template missing or damaged." );
		s.Material = string.IsNullOrEmpty( value.Material ) ? null : Material.Load( value.Material );
		s.ReplaceMaterial = string.IsNullOrEmpty( value.Replace ) ? null : Material.Load( value.Replace );
		s.Tint = value.Tint;
		s.PreserveColors = value.Preserve;
	}

	public static PreviewData Preview( IEnumerable<GameObject> sources, PropWorkshopSettings s )
	{
		var parts = Geometry( sources );
		if ( parts.Count == 0 )
			throw new InvalidOperationException( "Capture visible meshes or models first." );
		var scene = new Scene();
		try
		{
			using ( scene.Push() )
			{
				var temporary = new List<GameObject>();
				foreach ( var part in parts )
				{
					var go = new GameObject( true, "Preview Source" );
					go.WorldTransform = part.WorldTransform;
					if ( part is MeshComponent source )
					{
						var mesh = go.Components.Create<MeshComponent>( false );
						mesh.Mesh = source.Mesh;
						mesh.Enabled = true;
					}
					else if ( part is ModelRenderer model )
					{
						var renderer = go.Components.Create<ModelRenderer>();
						renderer.Model = model.Model;
						renderer.BodyGroups = model.BodyGroups;
						renderer.CreateAttachments = false;
					}

					temporary.Add( go );
				}

				var root = Create( scene, temporary, s );
				var boxes = new List<BBox>();
				var edges = new List<(Vector3, Vector3)>();
				var geometry = Geometry( [root] );
				if ( s.Collision == PropCollision.Box )
					boxes.Add( Bounds( geometry ) );
				else if ( s.Collision != PropCollision.None )
					foreach ( var part in geometry )
					{
						if ( part is MeshComponent m && s.Collision == PropCollision.ExactStatic )
						{
							foreach ( var face in m.Mesh.FaceHandles )
							{
								var v = m.Mesh.GetFaceVertices( face ).Select( v => m.Mesh.GetVertexPosition( v ) ).Select( v => m.WorldTransform.PointToWorld( v ) ).ToArray();
								for ( var i = 0; i < v.Length; i++ )
									edges.Add( (v[i], v[(i + 1) % v.Length]) );
							}
						}
						else
							boxes.Add( Bounds( [part] ) );
					}

				var body = root.Components.Get<Rigidbody>();
				return new( boxes, edges, body != null ? root.WorldTransform.PointToWorld( body.MassCenter ) : Bounds( geometry ).Center, body != null );
			}
		}
		finally
		{
			scene.Destroy();
		}
	}
}

public sealed partial class PropWorkshopEditor
{
	PropWorkshop.PreviewData preview;
	void DrawWorkshopPreview()
	{
		if ( preview is null )
			return;
		Gizmo.Draw.Color = Color.Yellow;
		if ( Settings.ShowCollisionPreview )
		{
			foreach ( var box in preview.Boxes )
			{
				var a = box.Mins;
				var b = box.Maxs;
				var v = new[]
				{
					new Vector3( a.x, a.y, a.z ),
					new Vector3( b.x, a.y, a.z ),
					new Vector3( b.x, b.y, a.z ),
					new Vector3( a.x, b.y, a.z ),
					new Vector3( a.x, a.y, b.z ),
					new Vector3( b.x, a.y, b.z ),
					new Vector3( b.x, b.y, b.z ),
					new Vector3( a.x, b.y, b.z )
				};
				for ( var i = 0; i < 4; i++ )
				{
					Gizmo.Draw.Line( v[i], v[(i + 1) % 4] );
					Gizmo.Draw.Line( v[i + 4], v[(i + 1) % 4 + 4] );
					Gizmo.Draw.Line( v[i], v[i + 4] );
				}
			}

			foreach ( var edge in preview.Edges )
				Gizmo.Draw.Line( edge.A, edge.B );
		}

		if ( Settings.ShowMassCenter )
		{
			Gizmo.Draw.Color = Color.Green;
			Gizmo.Draw.SolidSphere( preview.Center, 3 );
			DrawLabel( preview.Center, preview.Physical ? "Physics mass center" : "Bounds center", Color.Green );
		}
	}
}