Skip to content

feat(animations): custom .vrma folder in Settings → Directories - #45

Merged
rosspeili merged 2 commits into
ARPAHLS:mainfrom
AUDOSt0ck1ng:feat/custom-animations-directory
Aug 8, 2026
Merged

feat(animations): custom .vrma folder in Settings → Directories#45
rosspeili merged 2 commits into
ARPAHLS:mainfrom
AUDOSt0ck1ng:feat/custom-animations-directory

Conversation

@AUDOSt0ck1ng

Copy link
Copy Markdown
Collaborator

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 .vrma files 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, .vrma only, file names become labels. A folder with no .vrma is 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 bundled default sequence.

Why it touches more than the Directories row

The catalog was only reachable as a module singleton — getAnimationById / resolveVrmaUrl / getSelectableAnimations closed over animationCatalog, and BarCommandMenu and VrmAvatar called them with no way to inject one. They now take the catalog to read.

createVrmaResolver is not ceremony: playSequence does ids.map(resolveUrl), so handing it the now-two-arity resolveVrmaUrl would 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.js is split into vrmaAssets.js (Vite-resolved .vrma imports), 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 plain node --test, which cannot resolve a .vrma import.

npm test now also runs src/**/*.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): .vrma filtering and sort, .vrm / .vrma isolation 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 returning undefined, resolveVrmaUrl answering only for vrma entries, the .map() footgun, selectable handling, and all resolveAnimationId fallbacks.

By hand — real Electron production build against an isolated --user-data-dir so no live config was touched. Verified: scan + IPC + blob load + catalog swap + id resolution (a .vrm decoy 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.

loadLibraryAnimations and the AvatarStage effect need React / DOM / URL.createObjectURL and 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 .vrma the app cannot stat (permission-denied) is dropped by the scan itself, so a folder of only those reports "no .vrma files" 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 .vrma authoring/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:

  • On launch, a bad folder correctly falls back to Default in state, but the correction is not written back to config.yaml — the hydrate skipNextSave consumes that save — so the error notice returns every launch.
  • Re-picking the same folder does not rescan (the effect keys on the path), so edits on disk need a different folder or a restart. Documented as current behaviour.

🤖 Generated with Claude Code

AUDOSt0ck1ng and others added 2 commits August 8, 2026 21:24
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>
@rosspeili

Copy link
Copy Markdown
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.

@rosspeili
rosspeili merged commit 285c03d into ARPAHLS:main Aug 8, 2026
1 check passed
@AUDOSt0ck1ng

Copy link
Copy Markdown
Collaborator Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants