Skip to content

fix(canvas): hide masks under previews again; tidy Layers panel visibility and nesting - #10111

Merged
joshistoast merged 13 commits into
mainfrom
fix/layer-eyes-and-staging-masks
Nov 5, 2026
Merged

joshistoast merged 13 commits into
mainfrom
fix/layer-eyes-and-staging-masks

Conversation

@joshistoast

@joshistoast joshistoast commented Nov 2, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Masks hide during inpaint renders again. Since feat: UX refinement pass: undo-first actions, Invoke card routing, queue cache popover, canvas without the generation lock, layered project settings #10074, inpaint masks and regional guidance stayed drawn over a progress or staged preview whenever a mask or region was selected — the normal state after painting an inpaint mask. They are now hidden under a shown preview regardless of selection, and appear above it only while a brush, eraser or box/freehand shape drag is editing the selected mask or region, or once a move drag passes its drag threshold (a plain click shows nothing); they hide again on release or cancel. The selected mask or region stays loaded while hidden, so a stroke on it starts at once. Pan, zoom, color picker, frame and selection gestures never reveal them, and a locked or disabled mask (or a selected group, except for a move) reveals nothing.
  • One visibility column in the Layers panel. Layer, group and child (modifier) rows now end in an eye beside the lock, aligned with the stack headers' eye and + buttons at every nesting depth. Child rows replace their leading on/off dot with the same eye; a canvas-only hidden layer shows its closed eye just left of the eye.
  • Tree connector lines. Groups connect to their members and layers to their adjustment/modifier rows with rounded file-tree branches (none from the stack headers). Each nesting level indents 28px so a branch fits between parent and child.
  • Group thumbnails fill their box. A group's preview pile sizes its cards by how many it draws, so a one-member group shows a full-size thumbnail.
  • Staging bar status. "N results pending" is plain text, separated from the step controls by the bar's divider instead of a pill.

Layers panel

Related Issues / Discussions

User report: "when rendering with inpaint, the inpaint mask doesn't automatically hide while rendering."

QA Instructions

  1. Paint an inpaint mask and keep it selected, then Invoke (or stage a result). The mask is hidden under the progress image and staged result.
  2. With a result staged, paint a brush stroke on the mask: all masks show over the result during the stroke and hide on release. Hiding the preview in the staging bar shows them again.
  3. In the Layers panel, add masks, regional guidance, control and raster layers, a group and an adjustment: every eye sits in one column, locks in the next, header eye/+ above them.
  4. Nest a layer with adjustments two groups deep: connector lines drop from under each parent's thumbnail into its children (├ for middle children, └ for the last), and each one-member group's thumbnail fills its box.
  5. Stage two or more results: the status reads as plain text followed by a divider.

Checks: pnpm check:release passes (lint, typecheck, unit, browser, fixtures, performance architecture/build/contracts/browser, project files, accessibility). Changed interactions verified in a browser against the mock backend; the progress-preview case was not seen live (the mock cannot generate) and is covered by engine tests. The later move-threshold reveal and the loaded-while-hidden mask are covered by engine and composite tests, not seen live.

Review

Resolved findings: transform and gradient no longer claim to edit masks; brush and shape reveal masks only when the press will actually edit the selected mask, and move only once its drag passes the threshold (no flash on locked or hidden layers, none when the brush would create a new layer); tests now check a frame mid-gesture for pan, Space-hold and colour-picker presses; header-to-row column alignment shares constants and has a browser test. Remaining limits: the row's hover fill starts after the indent gutter (clicks in the gutter still select); dragging sideways to change depth takes 28px per level; groups nested beyond about five levels overflow a narrow panel. Moving a multi-selection whose primary layer is a raster moves an included mask while it is hidden. Edits that no revealing gesture carries, such as undoing a mask stroke or closing a polygon, change nothing visible while a preview hides the masks.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Meaningful regression coverage added / updated where needed; obsolete tests/code removed
  • Persisted-state and API changes include required migrations / compatibility validation
  • Relevant performance/efficiency opportunities considered; material claims have evidence
  • Material review findings resolved and relevant checks rerun
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

Inpaint masks, regional guidance and the regenerate tint stay hidden under a shown preview even when a mask is selected.
They show only while a brush, eraser, move or shape drag edits the selected mask or region, then hide on release.
Layer, group and child rows end in an eye beside the lock, aligned with the stack headers' eye and add buttons.
Child rows trade their leading dot for the same eye; the Layers panel docs and screenshot match.
The staging bar's result count drops its pill and is separated from the step controls by the bar's divider.
Groups connect to their members and layers to their adjustment and modifier rows with rounded branch lines.
Each nesting level indents 28px so a branch fits between parent and child; the Layers panel docs and screenshot match.
Pile cards shrink only by the offsets they need, so a one-member group shows a full-size thumbnail.
@github-actions github-actions Bot added frontend PRs that change frontend files docs PRs that change docs labels Nov 2, 2026
@joshistoast
joshistoast requested a review from lstein November 2, 2026 10:17

