Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
76 commits
Select commit Hold shift + click to select a range
f70c2e3
fix(ci): replace inline python interpolation in version workflows (#123)
SashaRX Aug 6, 2026
8aac825
fix(ci): stop auto-committing built native binaries (#125)
SashaRX Aug 6, 2026
ab237f5
fix(tools): harden gen.bat argument forwarding (#135)
SashaRX Aug 6, 2026
9d2f97f
fix(docs): shell-escape skills-overhaul parameter file (#136)
SashaRX Aug 6, 2026
254e131
fix(packaging): mirror .gitignore exclusions into .npmignore (#164)
SashaRX Aug 6, 2026
7e05c11
fix(native): vendor xatlas instead of fetching a moving branch (#127)
claude Aug 6, 2026
ed32493
fix(repack): prevent atlas oversample size overflow (#129)
SashaRX Aug 6, 2026
a6667c5
fix(benchmark): escape CSV formula prefixes in sweep summaries (#157)
SashaRX Aug 6, 2026
0e50071
fix(fbx): escape CSV formula prefixes in metrics export (#159)
SashaRX Aug 6, 2026
e157498
fix(benchmark): escape CSV formula prefixes in benchmark records (#161)
SashaRX Aug 6, 2026
d183260
fix(tools): escape model name in gallery nav href (#158)
SashaRX Aug 6, 2026
8892f9a
refactor(editor): share one CsvUtil.Escape across the CSV writers (#1…
claude Aug 6, 2026
8fc1373
fix(collision): validate sidecar collision entries before mesh rebuil…
SashaRX Aug 6, 2026
3bfda6e
fix(repack): sanitize sidecar-supplied xatlas settings and save path …
SashaRX Aug 6, 2026
d333790
fix(replay): preserve optimized vertex colors during sidecar replay (…
SashaRX Aug 6, 2026
b8e7c8b
fix(replay): validate sidecar UV replay channels (#139)
SashaRX Aug 6, 2026
e6ef72b
fix(replay): validate sidecar submesh triangle counts (#126)
SashaRX Aug 6, 2026
95eef91
fix(replay): abort incomplete stale remap rebuilds (#188)
SashaRX Aug 6, 2026
dcfeddd
fix(replay): bound sidecar vertex remapping work (#185)
SashaRX Aug 6, 2026
5ade5c8
fix(arap): reject ARAP UVs outside float range (#134)
SashaRX Aug 6, 2026
b80baff
fix(benchmark): bound benchmark UV PNG generation (#137)
SashaRX Aug 6, 2026
b4e9f78
fix(transfer): drop partial UV2 output on cancellation (#156)
SashaRX Aug 6, 2026
c021b9e
fix(repack): prevent concurrent xatlas native sessions (#128)
SashaRX Aug 6, 2026
11474a5
fix(benchmark): validate sweep parameters before running (#138)
SashaRX Aug 6, 2026
0b1d245
fix(repack): cache mesh area preview instead of scanning per repaint …
claude Aug 6, 2026
49e1c87
fix(preview): guard shell color hash against int.MinValue (#168)
SashaRX Aug 6, 2026
7dbedc9
fix(transfer): bound grouped transfer overlap scan (#186)
SashaRX Aug 6, 2026
79decd4
fix(partition): bound spatial overlap detection work (#190)
SashaRX Aug 6, 2026
390f7cb
fix(scene): bound SceneView hover raycasts (#187)
SashaRX Aug 6, 2026
494b049
fix(lod): bound LOD indices parsed from object names (#142)
claude Aug 6, 2026
9d11d56
fix(lod): normalize LOD0 transition before generating levels (#179)
claude Aug 6, 2026
d57c3c6
fix(transfer): isolate cross-LOD hints per mesh group (#181)
SashaRX Aug 6, 2026
dab2c98
fix(collision): bound V-HACD recursion depth (#122)
SashaRX Aug 6, 2026
e24de00
fix(collision): validate V-HACD triangle indices (#152)
SashaRX Aug 6, 2026
504f84d
fix(fbx): avoid overflow in FBX backup path hashes (#162)
SashaRX Aug 6, 2026
fbd797d
fix(transfer): expose Apply UV2 after source-only repack (#170)
SashaRX Aug 6, 2026
13d50b6
fix(repack): keep post-pack density correction inside packed charts (…
SashaRX Aug 6, 2026
fe5c210
fix(repack): keep UV2 clamp toggle independently editable (#172)
SashaRX Aug 6, 2026
135f7c4
fix(repack): forward RepackUv rotate flag to xatlas (#173)
SashaRX Aug 6, 2026
0e7dd50
fix(ao): defer GPU AO cleanup until queued work completes (#140)
SashaRX Aug 6, 2026
8b5b283
fix(ao): guard AO blur against incomplete mesh attributes (#163)
SashaRX Aug 6, 2026
ddbadcc
fix(ao): bound Vertex AO seam matching work (#143)
claude Aug 6, 2026
c1eeb19
fix(ao): bound 3D AO blur work on dense meshes (#144)
claude Aug 6, 2026
42feec8
fix(ao): make atomic AO accumulation NaN-safe (#147)
SashaRX Aug 6, 2026
454d491
fix(ao): bound face-area AO correction on degenerate meshes (#148)
claude Aug 6, 2026
4a6ba2e
fix(ao): clear stale AO results before loading mesh data (#177)
SashaRX Aug 6, 2026
362bd23
fix(ao): prevent duplicate Vertex AO preview backups (#145)
SashaRX Aug 6, 2026
a2e2be8
fix(ao): guard vertex AO bake workload (#149)
claude Aug 6, 2026
3d898b1
fix(canvas): render Inverted triangles in validation overlay (#174)
claude Aug 6, 2026
40f0036
fix(export): avoid cumulative transform baking on shared FBX meshes (…
claude Aug 6, 2026
d0a6ba9
fix(hub): restore preview meshes before undo/redo refresh (#176)
claude Aug 6, 2026
543795e
fix(collision): actually strip extra vertex channels (#178)
claude Aug 6, 2026
24bb542
fix(repack): normalize UV0 shell winding at the repack boundary (#180)
claude Aug 6, 2026
e0d85e4
fix(transfer): reject incomplete source UV channels (#183)
claude Aug 6, 2026
e1218ad
fix(meshopt): guard buffer sizes before the unsafe native call (#184)
claude Aug 6, 2026
3e82c9d
docs(changelog): record consolidated security and bugfix sweep
claude Aug 6, 2026
78a6e7a
fix(export): destroy temporary mesh clones after FBX export (#175 fol…
claude Aug 6, 2026
2d84f15
fix(ci): harden checkout credentials and package.json validation (PR …
claude Aug 6, 2026
a3262de
fix(npm): exclude agent and workspace config from the published packa…
claude Aug 6, 2026
1424e67
fix(csv): flatten line breaks so one CSV record stays one physical li…
claude Aug 6, 2026
8bd14b2
fix(partition): dedupe vertex-pair keys of degenerate triangles (PR #…
claude Aug 6, 2026
d1c41bf
fix(lod): share LOD index parsing and apply the level cap where it bi…
claude Aug 6, 2026
cdaeae1
fix(postprocess): keep sidecar colors authoritative and abort partial…
claude Aug 6, 2026
d964ed2
fix(png): reject index lists that are not whole triangles (PR #191 re…
claude Aug 6, 2026
902566b
fix(ao): bound seam scanning by candidates examined and drop cancelle…
claude Aug 6, 2026
39565fc
fix(repack): report session contention as a result error, free the te…
claude Aug 6, 2026
62ac795
fix(benchmark): decouple atlas utilization from the PNG snapshot budg…
claude Aug 6, 2026
ed73bcc
fix(transfer): gate Apply UV2 on a repack that actually produced mesh…
claude Aug 6, 2026
043b0d2
fix(native): validate the remaining collision ABI parameters (PR #191…
claude Aug 6, 2026
6f98c59
fix(native): record the vendored xatlas upstream snapshot (PR #191 re…
claude Aug 6, 2026
388dcbd
fix(tools): validate the values gen.bat forwards to python (PR #191 r…
claude Aug 6, 2026
5eb9e3a
fix(preview): share the non-negative colour key helper (PR #191 review)
claude Aug 6, 2026
2eb1a5e
fix(native): rename xatlas VERSION file shadowing the C++ <version> h…
claude Aug 6, 2026
bf9013c
fix(export): destroy remaining temporary export meshes (leak cleanup)
claude Aug 6, 2026
b579d2f
fix(export): destroy sidecar-built collision meshes after FBX export …
claude Aug 6, 2026
9625a69
fix(transfer): create analysis material folders only when debug is en…
claude Aug 6, 2026
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
18 changes: 7 additions & 11 deletions .claude/skills/skills-overhaul-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -522,19 +522,15 @@ if [ -n "$NAMESPACE" ] && ! echo "$NAMESPACE" | grep -qE '^SashaRX\.'; then
echo "WARN: namespace '$NAMESPACE' does not match SashaRX.<Package> — migration required in Phase 2."
fi

# 0.7 Write the parameter file used by later phases
cat > /tmp/skills-overhaul.env <<EOF
REPO_NAME=$REPO_NAME
PACKAGE_ID=$PACKAGE_ID
DISPLAY_NAME=$DISPLAY_NAME
NAMESPACE=$NAMESPACE
UNITY_MIN=$UNITY_MIN
DEFAULT_BRANCH=$DEFAULT_BRANCH
EOF
# 0.7 Write shell-escaped parameters used by later phases.
# `declare -p` produces Bash syntax that cannot reinterpret repository values
# (for example, command substitutions in package.json) when the file is sourced.
declare -p REPO_NAME PACKAGE_ID DISPLAY_NAME NAMESPACE UNITY_MIN DEFAULT_BRANCH \
> /tmp/skills-overhaul.env
cat /tmp/skills-overhaul.env
```

**Verification:** `/tmp/skills-overhaul.env` is non-empty; working tree either clean or known-dirty; git tag created.
**Verification:** `/tmp/skills-overhaul.env` is non-empty and contains only `declare -- NAME=...` records; working tree either clean or known-dirty; git tag created.

**Rollback:** `git reset --hard <pre-overhaul-sha>` (SHA saved in `/tmp/pre-overhaul-sha.txt`), and `git tag -d pre-skills-overhaul-<date>`.

Expand Down Expand Up @@ -895,4 +891,4 @@ The overhaul is successful when, in each of the three repos:

1. **Run Phase 0 in each of the three repos and paste back the `/tmp/skills-overhaul.env` output for each.** This resolves the repo-identity question (are `UnityLodUvLightmapTransfer` / `lightmap-uv-tool` the same as `UnityMeshLab`?) and gives the plan the parameter values it needs. Estimated time: 3 minutes per repo.
2. **Decide the two policy questions in §5.2**: (a) namespace Stance A vs B and (b) whether to include directive "ALWAYS invoke" phrasing on the three most-critical skills. These decisions change only a handful of lines in `_shared/naming-conventions.md` and in three frontmatter descriptions.
3. **Execute Phase 1 (scaffold) in one repo as a pilot**, then Phase 2.1 (`_shared/` and `_checklists/` files) — these are repo-agnostic and can be copied identically to the other two repos once validated. Commit per the sub-commit plan. Only after that pilot runs cleanly through Phase 4 validation do you replicate to the remaining repos.
3. **Execute Phase 1 (scaffold) in one repo as a pilot**, then Phase 2.1 (`_shared/` and `_checklists/` files) — these are repo-agnostic and can be copied identically to the other two repos once validated. Commit per the sub-commit plan. Only after that pilot runs cleanly through Phase 4 validation do you replicate to the remaining repos.
53 changes: 9 additions & 44 deletions .github/workflows/build-native.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
name: Build Native Libraries

# This workflow deliberately has no repository write permission. Native artifacts
# must be reviewed and committed by a maintainer rather than published by CI.
permissions:
contents: read

on:
push:
paths:
Expand Down Expand Up @@ -33,7 +38,9 @@ jobs:
name: Build (${{ matrix.os }})

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false

- name: Configure CMake
run: cmake -S 'Native~' -B build ${{ matrix.cmake_args }} -DCMAKE_BUILD_TYPE=Release
Expand All @@ -42,50 +49,8 @@ jobs:
run: cmake --build build --config Release

- name: Upload artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: native-${{ matrix.os }}
path: ${{ matrix.plugin_dir }}/${{ matrix.artifact }}
if-no-files-found: error

commit:
needs: build
runs-on: ubuntu-latest
if: github.event_name == 'push'
permissions:
contents: write

steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Copy binaries to plugin directories
run: |
# Windows
mkdir -p Plugins/x86_64
cp artifacts/native-windows-latest/xatlas-unity.dll Plugins/x86_64/

# Linux
cp artifacts/native-ubuntu-latest/libxatlas-unity.so Plugins/x86_64/

# macOS (universal binary: x86_64 + arm64)
mkdir -p Plugins/macOS
cp artifacts/native-macos-latest/libxatlas-unity.dylib Plugins/macOS/

- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Plugins/
if git diff --cached --quiet; then
echo "No changes to native libraries"
else
git commit -m "Build native libraries (auto) [skip ci]"
git push
fi
9 changes: 6 additions & 3 deletions .github/workflows/meta-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,15 @@ jobs:
- uses: actions/checkout@v4

- name: Check package.json is valid JSON
run: python3 -c "import json; json.load(open('package.json'))"
# `jq empty` succeeds on a stream of several root values ("{...}{...}"),
# which every JSON consumer but jq rejects. Slurping and asserting a
# single object root is what "valid package.json" actually means.
run: jq -e -s 'length == 1 and (.[0] | type == "object")' package.json

- name: Check version format (semver)
run: |
VERSION=$(python3 -c "import json; print(json.load(open('package.json'))['version'])")
if echo "$VERSION" | grep -qP '^\d+\.\d+\.\d+$'; then
VERSION=$(jq -er '.version | select(type == "string")' package.json)
if [[ "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "Version: $VERSION"
else
echo "::error::Invalid version format: $VERSION (expected semver X.Y.Z)"
Expand Down
45 changes: 31 additions & 14 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,36 +22,53 @@ jobs:
- uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.GITHUB_TOKEN }}
persist-credentials: false

- name: Bump patch version in package.json
id: bump
run: |
CURRENT=$(python3 -c "import json; print(json.load(open('package.json'))['version'])")
# A file with several root values ("{...}{...}") passes `jq empty` but
# makes `jq -er .version` emit one line per document — validate the
# single-object root before anything reads or rewrites the manifest.
if ! jq -e -s 'length == 1 and (.[0] | type == "object")' package.json > /dev/null; then
echo "::error::package.json is not a single JSON object"
exit 1
fi
CURRENT=$(jq -er '.version | select(type == "string")' package.json)
if [[ ! "$CURRENT" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo "::error::Invalid version format: $CURRENT (expected semver X.Y.Z)"
exit 1
fi
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
# $((PATCH + 1)) wraps silently at the 64-bit boundary (it would turn
# 9223372036854775807 into a negative patch), so bound the input
# first. Compare digit count, not value: a numeric [ -ge ] test on a
# 20-digit patch is itself an "integer expression expected" error.
if [ "${#PATCH}" -gt 9 ]; then
echo "::error::Patch component out of range: $PATCH (expected < 1000000000)"
exit 1
fi
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="${MAJOR}.${MINOR}.${NEW_PATCH}"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
python3 -c "
import json
with open('package.json', 'r') as f:
data = json.load(f)
data['version'] = '${NEW_VERSION}'
with open('package.json', 'w') as f:
json.dump(data, f, indent=2)
f.write('\n')
"
TEMP_FILE=$(mktemp)
jq --arg version "$NEW_VERSION" '.version = $version' package.json > "$TEMP_FILE"
mv "$TEMP_FILE" package.json
echo "old=$CURRENT" >> "$GITHUB_OUTPUT"
echo "new=$NEW_VERSION" >> "$GITHUB_OUTPUT"

- name: Commit version bump
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OLD_VERSION: ${{ steps.bump.outputs.old }}
NEW_VERSION: ${{ steps.bump.outputs.new }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json
if git diff --cached --quiet; then
echo "No version change"
else
git commit -m "Bump version to ${{ steps.bump.outputs.new }} [skip ci]"
git push
echo "Bumped: ${{ steps.bump.outputs.old }} → ${{ steps.bump.outputs.new }}"
git commit -m "Bump version to ${NEW_VERSION} [skip ci]"
git push "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:main
echo "Bumped: ${OLD_VERSION} → ${NEW_VERSION}"
fi
42 changes: 42 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ CLAUDE.md
CLAUDE.md.meta
REVIEW.md
REVIEW.md.meta
.claude/
.codex/

# Editor/workspace config — git only
.vscode/
.prettierignore

# CI/CD & GitHub
.github/
Expand All @@ -18,3 +24,39 @@ CONFLICT_CHECK_LAST_5_PRS.md
CONFLICT_CHECK_LAST_5_PRS.md.meta
.gitignore
*.bat

# Local Unity-generated files (mirrors .gitignore because npm stops using it
# whenever this file exists)
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Bb]uild/
[Bb]uilds/
[Ll]ogs/
[Uu]ser[Ss]ettings/
*.csproj
*.sln
*.suo
*.user
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db

# Local OS and IDE files
.DS_Store
Thumbs.db
.idea/
.vs/
*.swp

# Build intermediates and temporary files
*.obj
*.lib
*.exp
test.txt
.commitmsg
commitmsg.txt
Comment thread
coderabbitai[bot] marked this conversation as resolved.
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- Repack tab: the `Advanced (debug)` section (manual texels-per-UV-unit, post-pack density correction, SymSplit threshold mode).
- Unity top menu: `Mesh Lab ▸ Export FBX Metrics (Selected Assets)` and `(Scene LODGroup)`.
- Assets ▸ Create context menu: `Mesh Lab ▸ Sweep Test Suite` (was always-visible `Lightmap UV Tool/Test Suite` via `[CreateAssetMenu]`).
- On-disk transfer-analysis artefacts: `BenchmarkRecorder.NewRun` returns a no-op scope when the toggle is off, so Run Full Pipeline / Repack / Transfer All no longer create `<projectRoot>/BenchmarkReports/` and its per-mesh `*_png/` subfolder on a production run. Existing report folders on disk are left untouched.
- **In-flight gate + `FireAndForget` helper** for fire-and-forget async UI actions. The "Run Full Pipeline" / "Run Repack only" / "Run Transfer only" / "Repack All" / "Transfer All Targets" buttons now sit inside an `EditorGUI.DisabledScope` on `_pipelineInFlight` so a second click can't launch an interleaving run; `FireAndForget` attaches `ContinueWith` on the Unity sync context to log Task faults through `UvtLog.Error`, release the gate, and call `UvProgress.Fail` so a thrown exception can't leave the strip stuck on a stale phase.
- **Cancel-aware benchmark recording** in `ExecTransferAllImpl` — mirrors the `completedSuccessfully` guard `ExecFullPipelineImpl` already uses. Cancelled transfers no longer emit stale `shellTransferResult` / validation rows that taint sweep aggregates.

Expand All @@ -37,6 +38,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
- `[CreateAssetMenu(menuName = "Lightmap UV Tool/Test Suite")]` from `TestSuiteAsset` — replaced with a gated `[MenuItem("Assets/Create/Mesh Lab/Sweep Test Suite")]`.
- Transfer tab decluttered: removed the misplaced `Generate LODs` section (full LOD-Gen UI lives in the dedicated LOD Gen tab), the `FBX Export` block (Export as New / Overwrite Source — both duplicates of the sidebar footer's `Export New FBX` / `Overwrite FBX`), and the `Save FBX from main (_main)` button (duplicate of the footer's `Backup from main`). Transfer tab now reads: per-LOD status list → `Transfer All Targets` → `Quality Report` → `Validation Overlay` → `Apply UV2` actions.

### Security
Consolidated hardening sweep — incorporates PRs #122–#123, #125–#129, #131, #134–#140, #142–#145, #147–#149, #151–#152, #156–#159, #161–#164, #168 and #170–#190.

- **CI and build tooling** — release/version workflows no longer interpolate untrusted values straight into inline Python and no longer auto-commit built native binaries; `gen.bat` and the skills-overhaul parameter file quote/escape their arguments; `.npmignore` mirrors `.gitignore` so ignored artifacts can't leak into a published tarball (#123, #125, #135, #136, #164).
- **Vendored xatlas** — `Native~/CMakeLists.txt` builds a pinned, vendored xatlas source tree instead of `FetchContent`-ing a moving upstream branch (#127).
- **Sidecar replay validation** — `Uv2DataAsset` payloads are now treated as untrusted input: UV channel dimensions and lengths, submesh triangle counts, collision entries, vertex-color arrays, xatlas settings and the sidecar-supplied save path are validated before use, stale/incomplete remap rebuilds abort instead of half-applying, and remap work is bounded (#126, #139, #151, #182, #185, #188, #189).
- **Resource limits** — bounded or overflow-checked work budgets across Vertex/GPU AO (bake workload, seam matching, 3D and 2D blur, face-area correction, NaN-safe atomic accumulation, deferred GPU cleanup), repack (atlas oversample sizing, pack cost, exclusive native xatlas sessions, cached area preview), grouped transfer and spatial overlap scans, SceneView hover raycasts, benchmark sweep parameters and UV PNG generation, ARAP UV ranges, FBX backup path hashing, V-HACD recursion depth and triangle indices, and the meshoptimizer packing buffers feeding the unsafe/native path (#122, #128–#129, #131, #134, #137–#138, #140, #143–#144, #147–#149, #152, #162–#163, #184, #186–#187, #190).
- **Output escaping** — a shared `CsvUtil.Escape` neutralises formula prefixes in every CSV writer (sweep summaries, FBX metrics, benchmark records) and the gallery generator escapes model names before embedding them in HTML hrefs (#157–#159, #161).

### Fixed
- **UV transfer / repack correctness** — Repack normalizes UV0 shell winding at its own API boundary instead of assuming the optional Weld stage ran; source meshes with incomplete (non-empty but short) UV0/UV2 channels are rejected before indexing; cancelled transfers drop their partial UV2 output; post-pack density correction stays inside each packed chart; `RepackUv` forwards its rotate flag to xatlas; the UV2 clamp toggle is independently editable; `Apply UV2` is reachable after a source-only repack; cross-LOD hints are isolated per mesh group (#128, #156, #170–#173, #180–#181, #183).
- **Undo and preview handling** — undo/redo restores preview-swapped meshes before rebuilding `MeshEntries` so a preview mesh can't become the cached baseline; Vertex AO no longer takes duplicate preview backups and clears stale results before loading new mesh data; the shell colour hash survives `int.MinValue`; Inverted triangles are rendered in the validation overlay instead of drawing clean or vanishing under the filter (#145, #168, #174, #176–#177).
- **LOD parsing and generation** — LOD indices parsed from object names are bounded, and LOD0's transition height is normalized before generating the remaining levels (#142, #179).
- **FBX export** — `NormalizeExportHierarchy` bakes each node's transform into a per-node mesh copy, so a shared FBX sub-asset is no longer mutated cumulatively (#175).
- **Collision meshes** — `StripCollisionMesh` calls `mesh.Clear(false)`; with the default `keepVertexLayout=true` the extra tangent/color/UV channels were retained and the strip was a no-op (#178).

## [1.0.5] - 2026-05-13

### Added
Expand Down
Loading
Loading