feat(animations): custom .vrma folder in Settings → Directories - #45
Merged
rosspeili merged 2 commits intoAug 8, 2026
Merged
Conversation
The Animations row in Settings → Directories was greyed out as "coming soon" while config.yaml already reserved directories.animations. Wire it up with the same replace semantics as Avatars: point it at a folder and Gear → Animations lists those clips only, hiding the bundled Default sequence and the whole VRMA Motion Pack. Scan mirrors the avatars/environments path — scanAnimations in user-library.cjs behind library:scan-animations, flat top-level scan, .vrma only, file names to labels. A folder with no .vrma is not applied (error notice, previous source kept). Selection is stored as the scanned id, which hashes the file path, so it survives restarts and rescans; a clip gone since last launch falls back to the first in the folder, and returning the row to Default restores the bundled `default` sequence. The catalog was reachable only as a module singleton: getAnimationById, resolveVrmaUrl and getSelectableAnimations closed over animationCatalog, and BarCommandMenu and VrmAvatar called them with no way to inject one. They now take the catalog to read. createVrmaResolver exists because playSequence does ids.map(resolveUrl) — handing it the two-arity resolveVrmaUrl would pass the array index in as the catalog and resolve nothing. Reading a clip that fails is skipped and counted rather than costing the whole folder; a folder whose clips are all unreadable reports that instead of claiming it holds no .vrma. Note this is narrow: a file the scan cannot stat (permission-denied) never reaches the read at all and reads as an empty folder, which the docs now say. Two leaks found while testing the swap path: bailing out mid-load dropped the only references to blob urls already minted in that loop, and useVrmAnimation's cache keys on url, so a custom folder minting fresh blob urls per rescan grew it without bound. Both bounded now. Custom clips are labelled from file names, so the Animations menu title-cases them to match the bundled labels. To get the lookups under test, config/animations.js is split: vrmaAssets holds the Vite-resolved .vrma imports, animationLookup the pure lookups, and animations composes both and re-exports them — the public API and every call site are unchanged. The split is what makes them loadable by plain `node --test`, which cannot resolve a .vrma import; npm test now covers src/**/*.test.mjs and CONTRIBUTING documents the constraint. Renderer paths needing React, the DOM or URL.createObjectURL still have no automated coverage — loadLibraryAnimations and the AvatarStage effect were verified by hand against a real Electron build on an isolated profile (scan, catalog swap, id resolution, restart persistence, empty and unreadable folders, reset). Refs ARPAHLS#23 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ns row The shot still showed the row greyed out with ".vrma — coming soon" and the "Custom animations — coming soon" note under it, which now contradicts both the code and the surrounding docs. Replace it with the row in Custom mode — hint, active select, path line, and Change folder / Open / Reset — so it matches what the text describes. Referenced from docs/using-the-app.md and docs/user-settings.md. Refs ARPAHLS#23 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
|
Thanks @AUDOSt0ck1ng, this looks good for start for 23, merging as-is. Will open a follow-up to flip the menu to additive (bundled always available + custom appended, scroll, custom accent, filter) on top of this plumbing, not a change request on this PR. |
2 tasks
Collaborator
Author
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements #23, including the docs request added in the issue comment. Left for you to close — the UI extensibility point raised in the thread is unresolved and may be worth keeping the issue open for.
What
Settings → Directories → Animations is no longer greyed out. Point it at a folder of
.vrmafiles and Gear → Animations lists those clips instead of the bundled catalog — replace semantics, matching Avatars, so the Default sequence and the VRMA Motion Pack are hidden while Custom is active.Flat top-level scan,
.vrmaonly, file names become labels. A folder with no.vrmais not applied (error notice, previous source kept). The selection persists as the scanned id, which hashes the file path, so it survives restarts and rescans; a clip gone since last launch falls back to the first in the folder, and returning the row to Default restores the bundleddefaultsequence.Why it touches more than the Directories row
The catalog was only reachable as a module singleton —
getAnimationById/resolveVrmaUrl/getSelectableAnimationsclosed overanimationCatalog, andBarCommandMenuandVrmAvatarcalled them with no way to inject one. They now take the catalog to read.createVrmaResolveris not ceremony:playSequencedoesids.map(resolveUrl), so handing it the now-two-arityresolveVrmaUrlwould pass the array index in as the catalog and silently resolve nothing. A test maps the resolver over an array specifically to lock that.Two leaks surfaced while testing the swap path and are fixed here: bailing out mid-load dropped the only references to blob urls already minted in that loop, and
useVrmAnimation's cache keys on url — a custom folder minting fresh blob urls per rescan grew it without bound.Test scope change
To get the lookups under test,
config/animations.jsis split intovrmaAssets.js(Vite-resolved.vrmaimports),animationLookup.js(pure lookups), and a composing entry point that re-exports both. Public API and every call site are unchanged. The split is what makes them loadable by plainnode --test, which cannot resolve a.vrmaimport.npm testnow also runssrc/**/*.test.mjs, and CONTRIBUTING gains a Where tests go section documenting the constraint and the split pattern.How tested
npm run lint,npm test(48 pass),npm run build— all clean, and CI green on the fork.Automated — 13 new tests. Main process (6):
.vrmafiltering and sort,.vrm/.vrmaisolation including case, flat scan, empty/missing/non-directory/null paths, id stability plus byte correctness, and read-throws for a vanished file. Lookups (7): catalog isolation between bundled and custom ids, never returningundefined,resolveVrmaUrlanswering only forvrmaentries, the.map()footgun,selectablehandling, and allresolveAnimationIdfallbacks.By hand — real Electron production build against an isolated
--user-data-dirso no live config was touched. Verified: scan + IPC + blob load + catalog swap + id resolution (a.vrmdecoy and a nested clip correctly excluded), a custom clip actually retargeting onto the avatar, restart persistence of a mid-list clip, empty-folder rejection, per-file skip, and the bundled Default sequence still mapping all 7 pack ids to their own distinct assets after the split.loadLibraryAnimationsand theAvatarStageeffect need React / DOM /URL.createObjectURLand have no automated coverage — they are covered by the manual pass above only.Notes for review
Entering Custom commits one render with an empty catalog, so the avatar rests until the folder finishes loading. Kept deliberately — it matches Avatars (which shows no model at all while loading), and falling back to the bundled catalog meanwhile would flash the Default greeting.
One behavioural sharp edge worth knowing, documented rather than fixed: a
.vrmathe app cannotstat(permission-denied) is dropped by the scan itself, so a folder of only those reports "no.vrmafiles" rather than a read error. The per-file skip-and-count path only covers clips that vanish between the scan and the load.Docs updated: VRMA guide (incl. the Bring your own
.vrmaauthoring/licensing section requested in the issue), using-the-app, user-settings, manual-testing, assets-and-credits, architecture overview, project layout, roadmap, README, CHANGELOG, CONTRIBUTING, and the animation issue template.Follow-ups (not in this PR)
Both are pre-existing and affect all three Directories rows equally:
config.yaml— the hydrateskipNextSaveconsumes that save — so the error notice returns every launch.🤖 Generated with Claude Code