Desktop ergonomics: zoom, panel resize, motion bar, exporter tagging - #245
Open
OmarB97 wants to merge 9 commits into
Open
Desktop ergonomics: zoom, panel resize, motion bar, exporter tagging#245OmarB97 wants to merge 9 commits into
OmarB97 wants to merge 9 commits into
Conversation
Rigged models (from the rigging extensions, or any GLB with clips) render frozen in bind pose today: the viewer loads the glTF but never creates an AnimationMixer, so the clips it just parsed never run. That reads as a failed rig rather than a missing feature. Create a mixer for models that carry clips, tick it from useFrame, and dispose it with the model. When a file carries more than one clip, offer a small picker so each motion can be reviewed without leaving the app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Asset rows now render their pre-rendered <name>.thumb.png beside the filename instead of showing bare text, falling back to the existing text-only row when no thumbnail exists. The Library panel can also be resized by dragging its edge, with the chosen width persisted across restarts.
…' into feat/desktop-ergonomics
npm ci fails on main because the committed lockfile is stale against package.json; npm install (what upstream CI itself falls back to) brings it back in sync. Committing the result so ci is usable again on this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ctrl/Cmd with +, -, or 0 now scales the whole window like a browser, clamped to [-2, 4] and persisted to <userData>/ui-zoom.json so it survives restarts. This extends the existing before-input-event handler (where the macOS quit shortcut already lives) and calls webContents.setZoomLevel directly, deliberately not CSS zoom: a CSS transform scales the rendered pixels without telling the 3D viewport, so pointer math there (raycasting, gizmo dragging) goes wrong as soon as the page is scaled. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The panel's resize handle was a 1px strip with no resting affordance — easy to miss entirely. Widen it to 2px with a visible resting tint plus a stronger hover/active highlight, mark it up as a proper separator (role, aria-orientation, title) matching the Library panel's handle, raise the width cap from 520 to 900 so wide parameter forms have room, and persist the chosen width to localStorage (modly-panel-width) the same way the Library panel already remembers its own width. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Parameter labels used a truncating 5rem column that clipped anything longer than a couple of words; widen it to 8rem and let it wrap. - The prompt textarea was a fixed 3-row, non-resizable box; make it 14 rows by default and user-resizable (resize-y, 14rem min-height). - Say plainly which Generate inputs are required vs optional: "Your sketch or photo -- required" and "Extra details -- optional". - Add a text param type (a plain input, unlike the existing string type folder picker) so free-text fields like the exporter new name, project, and tags params render as usable text boxes instead of falling through to a numeric input. - Render clickable tag-suggestion pills beneath any tags param: slugified words from that node typed name/project fields, plus a small fixed game-asset vocabulary, deduped and capped at 10. Clicking a pill toggles it in the comma-separated value; no network calls. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The clip picker was a raw select mounted via drei Html at the model origin, recreating the whole AnimationMixer every time the clip changed (a hard cut, not a blend) and easy to lose behind the scene. Replace it with a proper bottom-left overlay component (MotionBar), matching the existing ViewerToolbar/stats-overlay idiom: - The mixer and active AnimationAction now live for as long as the model does; switching clips fades the old action out and the new one in over 0.25s instead of recreating anything. - GltfMeshModel reports its clip list up to Viewer3D (the same onStats/onObject callback pattern already used here), which owns the active index and renders MotionBar as a plain sibling of the Canvas. - Zero clips renders nothing, exactly one clip shows its name read-only, more than one gets a labelled dropdown plus a Next button that cycles with a cross-fade. All playback loops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The built-in exporter named every file export-<epoch-ms>.glb, so a folder of work told you nothing. Add three optional params: model_name (used to slug the file name, falling back to a dated model-YYYY-MM-DD-HHMM name), project (files land under Exports/<project-slug>/ so a game groups together instead of one long list), and tags. Output is always <slug>-<4-char-unique>.<ext> so re-exports never collide. Tags are the operator typed list plus what the file itself can tell: rigged (has skins), animated plus clip-<name> per animation, textured, and one of low-poly/mid-poly/high-poly by triangle count, plus the project slug. Tags are written both as a <file>.tags.json sidecar and embedded under the glTF asset extras (extras.modly), so they travel with the file either way. Ported from the already-working implementation at /mnt/tank/modly/app/extracted/resources/builtin-extensions/mesh-exporter on the operator machine. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 28, 2026
Open
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.
Summary
Ports a set of desktop ergonomics fixes into real source. These were previously applied to the built app as post-hoc patches against the extracted AppImage; every one of them is now a normal source change, so a future app update no longer silently erases them. Builds on top of #242 (animated model playback) and #243 (Library thumbnails and resize), both merged in unchanged.
What changed
+/-/0scales the whole window like a browser, clamped to [-2, 4] and persisted to<userData>/ui-zoom.json. Implemented in the main process againstwebContents.setZoomLevel, not CSS zoom, because a CSS transform does not tell the 3D viewport it happened, and pointer math there (raycasting, gizmo dragging) then reads wrong coordinates.role,aria-orientation,title), capped at 900px, and its width persists tolocalStoragethe same way the Library panel already does.resize-y, 14rem min-height); wasresize-none.<select>injected at the model origin, hard-cutting between clips by tearing down and recreating the wholeAnimationMixer. Rebuilt as a real React overlay (bottom-left, matching the existing toolbar/stats-overlay pattern): the mixer now lives for as long as the model does, clip switches cross-fade over 0.25s, and the clip list is reported up to the viewer the same way mesh stats already are. Zero clips renders nothing, one clip shows its name read-only, more than one gets a dropdown plus a "Next" button. All playback loops.mesh-exportergainsmodel_name,project, andtagsparams. Files land at<Exports>/<project-slug>/<name-slug>-<4-char-unique>.<ext>, falling back to a dated name when unnamed. Tags are what the operator typed plus what the file itself can tell (rigged,animated+clip-<name>per clip,textured, a poly-count tier, the project slug), written both as a<file>.tags.jsonsidecar and embedded underextras.modlyin the glTF itself. Ported from the already-working implementation running on the operator's machine, not reinvented.tagsfield in the Generate form now shows clickable suggestion pills underneath it — slugified words from that node's typed name/project fields, plus a small fixed game-asset vocabulary (prop,character,creature,environment,weapon,furniture,hero-asset,background,low-poly,stylized,realistic), deduped and capped at 10. Clicking toggles the tag in the comma-separated value. Fully client-side, no network calls.textparam type (distinct fromstring, which renders a folder picker) so free-text fields like the exporter's new ones render as plain inputs instead of falling through to a numeric one.Test plan
npm install && npm run build— clean, no errors.npm run lint— 0 problems.npm test— 23/23 Python tests, 40/40 Node tests, all passing.npm run package(electron-builder, AppImage target) and installed as the actual app the operator launches daily, replacing the previous extracted build (backed up first).+/-/0; UI visibly scaled andui-zoom.jsonupdated on disk each time..thumb.pngthumbnail images, not placeholders.squid-seamless-loop.glb); motion bar showed a dropdown with both clip names (idle_breathe,startle_flick) and a Next button; clicking Next cycled and cross-faded between them, then wrapped back around.Risk / rollback
All changes are additive to existing components; nothing upstream of these files changes behavior for models/flows that do not touch them. The riskiest piece is the motion bar rewrite (
GltfMeshModelinViewer3D.tsx) since it replaces the mixer lifecycle from #242 rather than only extending it — verified above with a real two-clip model, including the cross-fade and wraparound. If something regresses, reverting this branch on top of #242+#243 restores the prior (single mixer teardown per clip, DOM<select>) behavior.Notes for reviewers
textparam type is new to the codebase (no other manifest currently declares it); it is additive to the existingParamSchemaunion and only changes rendering for nodes that opt into it.npm cicurrently fails againstpackage.jsononmain(stale lockfile);npm install— what upstream CI already falls back to — regenerated it.