diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04b84d06..4bf130bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -56,6 +56,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- Reverse UV: the audit report is written only at the Verbose log level (the last 20 are kept) and a failed write no longer fails the transfer; the audit itself still gates every result. The ancestry kept per mesh and saved in the sidecar is a compact record of per-face overlap flags, charts, chain keys and overlap relations instead of the node's whole report (earlier sidecars are read and rewritten on restore), and the viewer's quality check no longer hashes it every frame.
### Fixed
+- A model's sidecar no longer counts as holding UV Transfer settings when none were saved in it. Unity gives every sidecar a settings block with the defaults, so selecting a model whose sidecar had none applied those defaults over the current settings, and a sidecar left with nothing else (after a UV2 reset, say) was kept instead of deleted. Settings saved before this change count when they differ from the defaults.
+- Prepare's Preset step no longer applies the preset's avatar description (the importer's human description: bone mapping and its scale): it is the model's own, as its remaps are.
+- Prepare's surface binding reports a warning, instead of failing, when Unity does not add the surface component to a collider's object (a script in an editor assembly, say).
- Reset UV2 (the hub's Reset, UV Transfer's Reset UV2 and Reset Pipeline State) no longer drops the collision nodes the sidecar places at the root: the scene objects and prefabs kept in place for them would have moved when the import stopped placing them. Clear Collision Placement is the way to drop them. A moved mesh on a prefab's root is no longer compensated twice in a variant built on that prefab.
- A model whose names contain "_Col" inside a word ("University_Inside_Column_U", "Chair_COLOR") is no longer treated as collision: Prefab Builder listed every node of it as COL, warned that the root name has a LOD/COL suffix, and Cleanup and the collider helpers could take a LOD node for the collision node (put its mesh on the root's MeshCollider, disable its renderer). Collision nodes are found by the naming rule (`_COL`, `_COL_*`, `_Collider`, `_Collision`) everywhere.
- A collision node off the root's placement no longer gives the root a misplaced collider. The Cleanup tab's collider fix put the first `_COL` child's mesh on the root's MeshCollider as it was, so a `_COL` offset from the root (Base_Concrete_COL at 0.392, 0, 0.002, say) collided that far away from its mesh. The fix now adds the MeshCollider to the `_COL` node itself when it sits off the root's placement (and to the root when it sits at it, within 0.1 mm in the world), and an enabled collider with a mesh on an active collision node counts as the model's collider (an empty one, on the root or a node, is filled instead of counting). An inactive offset `_COL` is activated for its collider (its renderer stays off, and on a prefab instance the change is kept as an override); one under an inactive node is skipped with a warning. Prefab Builder's buttons that put a collision mesh on the root refuse a mesh from such a node, naming its offset.
diff --git a/CLAUDE.md b/CLAUDE.md
index 2223943f..26900333 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -38,7 +38,7 @@ See `AGENTS.md` for shared rules that apply to all AI agents.
- **Bench:** `Editor/Bench/` — `SweepRunner` (matrix → cells → pipeline runs through `ISweepHost`), `BenchmarkRunner` (suite cases → spawn → techniques, through `IBenchmarkHost`), `BenchmarkRecorder`, `BenchmarkSweep` (aggregate, winner, gallery, manifest, archive), `TestSuiteAsset`, `CsvUtil`. A tab implements the host interfaces; it never loops over cells itself
- **Diagnostics:** `Editor/Diagnostics/` — `DebugUi` (the one Show Debug UI gate `Enabled`, the DEBUG banner, the log-filter block; `IUvToolDebugOnly` marks a tab the hub hides while the setting is off), `BakeHealth` (a Remesh & Bake node's health as a report: summary, scale check, warnings), `HierarchicalDiag`, `FbxMetricsExporter`. The Diagnostics tab (`Editor/Tools/DiagnosticsTool.cs`) hosts the sweep, the benchmark, the log filters and the reports; never read `showDebugUI` or draw a debug banner in a tool
- **Assets:** `Editor/Assets/` — `TextureAssets` (a texture from pixels, PNG/EXR encoding, file writes, image read-back, map importer configuration), `FbxExport` (every FBX write: the isolated channel re-save, the LOD-rebuild hierarchy passes, the atomic write, the post-reimport relink; UV / vertex-colour saves are delegated to `FbxChannelWrite`, which edits the FBX document through the FBX SDK via `FbxDocumentEdit` and pairs corners with vertices via `FbxCornerMatch` — never re-export a file to change a channel; generated LODs, sidecar collision and edited faces go into the same document through `FbxStructureWrite` / `FbxStructureEdit`, placed via `FbxSpaceFit`, built as `FbxMeshData`; a collision node's offset from the root is baked into its mesh by `FbxCollisionPlacement` (checked by a trial import before the file is replaced) or, in sidecar mode, on every import by `Editor/Mesh/CollisionRootPlacement` from the sidecar's list (the FBX untouched); both keep the objects that use a moved mesh, in scenes and prefab assets, in place through `CollisionConsumers`, with UV set names read by `FbxUvSetNames`) and `SidecarStore` (the `_uv2data.asset` sidecar: UV2 entries, collision hulls, collision placed at the root, tool settings). Tools never call `ModelExporter` or open a `Uv2DataAsset` themselves
-- **Prepare:** `Editor/Prepare/` — `PrepareReport` (lines at Unchanged / Changed / Warning / Error; an Error blocks Apply), `ModelRequirements` (each check Off / Warning / Error), `MeshValidation` (one mesh: degenerate, tiny faces, short edges, …; `IsConcave`), `ModelFileChecks` (import scale, units, node scale, size, smoothing groups), `Editor/Assets/FbxPolygonChecks` (the FBX's own polygons: concave ones), `ModelLodChecks` (LOD chain by group key, sibling names) and `ModelValidation` (a model asset or scene object, collision meshes held to the geometry checks). Steps: `IPrepareStep` (Plan changes nothing, Apply makes the change; each into its own `PrepareReport`, with `SkipReason`), run on a `PrepareTarget` (prefab asset, scene object with Undo, model asset) through `PrepareContext` edits by `PrepareRunner` (Preview, Apply refused while a plan has an Error; an `ICheckStep`'s errors on a model whose importer an `IImporterStep` changes wait for the reimport, then stop the steps after it); `ValidateStep` (first: `ModelValidation` against the recipe's `ModelRequirements`); which steps and settings a target takes is its `PrepareRecipe`'s (Project Settings: the first recipe whose name / folder pattern matches the target or its model; `PrepareChain` names it, a step it turns off or every step when none matches is a `SkippedStep` with the reason; one `SM_*` recipe by default); `ColliderStep` (a MeshCollider per `_COL`, the largest on the root, renderers removed, LOD fallback), `PrefabStep` (a model asset's Prefab Variant next to it, opened by `PrefabOutput`: the variant's contents or a new instance, saved after the steps; a prefab or scene object is changed in place), `PresetStep` and `MaterialStep` (`IImporterStep`s, applied before the variant opens: the recipe's importer preset minus remaps, mesh IDs and LOD percentages, each differing setting listed; each unremapped material slot → the project's .mat of that name, the nearest by folder) and `SurfaceBinding` (`ISurfaceBinding`; `ComponentSurfaceBinding` by type names from Project Settings, the surface from part, material or model name). A recipe's Run on Import (off by default) prepares a model on its first import through `PrepareOnImport`, told of each import by `Uv2AssetPostprocessor` (never add a model postprocessor: it reimports every model) and noted under Library/ for the main editor's `PrepareOnImportWatcher` (imports may run in a worker); `PrepareBatch.Run` is the `-executeMethod` batch entry. The Prepare tab (`Editor/Tools/PrepareTool.cs`) shows the chain, the selection's recipes and each run's `PrepareResult`, a step left out of one run through `PrepareChain.Without`. A prepare step reports into a `PrepareReport`; never add a tool-local issue list, and never name a game type in code (a surface goes through `ISurfaceBinding`)
+- **Prepare:** `Editor/Prepare/` — `PrepareReport` (lines at Unchanged / Changed / Warning / Error; an Error blocks Apply), `ModelRequirements` (each check Off / Warning / Error), `MeshValidation` (one mesh: degenerate, tiny faces, short edges, …; `IsConcave`), `ModelFileChecks` (import scale, units, node scale, size, smoothing groups), `Editor/Assets/FbxPolygonChecks` (the FBX's own polygons: concave ones), `ModelLodChecks` (LOD chain by group key, sibling names) and `ModelValidation` (a model asset or scene object, collision meshes held to the geometry checks). Steps: `IPrepareStep` (Plan changes nothing, Apply makes the change; each into its own `PrepareReport`, with `SkipReason`), run on a `PrepareTarget` (prefab asset, scene object with Undo, model asset) through `PrepareContext` edits by `PrepareRunner` (Preview, Apply refused while a plan has an Error; an `ICheckStep`'s errors on a model whose importer an `IImporterStep` changes wait for the reimport, then stop the steps after it); `ValidateStep` (first: `ModelValidation` against the recipe's `ModelRequirements`); which steps and settings a target takes is its `PrepareRecipe`'s (Project Settings: the first recipe whose name / folder pattern matches the target or its model; `PrepareChain` names it, a step it turns off or every step when none matches is a `SkippedStep` with the reason; one `SM_*` recipe by default); `ColliderStep` (a MeshCollider per `_COL`, the largest on the root, renderers removed, LOD fallback), `PrefabStep` (a model asset's Prefab Variant next to it, opened by `PrefabOutput`: the variant's contents or a new instance, saved after the steps; a prefab or scene object is changed in place), `PresetStep` and `MaterialStep` (`IImporterStep`s, applied before the variant opens: the recipe's importer preset minus remaps, mesh IDs, the avatar's human description and LOD percentages, each differing setting listed; each unremapped material slot → the project's .mat of that name, the nearest by folder) and `SurfaceBinding` (`ISurfaceBinding`; `ComponentSurfaceBinding` by type names from Project Settings, the surface from part, material or model name). A recipe's Run on Import (off by default) prepares a model on its first import through `PrepareOnImport`, told of each import by `Uv2AssetPostprocessor` (never add a model postprocessor: it reimports every model) and noted under Library/ for the main editor's `PrepareOnImportWatcher` (imports may run in a worker); `PrepareBatch.Run` is the `-executeMethod` batch entry. The Prepare tab (`Editor/Tools/PrepareTool.cs`) shows the chain, the selection's recipes and each run's `PrepareResult`, a step left out of one run through `PrepareChain.Without`. A prepare step reports into a `PrepareReport`; never add a tool-local issue list, and never name a game type in code (a surface goes through `ISurfaceBinding`)
- **Native:** `Plugins/` binaries, `Native~/` C++ source
- **Sidecar:** `Uv2DataAsset` persists UV2/collision data alongside FBX
diff --git a/Editor/Assets/SidecarStore.cs b/Editor/Assets/SidecarStore.cs
index 73c389dd..6bd0142c 100644
--- a/Editor/Assets/SidecarStore.cs
+++ b/Editor/Assets/SidecarStore.cs
@@ -180,7 +180,13 @@ static bool IsEmpty(Uv2DataAsset data)
=> (data.entries == null || data.entries.Count == 0)
&& (data.collisionEntries == null || data.collisionEntries.Count == 0)
&& (data.collisionAtRoot == null || data.collisionAtRoot.Count == 0)
- && data.toolSettings == null;
+ && !HasSettings(data);
+
+ // Settings saved (or, in a sidecar written before ToolSettings.saved, differing from the
+ // defaults): Unity gives every sidecar a ToolSettings, so a default unsaved one is none.
+ static bool HasSettings(Uv2DataAsset data)
+ => data.toolSettings != null
+ && (data.toolSettings.saved || JsonUtility.ToJson(data.toolSettings) != JsonUtility.ToJson(new ToolSettings()));
// ── Collision placed at the root (sidecar mode) ──
@@ -703,11 +709,12 @@ static bool Invalid(string message, out string error)
// ── Tool settings ──
- /// The transfer tool settings stored in the sidecar at , or null.
+ /// The transfer tool settings saved in the sidecar at ; null when there are none.
internal static ToolSettings LoadSettings(string sidecarPath)
{
if (string.IsNullOrEmpty(sidecarPath)) return null;
- return AssetDatabase.LoadAssetAtPath(sidecarPath)?.toolSettings;
+ var data = AssetDatabase.LoadAssetAtPath(sidecarPath);
+ return data != null && HasSettings(data) ? data.toolSettings : null;
}
///
@@ -722,6 +729,7 @@ internal static void SaveSettings(string sidecarPath, Action edit)
if (data == null) return;
if (data.toolSettings == null) data.toolSettings = new ToolSettings();
edit(data.toolSettings);
+ data.toolSettings.saved = true;
Save(data);
}
}
diff --git a/Editor/Prepare/PresetStep.cs b/Editor/Prepare/PresetStep.cs
index 699119a4..06ceda1b 100644
--- a/Editor/Prepare/PresetStep.cs
+++ b/Editor/Prepare/PresetStep.cs
@@ -2,7 +2,8 @@
// 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) and, unless asked, its LOD screen percentages. The plan lists each
+// (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.
@@ -23,8 +24,11 @@ internal sealed class PresetStep : IImporterStep
const string Check = "Importer";
const string LodScreenPercentages = "m_LODScreenPercentages";
- /// The importer properties a preset never sets: the model's remaps and the IDs its prefabs reference meshes by.
- internal static readonly string[] Kept = { "m_ExternalObjects", "m_InternalIDToNameTable", "m_UsedFileIDs" };
+ ///
+ /// 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;
@@ -39,7 +43,7 @@ internal PresetStep(Preset preset, bool applyLodScreenPercentages = false)
public string Name => StepName;
- public string Purpose => "The model's importer takes the recipe's preset, except its remaps, mesh IDs and LOD percentages.";
+ 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)
{
diff --git a/Editor/Prepare/SurfaceBinding.cs b/Editor/Prepare/SurfaceBinding.cs
index 6c1fc3a2..c9ffdb25 100644
--- a/Editor/Prepare/SurfaceBinding.cs
+++ b/Editor/Prepare/SurfaceBinding.cs
@@ -109,8 +109,9 @@ public void Bind(PrepareContext context, GameObject host, string surface, string
report.Add(PrepareLevel.Warning, subject, Check, $"no {data.Name} asset named '{surface}' ({origin} says {surface})");
return;
}
- report.Add(PrepareLevel.Changed, subject, Check, $"{component.Name} '{asset.name}' ({origin} says {surface})");
- if (apply) Set(context, existing != null ? existing : context.AddComponent(host, component), asset, subject, report);
+ // Applied, the change is reported once it is made.
+ if (!apply || Set(context, existing != null ? existing : context.AddComponent(host, component), asset, subject, report))
+ report.Add(PrepareLevel.Changed, subject, Check, $"{component.Name} '{asset.name}' ({origin} says {surface})");
}
// A surface already set stays: the binding is the game's to change.
@@ -123,18 +124,26 @@ bool Keeps(Component existing, string surface, string origin, string subject, Pr
return true;
}
- void Set(PrepareContext context, Component target, Object asset, string subject, PrepareReport report)
+ // Whether the surface was set; a line says why not.
+ bool Set(PrepareContext context, Component target, Object asset, string subject, PrepareReport report)
{
+ if (target == null)
+ {
+ // Unity refused to add it (a script in an editor assembly, say) and said why in the Console.
+ report.Add(PrepareLevel.Warning, subject, Check, $"Unity did not add {component.Name} (see the Console); its surface is not set");
+ return false;
+ }
var serialized = new SerializedObject(target);
var property = serialized.FindProperty(field);
if (property == null || property.propertyType != SerializedPropertyType.ObjectReference)
{
report.Add(PrepareLevel.Warning, subject, Check, $"{component.Name} has no serialized object field '{field}'; its surface is not set");
- return;
+ return false;
}
property.objectReferenceValue = asset;
if (context.UsesUndo) serialized.ApplyModifiedProperties();
else serialized.ApplyModifiedPropertiesWithoutUndo();
+ return true;
}
/// The type of that full or short name derived from ; the full name wins.
diff --git a/Editor/Uv2DataAsset.cs b/Editor/Uv2DataAsset.cs
index 1bff56d7..fad88c2a 100644
--- a/Editor/Uv2DataAsset.cs
+++ b/Editor/Uv2DataAsset.cs
@@ -210,6 +210,12 @@ public class ToolSettings
// Pipeline
public bool saveNewMeshAssets = true;
public string savePath = "Assets/UnityMeshLab/Output";
+
+ ///
+ /// Set when the settings are saved. Unity never leaves a serialized class field null, so a
+ /// sidecar that never had settings saved still holds a ToolSettings with the defaults.
+ ///
+ public bool saved;
}
///
diff --git a/README.md b/README.md
index c727cb54..0a8c09f4 100644
--- a/README.md
+++ b/README.md
@@ -128,7 +128,7 @@ One tab for assembling a prefab: **Hierarchy** (rebuild the LODGroup from `_LOD{
## Prepare
-Runs the prepare chain on what is selected in the Project window or a scene: **Validate** (the recipe's requirements; an error blocks Apply), **Preset** (the recipe's importer preset, except remaps, mesh IDs and LOD percentages), **Materials** (each unremapped slot to the project's material of that name), **Colliders** (a MeshCollider per `_COL`, the largest on the root, renderers removed, each collider's surface) and **Prefab** (an FBX's Prefab Variant next to it). Each target takes the first recipe its name and folder match (Project Settings ▸ Mesh Lab ▸ Prepare: Recipes); a step's card can leave it out of one run. **Preview** changes nothing; **Apply** makes the changes, and the **Result** page shows each target's steps and their findings.
+Runs the prepare chain on what is selected in the Project window or a scene: **Validate** (the recipe's requirements; an error blocks Apply), **Preset** (the recipe's importer preset, except remaps, mesh IDs, the avatar description and LOD percentages), **Materials** (each unremapped slot to the project's material of that name), **Colliders** (a MeshCollider per `_COL`, the largest on the root, renderers removed, each collider's surface) and **Prefab** (an FBX's Prefab Variant next to it). Each target takes the first recipe its name and folder match (Project Settings ▸ Mesh Lab ▸ Prepare: Recipes); a step's card can leave it out of one run. **Preview** changes nothing; **Apply** makes the changes, and the **Result** page shows each target's steps and their findings.
A recipe can also run by itself: with **Run on Import** on (off by default) a model it matches is prepared the first time it is imported. For a whole project or folder from the command line:
diff --git a/Tests/Editor/CollisionRootPlacementTests.cs b/Tests/Editor/CollisionRootPlacementTests.cs
index a97bd269..672f3e65 100644
--- a/Tests/Editor/CollisionRootPlacementTests.cs
+++ b/Tests/Editor/CollisionRootPlacementTests.cs
@@ -169,13 +169,18 @@ public void DeleteKeepingCollisionPlacement_KeepsOnlyTheNodesPlacedAtTheRoot()
try
{
SidecarStore.SetCollisionAtRoot(fbx, new[] { "Seat_COL[0]" });
+ Assert.IsNull(SidecarStore.LoadSettings(sidecar), "a sidecar no settings were saved in has none");
SidecarStore.SaveSettings(sidecar, settings => { });
- Assert.IsNotNull(SidecarStore.LoadSettings(sidecar));
+ Assert.IsNotNull(SidecarStore.LoadSettings(sidecar), "saved settings, the defaults included");
Assert.AreEqual(1, SidecarStore.DeleteKeepingCollisionPlacement(new[] { fbx }));
CollectionAssert.AreEqual(new[] { "Seat_COL[0]" }, SidecarStore.CollisionAtRoot(fbx), "a UV2 reset keeps the nodes placed at the root");
Assert.IsNull(SidecarStore.LoadSettings(sidecar), "and drops the rest of the sidecar");
+ var kept = SidecarStore.Load(fbx);
+ kept.toolSettings = new ToolSettings { atlasResolution = 2048 }; // as saved before ToolSettings.saved
+ SidecarStore.Save(kept);
+ Assert.AreEqual(2048, SidecarStore.LoadSettings(sidecar)?.atlasResolution, "older settings count when they differ from the defaults");
}
finally { AssetDatabase.DeleteAsset(sidecar); }
}
diff --git a/Tests/Editor/PrepareCollidersTests.cs b/Tests/Editor/PrepareCollidersTests.cs
index b442c68f..8bd9098b 100644
--- a/Tests/Editor/PrepareCollidersTests.cs
+++ b/Tests/Editor/PrepareCollidersTests.cs
@@ -6,6 +6,7 @@
using System.Linq;
using NUnit.Framework;
using UnityEngine;
+using UnityEngine.TestTools;
namespace SashaRX.UnityMeshLab.Tests
{
@@ -269,6 +270,34 @@ public void ComponentBinding_PutsTheSurfaceAssetOnEachColliderAndKeepsOneAlready
Assert.IsFalse(again.Changes, again.ToString());
}
+ [Test]
+ public void ComponentBinding_ReportsAComponentUnityDoesNotAdd()
+ {
+ var wood = ScriptableObject.CreateInstance();
+ wood.name = "Wood";
+ owned.Add(wood);
+ var settings = new SurfaceBindingSettings
+ {
+ componentType = typeof(PrepareEditorOnlySurface).FullName, field = "_material", dataType = nameof(PrepareTestSurfaceData),
+ };
+ var binding = new ComponentSurfaceBinding(settings, (type, name, near) => name == "Wood" ? wood : null);
+ var root = Root("Bench");
+ var seat = Node(root, "Seat_Wood_COL", Box("Seat_Wood_COL", 1), new Vector3(0, 1, 0));
+
+ // Unity says in the Console why it adds no editor script to an object.
+ LogAssert.ignoreFailingMessages = true;
+ try
+ {
+ var result = Apply(root, new ColliderStep(binding));
+
+ Assert.IsNull(seat.GetComponent());
+ var line = result.Steps.Single().Report.Lines.Single(l => l.check == ComponentSurfaceBinding.Check);
+ Assert.AreEqual(PrepareLevel.Warning, line.level, "no change is reported for it: " + result);
+ StringAssert.Contains("Unity did not add PrepareEditorOnlySurface", line.message);
+ }
+ finally { LogAssert.ignoreFailingMessages = false; }
+ }
+
[Test]
public void ComponentBinding_SaysWhyItCannotBind()
{
diff --git a/Tests/Editor/PrepareEditorOnlySurface.cs b/Tests/Editor/PrepareEditorOnlySurface.cs
new file mode 100644
index 00000000..c487e313
--- /dev/null
+++ b/Tests/Editor/PrepareEditorOnlySurface.cs
@@ -0,0 +1,13 @@
+// PrepareEditorOnlySurface.cs — a surface component in the editor tests' assembly, which Unity
+// will not add to an object (an editor script): the binding reports it instead of failing.
+using UnityEngine;
+
+namespace SashaRX.UnityMeshLab.Tests
+{
+ public class PrepareEditorOnlySurface : MonoBehaviour
+ {
+ [SerializeField] PrepareTestSurfaceData _material;
+
+ public PrepareTestSurfaceData Material => _material;
+ }
+}
diff --git a/Tests/Editor/PrepareEditorOnlySurface.cs.meta b/Tests/Editor/PrepareEditorOnlySurface.cs.meta
new file mode 100644
index 00000000..437e8eb7
--- /dev/null
+++ b/Tests/Editor/PrepareEditorOnlySurface.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1a73412c219a4b9893d58b8399e2484b
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tests/Editor/PrepareMaterialsTests.cs b/Tests/Editor/PrepareMaterialsTests.cs
index 5725d9a9..489d37d8 100644
--- a/Tests/Editor/PrepareMaterialsTests.cs
+++ b/Tests/Editor/PrepareMaterialsTests.cs
@@ -89,11 +89,14 @@ public void Apply_RemapsTheSlotAndMakesTheVariantOnceThenChangesNothing()
Assert.IsTrue(first.Applied && first.Changes, first.ToString());
var importer = (ModelImporter)AssetImporter.GetAtPath(fbxPath);
+ // By the asset: the reimport may give the material a new instance.
+ string projectPath = $"{folder}/M_Wood_Oak.mat";
var remap = importer.GetExternalObjectMap().Single(kv => kv.Key.type == typeof(Material) && kv.Key.name == "M_Wood_Oak");
- Assert.AreSame(project, remap.Value);
+ Assert.AreEqual(projectPath, AssetDatabase.GetAssetPath(remap.Value));
var variant = AssetDatabase.LoadAssetAtPath($"{folder}/Bench.prefab");
Assert.IsNotNull(variant, "the variant next to the model");
- Assert.AreSame(project, variant.transform.Find("Seat_LOD0").GetComponent().sharedMaterial, "the variant shows the remapped material");
+ Assert.AreEqual(projectPath, AssetDatabase.GetAssetPath(variant.transform.Find("Seat_LOD0").GetComponent().sharedMaterial),
+ "the variant shows the remapped material");
Assert.IsNotNull(variant.transform.Find("Seat_COL").GetComponent());
var second = PrepareRunner.Apply(target, steps);
@@ -137,7 +140,7 @@ public void Preset_AppliesTheDifferingSettingsButNotTheModelsRemaps()
importer = (ModelImporter)AssetImporter.GetAtPath(fbxPath);
Assert.AreEqual(2f, importer.globalScale);
- Assert.AreSame(mine, importer.GetExternalObjectMap()[slot], "the model keeps its own remap");
+ Assert.AreEqual($"{folder}/Mine.mat", AssetDatabase.GetAssetPath(importer.GetExternalObjectMap()[slot]), "the model keeps its own remap");
Assert.IsFalse(PrepareRunner.Preview(target, new IPrepareStep[] { step }).Changes, "a second run changes nothing");
}
diff --git a/Tests/Editor/SashaRX.UnityMeshLab.Tests.Editor.asmdef b/Tests/Editor/SashaRX.UnityMeshLab.Tests.Editor.asmdef
index 898b6670..de61a49f 100644
--- a/Tests/Editor/SashaRX.UnityMeshLab.Tests.Editor.asmdef
+++ b/Tests/Editor/SashaRX.UnityMeshLab.Tests.Editor.asmdef
@@ -3,6 +3,7 @@
"rootNamespace": "SashaRX.UnityMeshLab.Tests",
"references": [
"SashaRX.UnityMeshLab.Editor",
+ "SashaRX.UnityMeshLab.Tests.Fixtures",
"Autodesk.Fbx",
"UnityEngine.TestRunner",
"UnityEditor.TestRunner"
diff --git a/Tests/Fixtures.meta b/Tests/Fixtures.meta
new file mode 100644
index 00000000..0aa8d5fc
--- /dev/null
+++ b/Tests/Fixtures.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: b1b9002fda0d45a0a0314aef3c89e5f4
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tests/Editor/PrepareTestSurface.cs b/Tests/Fixtures/PrepareTestSurface.cs
similarity index 64%
rename from Tests/Editor/PrepareTestSurface.cs
rename to Tests/Fixtures/PrepareTestSurface.cs
index 3cdc0c95..7ec00906 100644
--- a/Tests/Editor/PrepareTestSurface.cs
+++ b/Tests/Fixtures/PrepareTestSurface.cs
@@ -1,5 +1,6 @@
// PrepareTestSurface.cs — a stand-in for a game's surface component (a ballistic material, say)
-// for the surface binding's tests: an object field that takes a PrepareTestSurfaceData.
+// for the surface binding's tests: an object field that takes a PrepareTestSurfaceData. Outside
+// the editor tests' assembly: Unity adds no component whose script is in an editor assembly.
using UnityEngine;
namespace SashaRX.UnityMeshLab.Tests
@@ -8,6 +9,6 @@ public class PrepareTestSurface : MonoBehaviour
{
[SerializeField] PrepareTestSurfaceData _material;
- internal PrepareTestSurfaceData Material => _material;
+ public PrepareTestSurfaceData Material => _material;
}
}
diff --git a/Tests/Editor/PrepareTestSurface.cs.meta b/Tests/Fixtures/PrepareTestSurface.cs.meta
similarity index 100%
rename from Tests/Editor/PrepareTestSurface.cs.meta
rename to Tests/Fixtures/PrepareTestSurface.cs.meta
diff --git a/Tests/Editor/PrepareTestSurfaceData.cs b/Tests/Fixtures/PrepareTestSurfaceData.cs
similarity index 100%
rename from Tests/Editor/PrepareTestSurfaceData.cs
rename to Tests/Fixtures/PrepareTestSurfaceData.cs
diff --git a/Tests/Editor/PrepareTestSurfaceData.cs.meta b/Tests/Fixtures/PrepareTestSurfaceData.cs.meta
similarity index 100%
rename from Tests/Editor/PrepareTestSurfaceData.cs.meta
rename to Tests/Fixtures/PrepareTestSurfaceData.cs.meta
diff --git a/Tests/Fixtures/SashaRX.UnityMeshLab.Tests.Fixtures.asmdef b/Tests/Fixtures/SashaRX.UnityMeshLab.Tests.Fixtures.asmdef
new file mode 100644
index 00000000..d19d8cfb
--- /dev/null
+++ b/Tests/Fixtures/SashaRX.UnityMeshLab.Tests.Fixtures.asmdef
@@ -0,0 +1,16 @@
+{
+ "name": "SashaRX.UnityMeshLab.Tests.Fixtures",
+ "rootNamespace": "SashaRX.UnityMeshLab.Tests",
+ "references": [],
+ "includePlatforms": [],
+ "excludePlatforms": [],
+ "allowUnsafeCode": false,
+ "overrideReferences": false,
+ "precompiledReferences": [],
+ "autoReferenced": false,
+ "defineConstraints": [
+ "UNITY_INCLUDE_TESTS"
+ ],
+ "versionDefines": [],
+ "noEngineReferences": false
+}
diff --git a/Tests/Fixtures/SashaRX.UnityMeshLab.Tests.Fixtures.asmdef.meta b/Tests/Fixtures/SashaRX.UnityMeshLab.Tests.Fixtures.asmdef.meta
new file mode 100644
index 00000000..f6ddb9f3
--- /dev/null
+++ b/Tests/Fixtures/SashaRX.UnityMeshLab.Tests.Fixtures.asmdef.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 378b19a186364bb295a9855bb3afc5c2
+AssemblyDefinitionImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Tools~/compile_check.py b/Tools~/compile_check.py
index 2bd65cdc..4a6d3d95 100644
--- a/Tools~/compile_check.py
+++ b/Tools~/compile_check.py
@@ -20,7 +20,7 @@
Unity build sees and this check cannot.
The Unity Test Framework is a separate package, absent from Unity3D.SDK. Its
-UnityTest marker and LogAssert.Expect signatures are stubbed only in the generated
+UnityTest marker and LogAssert surface (Expect, ignoreFailingMessages) are stubbed only in the generated
compile-check sources; coroutine execution and log assertions are verified by the
real Unity Test Runner.
@@ -105,6 +105,7 @@
{
public static void Expect(UnityEngine.LogType type, string message) { }
public static void Expect(UnityEngine.LogType type, System.Text.RegularExpressions.Regex message) { }
+ public static bool ignoreFailingMessages { get; set; }
}
}
"""
diff --git a/Tools~/run-unity-coverage.ps1 b/Tools~/run-unity-coverage.ps1
index 4e6afd2b..081baa79 100644
--- a/Tools~/run-unity-coverage.ps1
+++ b/Tools~/run-unity-coverage.ps1
@@ -124,6 +124,15 @@ try {
if ($text.Length -gt 800) { $text = $text.Substring(0, 800) + '...' }
Write-Output "FAILED $($case.fullname): $text"
if ($stack) { ($stack.InnerText.Trim() -split "`n" | Select-Object -First 3) | ForEach-Object { Write-Output " $($_.Trim())" } }
+ # The last lines the test logged: a warning there often says why it failed.
+ $logged = $case.SelectSingleNode('output')
+ if ($logged -and $logged.InnerText.Trim()) {
+ ($logged.InnerText.Trim() -split "`n" | Select-Object -Last 20) | ForEach-Object {
+ $line = $_.Trim()
+ if ($line.Length -gt 300) { $line = $line.Substring(0, 300) + '...' }
+ Write-Output " | $line"
+ }
+ }
}
}
throw "Unity coverage failed with exit code $($process.ExitCode)"