Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 11 additions & 3 deletions Editor/Assets/SidecarStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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) ──

Expand Down Expand Up @@ -703,11 +709,12 @@ static bool Invalid(string message, out string error)

// ── Tool settings ──

/// <summary>The transfer tool settings stored in the sidecar at <paramref name="sidecarPath"/>, or null.</summary>
/// <summary>The transfer tool settings saved in the sidecar at <paramref name="sidecarPath"/>; null when there are none.</summary>
internal static ToolSettings LoadSettings(string sidecarPath)
{
if (string.IsNullOrEmpty(sidecarPath)) return null;
return AssetDatabase.LoadAssetAtPath<Uv2DataAsset>(sidecarPath)?.toolSettings;
var data = AssetDatabase.LoadAssetAtPath<Uv2DataAsset>(sidecarPath);
return data != null && HasSettings(data) ? data.toolSettings : null;
}

/// <summary>
Expand All @@ -722,6 +729,7 @@ internal static void SaveSettings(string sidecarPath, Action<ToolSettings> edit)
if (data == null) return;
if (data.toolSettings == null) data.toolSettings = new ToolSettings();
edit(data.toolSettings);
data.toolSettings.saved = true;
Save(data);
}
}
Expand Down
12 changes: 8 additions & 4 deletions Editor/Prepare/PresetStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -23,8 +24,11 @@ internal sealed class PresetStep : IImporterStep
const string Check = "Importer";
const string LodScreenPercentages = "m_LODScreenPercentages";

/// <summary>The importer properties a preset never sets: the model's remaps and the IDs its prefabs reference meshes by.</summary>
internal static readonly string[] Kept = { "m_ExternalObjects", "m_InternalIDToNameTable", "m_UsedFileIDs" };
/// <summary>
/// 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).
/// </summary>
internal static readonly string[] Kept = { "m_ExternalObjects", "m_InternalIDToNameTable", "m_UsedFileIDs", "m_HumanDescription" };

readonly Preset preset;
readonly string[] excluded;
Expand All @@ -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)
{
Expand Down
17 changes: 13 additions & 4 deletions Editor/Prepare/SurfaceBinding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Comment thread
SashaRX marked this conversation as resolved.
Resolved
{
// 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;
}

/// <summary>The type of that full or short name derived from <paramref name="baseType"/>; the full name wins.</summary>
Expand Down
6 changes: 6 additions & 0 deletions Editor/Uv2DataAsset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ public class ToolSettings
// Pipeline
public bool saveNewMeshAssets = true;
public string savePath = "Assets/UnityMeshLab/Output";

/// <summary>
/// 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.
/// </summary>
public bool saved;
}

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading
Loading