Editor/Prism/Model/PreviewState.cs

Editor-side model classes for previewing 3D assets. PreviewState stores per-graph viewport settings (mesh/model, envmap, colors, flags, channel) and provides cloning/copying, while PreviewCamera stores orbit parameters with computed clamped properties and cloning.

namespace Editor.Prism.Model;

/// <summary>
/// Per-graph preview settings: what the viewport shows and how the camera is posed. Serialized with
/// the document so reopening a graph restores exactly the framing the author left it in.
/// </summary>
public sealed class PreviewState
{
	/// <summary>Built-in mesh name from <see cref="Meshes"/>. Ignored when <see cref="Model"/> is set.</summary>
	public string Mesh { get; set; } = "Sphere";

	/// <summary>Custom model asset path. Takes precedence over <see cref="Mesh"/> when non-empty.</summary>
	public string Model { get; set; }

	/// <summary>Environment map asset path used for image-based lighting and the skybox.</summary>
	public string Envmap { get; set; }

	/// <summary>Whether the ground plane is drawn.</summary>
	public bool ShowGround { get; set; }

	/// <summary>Whether the skybox is drawn behind the subject.</summary>
	public bool ShowSkybox { get; set; } = true;

	/// <summary>Tint multiplied into the preview material.</summary>
	public Color Tint { get; set; } = Color.White;

	/// <summary>Viewport clear colour, used when the skybox is hidden.</summary>
	public Color Background { get; set; } = new( 0.05f, 0.06f, 0.07f, 1f );

	/// <summary>Camera orbit state.</summary>
	public PreviewCamera Camera { get; set; } = new();

	/// <summary>
	/// The debug channel currently displayed. <c>null</c> or empty means the shaded result; anything
	/// else names one of the preview channels (albedo, normal, roughness, uv, overdraw…).
	/// </summary>
	public string Channel { get; set; }

	/// <summary>True when a custom model asset is chosen rather than a built-in mesh.</summary>
	public bool UsesCustomModel => !string.IsNullOrWhiteSpace( Model );

	/// <summary>The mesh name to build, falling back to a sphere when the stored name is unknown.</summary>
	public string EffectiveMesh =>
		!string.IsNullOrWhiteSpace( Mesh ) && Meshes.Contains( Mesh, StringComparer.OrdinalIgnoreCase )
			? Mesh
			: "Sphere";

	/// <summary>Deep copy.</summary>
	public PreviewState Clone() => new()
	{
		Mesh = Mesh,
		Model = Model,
		Envmap = Envmap,
		ShowGround = ShowGround,
		ShowSkybox = ShowSkybox,
		Tint = Tint,
		Background = Background,
		Camera = Camera?.Clone() ?? new PreviewCamera(),
		Channel = Channel
	};

	/// <summary>Copy every field from another instance without replacing the object identity.</summary>
	public void CopyFrom( PreviewState other )
	{
		if ( other is null ) return;

		Mesh = other.Mesh;
		Model = other.Model;
		Envmap = other.Envmap;
		ShowGround = other.ShowGround;
		ShowSkybox = other.ShowSkybox;
		Tint = other.Tint;
		Background = other.Background;
		Camera = other.Camera?.Clone() ?? new PreviewCamera();
		Channel = other.Channel;
	}

	/// <inheritdoc/>
	public override string ToString() => UsesCustomModel ? $"Preview '{Model}'" : $"Preview {EffectiveMesh}";

	/// <summary>The built-in preview meshes the preview package generates procedurally.</summary>
	public static readonly IReadOnlyList<string> Meshes = new[]
	{
		"Sphere", "Cube", "Plane", "Quad", "Cylinder", "Cone", "Torus"
	};
}

/// <summary>The preview camera's orbit around the subject.</summary>
public sealed class PreviewCamera
{
	/// <summary>Orbit yaw in degrees.</summary>
	public float Yaw { get; set; } = 135f;

	/// <summary>Orbit pitch in degrees, clamped to +/-89 when applied.</summary>
	public float Pitch { get; set; } = 30f;

	/// <summary>Orbit distance in world units (inches).</summary>
	public float Distance { get; set; } = 150f;

	/// <summary>The pitch clamped into the range a look-at camera can actually use.</summary>
	public float SafePitch => Math.Clamp( Pitch, -89f, 89f );

	/// <summary>The distance clamped away from zero so the camera never lands inside the subject.</summary>
	public float SafeDistance => Math.Clamp( Distance, 1f, 100000f );

	/// <summary>Deep copy.</summary>
	public PreviewCamera Clone() => new() { Yaw = Yaw, Pitch = Pitch, Distance = Distance };

	/// <inheritdoc/>
	public override string ToString() => $"yaw {Yaw:0.#} pitch {Pitch:0.#} dist {Distance:0.#}";
}