@Pfannkuchensack Pfannkuchensack left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at ed3ab80. No blockers. The reveal logic matches the spec on every path I traced. Release, Escape, pointercancel, lost capture, reset() and a tool switch mid-drag all end in setGestureActive(false). Pan, zoom, picker, frame and selection never reveal, and a selected group reveals only for move. Invalidation now happens only on flag transitions while a preview is shown, which is cheaper than before. LayerActiveDot has no remaining users, and the golden image is a rename only.

Low

  • The reveal is decided once at press time (engine.ts ~1526). An undo or remote selection change mid-drag keeps the reveal until release. This is documented in ARCHITECTURE.md and fine, just noting it.
  • The pipeline -> engine wiring is covered only indirectly through engine.test.ts. A pipeline-level test for "middle-button pan does not fire onGestureChange" and "reset() mid-gesture clears the flag" would catch the regression that matters most: masks stuck visible over previews.
  • The child-row eye is tabIndex={-1} (LayerChildRow.tsx ~320), like the layer-row eye. Please confirm that keyboard users can toggle child rows through a row-level shortcut.
  • LayerTreeGuides uses hard-coded offsets tied to the row's pb="0.5" gap; a change to row height or gap would misalign the connectors silently.

CI
In frontend-webv2-tests every Vitest file passed. The job then failed in a later step on locator.waitFor: Timeout 30000ms exceeded. Please check whether that step is related to the Layers panel changes or is a flake.

…peline

Assert onGestureChange fires once per transition on release, cancel, Escape, reset and tool switch, before the tool sees the press, and never for middle-button pans.

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 6ac4774. The reveal logic held up against the paths I attacked: release, Escape, pointer cancel, tool switch, reset(), a preview published or cleared mid-gesture, and Alt/Space temporary tools. The findings below are nits and none of them block.

Nit 1: the selected mask's cache now depends on its Layers panel thumbnail

While a preview is shown, the mask stack is out of frame demand (compositeFrame.ts:214-228), so a selected mask is neither rasterized nor protected from eviction (rasterController.ts:172). If its cache is missing at press time, the brush refuses with the "still loading" toast (paintTool.ts:105, :202), while the masks still reveal for that press.

  • In the normal layout this looks harmless: a mounted row's LayerThumbnail requests a thumbnail, and that rebuilds the cache. A browser run with the budget lowered to 1 MB showed no refusals, though that run may not have exceeded the budget.
  • An engine-level test with no panel did refuse with layer-loading, and adding a requestLayerThumbnail call made the refusal go away.
  • From reading the code, a refusal needs the row to be unmounted (collapsed stack, scrolled away, panel closed, degraded thumbnails) as well as a missing cache. I have not reproduced that in a browser.

If you want to close this off, keep the selected mask or region cached while it is hidden. Adding it to the working set is not enough on its own, because ensureLayerCaches only walks frame.leaves.

Nit 2: a Move click with no drag flashes every mask

moveTool.ts:209 decides at press time, so a click that never crosses the drag threshold shows all masks for the length of the press and edits nothing. engine.test.ts:16052 expects this, but the PR description says masks appear only while a drag is editing. Revealing at the drag threshold would match the description.

Nit 3: edits made outside a pointer gesture are not visible under a preview

With a mask selected and a preview shown, undoing a stroke or closing a polygon changes nothing visible, and the existing mask can't be seen until you press. The polygon case is documented; I traced the undo case in the code but did not run it. This may be an acceptable result of the requested behaviour, so I'm only flagging it in case it wasn't intended.

…ask loaded under previews

Move shows masks over a preview when its drag crosses the threshold, not on a plain click.
While a preview hides masks, the on-screen selected mask or region stays cached and protected from eviction, so a brush press no longer refuses as still loading.
@joshistoast
joshistoast enabled auto-merge (squash) November 4, 2026 23:59
@joshistoast
joshistoast merged commit 57a715a into main Nov 5, 2026
24 checks passed
@joshistoast
joshistoast deleted the fix/layer-eyes-and-staging-masks branch November 5, 2026 00:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

7.0.0 docs PRs that change docs frontend PRs that change frontend files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants