Effigy/Features/RemeshFeature.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace Effigy;

/// <summary>
/// Fewer triangles, same shape. <see cref="SubdivideFeature"/>'s opposite number.
///
/// THE FEATURE THE IMPORTER CREATED THE NEED FOR. Everything the tool built itself arrived at a
/// sensible density because a primitive is a handful of quads and you add detail deliberately. A
/// mesh that came from somewhere else did not: a Meshy generation, a scan, a sculpt exported by
/// whoever made it, all land somewhere between a hundred thousand and a million triangles because
/// nothing upstream had a reason to stop. That mesh is the right SHAPE and the wrong SIZE, and at
/// that size it is not a model you can work on — the pick tree keeps hover usable and nothing keeps
/// subdivision, weight painting, the boolean or the compiler usable. Remesh is the step that turns
/// an import into a part.
///
/// IT COMES BACK AS TRIANGLES. Always, including where quads went in — see <see cref="Decimate"/>
/// for why, and for why the answer is not to pretend otherwise. So this belongs on an import and
/// not on a CAD body: rolling a Remesh over a quad cage you built with sketches and extrudes throws
/// away the topology that made it a cage, and the feature says so rather than letting you find out
/// two workspaces later. The refusal is a warning rather than an error because there are real
/// reasons to want it anyway — a collision hull, a distant LOD — and being told is enough.
///
/// THE TARGET IS IN TRIANGLES, NOT FACES, and that is a distinction the dialog has to keep making
/// because the tree everywhere else counts faces. A 500-face quad cage is 1000 triangles; typing
/// 500 into a triangle budget and getting half of what you meant is the kind of confusion that
/// looks like the algorithm being bad at its job.
/// </summary>
public sealed class RemeshFeature : Feature
{
	public override string TypeName => "Remesh";

	/// <summary>A part, and nothing finer. There is no per-face form of this: the quadrics are
	/// summed over the whole surface and a boundary drawn through the middle of one would be
	/// preserved as a border, which is the opposite of what picking a region would mean.</summary>
	public override GeometryKind Accepts => GeometryKind.Body;

	public readonly BodySelectionParam Bodies = new( "Bodies" );

	/// <summary>
	/// Which of the two numbers below is the one being read.
	///
	/// PERCENTAGE FIRST because it is the one you can answer without knowing anything. An import
	/// whose size you have not looked at has no obvious triangle budget, and "keep a tenth" is a
	/// decision you can make on the spot and correct once. A count is what you switch to when the
	/// budget is coming from somewhere else — a platform limit, an LOD ladder, a collision hull.
	/// </summary>
	public readonly ChoiceParam Target = new( "Target", new[] { "Percentage", "Triangle count" } );

	/// <summary>Fraction of the incoming triangles to keep, as a percentage. Read when
	/// <see cref="Target"/> is Percentage.</summary>
	public readonly FloatParam Percent = new( "Keep %", 10f, 0.1f, 100f );

	/// <summary>
	/// Triangles to stop at, per body. PER BODY rather than for the selection as a whole: splitting
	/// one budget across parts means deciding what a part is worth, and the honest version of that
	/// decision is to run the feature twice with two numbers rather than to have it guessed.
	/// </summary>
	public readonly IntParam Triangles = new( "Triangles", 5000, 4, 4_000_000 ) { Slider = false };

	/// <summary>Hold open borders and the lines between materials. See
	/// <see cref="Decimate.Options.PreserveBoundary"/> — off, a shell's opening goes ragged.</summary>
	public readonly BoolParam HoldBorders = new( "Hold borders", true );

	/// <summary>Merge vertices that share a position first. See
	/// <see cref="Decimate.Options.Weld"/> — off, an unwelded export cannot be reduced at all.</summary>
	public readonly BoolParam Weld = new( "Weld first", true );

	public override IReadOnlyList<IParam> Parameters =>
		new IParam[] { Bodies, Target, Percent, Triangles, HoldBorders, Weld };

	/// <summary>The two protections are answered once, if ever. The target is the whole
	/// question.</summary>
	public override IReadOnlyList<IParam> AdvancedParameters =>
		new IParam[] { HoldBorders, Weld };

	/// <summary>
	/// What this will cost, for a dialog that can say "412,908 → 41,290" before you press anything.
	///
	/// The same question <see cref="SubdivideFeature.PredictCost"/> answers, and needed more here:
	/// subdivision's cost is arithmetic anybody can do in their head, and a percentage of a number
	/// you have never seen is not.
	/// </summary>
	public (int From, int To) PredictCost( IEnumerable<Body> bodies )
	{
		var from = 0;
		var to = 0;

		foreach ( var body in bodies.Where( Bodies.Matches ) )
		{
			var count = Decimate.TriangleCount( body.Mesh );

			from += count;
			to += Math.Clamp( TargetFor( count ), 4, count );
		}

		return (from, to);
	}

	int TargetFor( int triangles ) =>
		Target.Index == 1
			? Triangles.Clamped
			: (int)MathF.Round( triangles * Math.Clamp( Percent.Clamped, 0f, 100f ) / 100f );

	protected override void Execute( FeatureContext ctx )
	{
		var targets = ctx.Bodies.Where( Bodies.Matches ).ToList();

		if ( targets.Count == 0 )
		{
			Fail(
				"Nothing to remesh",
				"The bodies this feature names are not in the model at this point in the tree.",
				"Pick a part and try again",
				"Or clear the selection to remesh every part" );
		}

		var missed = new List<string>();
		var quads = 0;

		foreach ( var body in targets )
		{
			var before = Decimate.TriangleCount( body.Mesh );

			if ( before <= 4 )
				continue;

			// Counted before the reduce, since afterwards everything is triangles and the question
			// cannot be asked any more.
			if ( body.Mesh.Faces.Any( f => f.Count > 3 ) )
				quads++;

			var result = Decimate.Run( body.Mesh, new Decimate.Options
			{
				TargetTriangles = TargetFor( before ),
				Weld = Weld.Value,
				PreserveBoundary = HoldBorders.Value,
			} );

			body.Mesh = result.Mesh;

			if ( !result.ReachedTarget )
				missed.Add( $"{body.Name} stopped at {result.ToTriangles:N0}" );
		}

		// BOTH OF THESE ARE WARNINGS, and the model is built either way. See the class note: a
		// remesh that could not reach its budget still produced a smaller mesh, and a remesh over a
		// quad cage is a thing somebody may have meant.
		if ( missed.Count > 0 )
		{
			Warn(
				missed.Count == 1
					? "The target was not reached"
					: $"{missed.Count} parts did not reach the target",
				"There were no collapses left that keep the surface manifold and the right way out: "
					+ string.Join( ", ", missed ) + ".",
				"Take what it gave you — this is as small as the shape goes without tearing",
				"Or turn off Hold borders, if the part is open and its opening does not matter" );
		}
		else if ( quads > 0 )
		{
			Warn(
				quads == 1 ? "The part came back as triangles" : $"{quads} parts came back as triangles",
				"Remesh reduces a triangle mesh, and the part it ran on had quads in it. Quads are "
					+ "what subdivision and skinning want, so a cage that had them has lost them.",
				"Suppress this feature if the part was a cage rather than an import",
				"Keep it if the triangles are what you wanted — a collision hull, or a distant LOD" );
		}
	}
}