Library popover: much wider default, single-click open, hover-preview clips - #248
Open
OmarB97 wants to merge 12 commits into
Open
Library popover: much wider default, single-click open, hover-preview clips#248OmarB97 wants to merge 12 commits into
OmarB97 wants to merge 12 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>
The agent had no idea what was actually on screen beyond a mesh path and triangle count -- it couldn't tell you which clip was playing, what the model was named, or what game it belonged to. Extend both ends of that pipeline: - ChatPanel now mirrors the viewer's selected clip and full clip list (Viewer3D reports them into appStore alongside the existing meshStats/meshSelected pattern) into the /agent/chat context, so requests like "make the stroll slower" resolve without re-asking. - agent.py resolves the current mesh's name/project/tags from its .tags.json sidecar, falling back to the GLB's own embedded extras.modly when no sidecar exists, and renders them into the system prompt in the same style as the existing scene-context lines. Lineage: when mesh-exporter's input traces back to an existing workspace asset, record derived_from in the sidecar -- the source's workspace- relative path plus its name if it had one. The workflow runner threads a sourceAssetPath through NodeOutput/ProcessInput from wherever a mesh first resolves (the Load 3D Mesh node) down to whichever node eventually exports, so multi-step chains still carry it. Re-deriving from an already -derived asset keeps just the immediate parent plus the chain's root (read from the parent's own derived_from, if any) instead of growing an unbounded list. Surfaced in the chat context alongside name/project/tags. Also fixes an open question: the rig-and-animate extension's "Describe the movement" param sets multiline: true, a flag ParamField never checked -- it fell into the plain "string" case and rendered as a single -line input with a stray folder-browse button. ParamField now renders any multiline-flagged param as a resizable textarea (matching the existing "Extra details" box), independent of its base type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Search now force-expands every section that contains a match instead of requiring a manual Show click per group, and clearing the query falls straight back to whatever was collapsed before (collapsedSectionKeys is never mutated by the search itself). The listing area now flexes to fill a resizable popup instead of being capped at max-h-64 (roughly 3-4 rows) - a new bottom-edge drag handle mirrors the existing width handle, with its own clamp/persist pair so the height survives restarts the same way the width already does. Also fixes the search input rendering as an oversized box with the placeholder vertically centered in the middle of it - a known Chromium input[type=search] quirk, fixed with appearance-none like the rest of the app's inputs effectively already assume.
The panel width floor, default, and ceiling were all far too small (260/320/560) for a panel that is now full-height - a thumbnail, a readable name, its full workspace path, and its type badge could not share a row without truncating. Raised to 380/640/960 and, since the floor moved too, a panel already dragged down toward the old 280px-ish minimum on a prior run now clamps back up to something usable on the next launch instead of silently staying cramped. Selecting a Library row and then separately clicking "Open selected asset" is gone as a required two-step. A row click now selects and opens in one motion via a shared openLibraryEntry() helper; the button stays as a secondary affordance (useful to retry after an error) but is no longer the only path. Non-openable entries still just select and show their reason, same as before. Hover previews: on hover, a row with a rendered preview manifest swaps its static thumbnail for a looping WebP clip, with a small mini-select control to cycle clips when more than one exists - lazily fetched and cached per row, never fetched for rows with no manifest. This reuses the existing workspace:library:thumbnail IPC channel rather than adding a parallel one: an optional previewClip request field fetches a named clip's WebP instead of the static thumbnail, and the default (no previewClip) response now also carries the manifest's clip list when one exists. The manifest's file field is trusted only as a filename (basename-only) and re-resolved through the same normalizeWorkspaceAssetPath guard every other workspace read uses, so a malformed or stale manifest can never point outside the asset's own directory. No preview manifests exist yet (that pipeline is separate, in flight elsewhere) so today this is inert: previews stays empty and every row behaves exactly as it did before this change.
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.
Builds on feat/asset-library-ux (PR #247, still open) -- same head-branch-per-PR-against-main pattern as #242/#243/#244/#245/#246/#247. Scoped entirely to the Library popover in the Generate area; doesn't redo anything #247 already shipped (taller default, bottom drag handle with persisted height, search auto-expand, 40px thumbnails).
Problem
Follow-up feedback on the #247 popover, from real use:
Changes
assetLibraryUi.ts. The floor moved too, not just the default:getStoredAssetLibraryPanelWidth()clamps whatever's inlocalStoragethrough the current bounds, so a panel someone already dragged down toward the old ~260-280px floor in a prior session clamps back up to something usable on the next launch instead of silently staying cramped. At the new width a row's thumbnail, name, full workspace path, and type badge all sit on one line without truncating for realistic asset names -- verified against real workspace assets, not fixtures (see Verification). No other layout changes; the drag handle, persistence mechanism, and max-width viewport clamp from Rework the Library popover: usable default size, search auto-expand, drag-resizable height #247 are untouched.handleOpenSelectedLibraryEntryinGeneratePage.tsxis now backed by a sharedopenLibraryEntry(entry)helper; a row'sonActivatecalls it directly after selecting, and the "Open selected asset" button calls it via the existing selected-entry lookup. Non-openable entries still just select and surface their existing "why not" message -- only openable entries actually open. The button remains as a secondary affordance (useful to retry after a transient error) but is no longer the only path.‹ ›control appears on the tile to cycle clips when a model has more than one, without leaving the list. This reuses the existingworkspace:library:thumbnailIPC channel end to end rather than adding a parallel one:AssetLibraryThumbnailRequestgained an optionalpreviewClipfield;AssetLibraryThumbnailResult's success case gained an optionalpreviews: { clip, duration }[].previewClip) request behaves exactly as before, plus now attaches the manifest's clip list (name + duration only, no image bytes) when a<model>.previews.jsonmanifest exists beside the asset -- so the UI learns whether hover-to-animate is available for free, off the same eager thumbnail load every entry already does.previewClip: <name>fetches that one clip's<model>.preview-<clip>.webpas a data URL instead, lazily (only on hover, only once per clip per row) and cached per row for the row's lifetime.filefield is trusted only as a filename (basename()-only, any directory component discarded) and the reconstructed sibling path is re-validated through the samenormalizeWorkspaceAssetPathguard every other workspace-library read already uses, so a malformed or stale manifest can never point outside the asset's own directory. A non-.webpextension or a file that doesn't exist both fail closed the same way a missing thumbnail does today.previewsis absent from every response, no extra IPC calls happen, and every row renders and behaves exactly as it did before this PR. Confirmed on the real desktop build, not just by reading the code (see Verification).Files changed
src/areas/generate/assetLibraryUi.ts-- width floor/default/ceiling constants.src/areas/generate/assetLibraryUi.test.ts-- updated width-bound assertions, including the "already-narrow persisted value clamps up" case.src/areas/generate/GeneratePage.tsx--openLibraryEntryextraction + row-activation wiring; newAssetLibraryEntryRowcomponent (hover state, lazy per-clip fetch, mini cycle control);libraryPreviewsstate andfetchLibraryPreviewFrame.src/shared/types/assetLibrary.ts--AssetLibraryPreviewClip;previewClipon the thumbnail request;previewson the thumbnail result.src/shared/types/assetLibrary.test.ts-- type-shape coverage for the new request/result fields.electron/main/artifact-registry-service.ts-- preview manifest parsing, clip-path resolution (basename +normalizeWorkspaceAssetPathre-validation), and the extendedreadWorkspaceAssetLibraryThumbnail/IPC handler.electron/main/artifact-registry-service.test.ts-- manifest happy path, missing/malformed manifest, and traversal/wrong-extension safety tests for the new clip resolution.electron/preload/artifact-registry-preload.test.ts--previewClippassthrough over the existing channel.src/areas/generate/assetLibraryService.test.ts-- renderer-sidepreviewClippassthrough.Verification
Built, packaged (
npm run package, AppImage target), and installed the same way the currently-running build was installed: asar extracted intoresources/app, original parked asapp.asar.orig, prior install backed up before swapping in.GET /workspace/...for that exact asset, fired by the one click..previews.jsonmanifest plus two small real WebP images beside one existing workspace asset (not shipped with this PR -- removed immediately after verification): hovering swapped the static thumbnail for the first clip's image, the‹ ›control appeared, and clicking it swapped to the second clip's image without opening the asset (confirmed no newGET /workspace/...request fired for that click). Removed the fixtures and refreshed: hovering the same row went back to exactly the old behavior, no leftover control, no errors.npm run lintandnpm test(test:py+test:node, all green) both pass.Test plan
npm run lintnpm testnpm run buildnpm run package(AppImage) + real install + manual verification on the desktop app, including a temporary hand-built preview manifest to exercise the hover/cycle path end to end