// PresetStep.cs — the Preset step, first in the chain. The model importer preset of the // model's recipe (Project Settings ▸ Mesh Lab ▸ Prepare: Recipes, SM_ models by default) is // applied to the model, all but the properties that hold the model's own links: its material // remaps (m_ExternalObjects), the IDs its prefabs and variants find its meshes by // (m_InternalIDToNameTable, m_UsedFileIDs), its avatar's human description (m_HumanDescription) // and, unless asked, its LOD screen percentages. The plan lists each // setting that differs, was → becomes, as the line's details; Apply applies only those and // reimports the model (IImporterStep: before its variant is opened). A model that already // matches the preset is left as it is. using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using UnityEditor; using UnityEditor.Presets; using Object = UnityEngine.Object; namespace SashaRX.UnityMeshLab { internal sealed class PresetStep : IImporterStep { internal const string StepName = "Preset"; const string Check = "Importer"; const string LodScreenPercentages = "m_LODScreenPercentages"; /// /// The importer properties a preset never sets: the model's remaps, the IDs its prefabs /// reference meshes by, and its avatar's human description (its own bones and scale). /// internal static readonly string[] Kept = { "m_ExternalObjects", "m_InternalIDToNameTable", "m_UsedFileIDs", "m_HumanDescription" }; readonly Preset preset; readonly string[] excluded; /// The model importer preset; none skips the step. /// Whether the preset's LOD screen percentages are applied too. internal PresetStep(Preset preset, bool applyLodScreenPercentages = false) { this.preset = preset; excluded = applyLodScreenPercentages ? Kept : Kept.Append(LodScreenPercentages).ToArray(); } public string Name => StepName; public string Purpose => "The model's importer takes the recipe's preset, except its remaps, mesh IDs, avatar description and LOD percentages."; public string SkipReason(PrepareContext context) { if (context.Target.Kind != PrepareTargetKind.ModelAsset) return "an importer preset is applied to the model: prepare the FBX"; if (preset == null) return "the recipe sets no importer preset (Project Settings ▸ Mesh Lab ▸ Prepare: Recipes)"; if (!(AssetImporter.GetAtPath(context.Target.AssetPath) is ModelImporter importer)) return "the model has no model importer"; return preset.CanBeAppliedTo(importer) ? null : $"'{preset.name}' is not a model importer preset"; } public void Plan(PrepareContext context, PrepareReport report) => Run(context, report, false); public void Apply(PrepareContext context, PrepareReport report) => Run(context, report, true); void Run(PrepareContext context, PrepareReport report, bool apply) { string path = context.Target.AssetPath; var importer = (ModelImporter)AssetImporter.GetAtPath(path); // The preset's own exclusions (its Inspector's) are never applied either. var differences = Differences(preset, importer, excluded.Concat(preset.excludedProperties ?? Array.Empty())); if (differences.Count == 0) { report.Add(PrepareLevel.Unchanged, $"'{path}'", Check, $"the importer already matches '{preset.name}'"); return; } report.Add(PrepareLevel.Changed, $"'{path}'", Check, $"{MeshValidation.Plural(differences.Count, "setting", "settings")} from '{preset.name}'", differences.Select(d => $"{d.path}: {d.was} → {d.becomes}").ToList()); if (!apply) return; if (!preset.ApplyTo(importer, differences.Select(d => d.path).ToArray())) { report.Add(PrepareLevel.Warning, $"'{path}'", Check, $"'{preset.name}' could not be applied; the importer is as it was"); return; } importer.SaveAndReimport(); } /// /// The preset's properties whose value on differs from the /// preset's, with both values; a property under one of is left out. /// internal static List<(string path, string was, string becomes)> Differences(Preset preset, Object target, IEnumerable excluded) { var kept = excluded.ToList(); var current = new SerializedObject(target); var differences = new List<(string path, string was, string becomes)>(); foreach (var modification in preset.PropertyModifications) { string path = modification.propertyPath; if (kept.Any(k => path == k || path.StartsWith(k + ".", StringComparison.Ordinal))) continue; var property = current.FindProperty(path); if (property != null && Same(property, modification)) continue; string becomes = modification.objectReference != null ? modification.objectReference.name : modification.value; differences.Add((path, property != null ? Value(property) : "(none)", becomes)); } return differences; } static bool Same(SerializedProperty property, PropertyModification modification) { switch (property.propertyType) { case SerializedPropertyType.ObjectReference: return property.objectReferenceValue == modification.objectReference; case SerializedPropertyType.Float: return double.TryParse(modification.value, NumberStyles.Float, CultureInfo.InvariantCulture, out double f) && Math.Abs(property.doubleValue - f) <= 1e-6 * Math.Max(1, Math.Abs(f)); case SerializedPropertyType.Boolean: return (property.boolValue ? "1" : "0") == modification.value; case SerializedPropertyType.Integer: case SerializedPropertyType.Enum: case SerializedPropertyType.ArraySize: case SerializedPropertyType.LayerMask: return long.TryParse(modification.value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long n) && property.longValue == n; default: return Value(property) == modification.value; } } static string Value(SerializedProperty property) { switch (property.propertyType) { case SerializedPropertyType.ObjectReference: return property.objectReferenceValue != null ? property.objectReferenceValue.name : "(none)"; case SerializedPropertyType.Float: return property.doubleValue.ToString("G7", CultureInfo.InvariantCulture); case SerializedPropertyType.Boolean: return property.boolValue ? "1" : "0"; case SerializedPropertyType.String: return property.stringValue; case SerializedPropertyType.Integer: case SerializedPropertyType.Enum: case SerializedPropertyType.ArraySize: case SerializedPropertyType.LayerMask: return property.longValue.ToString(CultureInfo.InvariantCulture); default: return property.propertyType.ToString(); } } } }