Skip to content
This repository was archived by the owner on Oct 4, 2026. It is now read-only.

feat(ui): image map cluster selection mode - #42

Merged
lstein merged 5 commits into
mainfrom
feat/image-map-09-cluster-select
Aug 17, 2026
Merged

lstein merged 5 commits into
mainfrom
feat/image-map-09-cluster-select

Conversation

@lstein

@lstein lstein commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

PR 9 of the Image Map stack — PhotoMapAI's cluster-click behavior:

  • A header toggle (persisted in widget values) switches map clicks between selecting one image and selecting the clicked point's whole DBSCAN cluster: setGalleryMultiSelection with the members ordered by distance from the click (proximity ordering, capped at 5000); the clicked image becomes primary so Preview follows. Noise points fall back to single selection.
  • The gallery's multi-selection renders on the map as a white-outlined "Highlighted Points" overlay between the base points and the gold marker, wherever the selection was made.
  • Performance/correctness architecture (from adversarial review): selection changes restyle the overlay traces in place — the WebGL scene rebuilds only when the point set itself changes, so arrow-key gallery navigation never re-uploads a 50k-point scatter, and the gold marker is re-applied after every rebuild (it can no longer blank when the multi-selection changes around a stable primary). Single- and cluster-selection share one monotonic click sequence, so rapid mixed-mode clicks always resolve to the latest click. The highlight overlay uses hoverinfo: 'skip' so it never shadows the base points.

Known accepted gap (also noted in the floating-windows PR): floating windows render widgets without header/footer chrome, so the mode toggle is only reachable while docked.

Testing

Cluster collection (proximity order, noise/unknown handling, cap keeps nearest incl. the clicked point), highlight trace builder (multi-only, outline), full suite + lint + build green at the stack top.

🤖 Generated with Claude Code

@lstein
lstein requested a review from blessedcoolant as a code owner August 2, 2026 21:49
@github-actions github-actions Bot added the python label Aug 2, 2026
@lstein
lstein marked this pull request as draft August 2, 2026 22:24
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from aa21512 to 32045e9 Compare August 2, 2026 22:38
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from 3990b04 to 5784c6a Compare August 2, 2026 22:38
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 32045e9 to bf26409 Compare August 3, 2026 01:38
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch 2 times, most recently from 1966859 to 238768f Compare August 3, 2026 02:26
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch 2 times, most recently from 0015963 to 32b12f4 Compare August 3, 2026 18:20
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from 238768f to 0fca217 Compare August 3, 2026 18:21
@lstein
lstein marked this pull request as ready for review August 3, 2026 18:28
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from 0fca217 to f204e11 Compare August 4, 2026 01:43
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 32b12f4 to 2d68eb8 Compare August 4, 2026 01:43
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from f204e11 to d0c4e2e Compare August 4, 2026 05:37
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch 2 times, most recently from 312156e to 8a2e9a2 Compare August 4, 2026 14:58
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from d0c4e2e to 6c2d8df Compare August 4, 2026 14:58
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 8a2e9a2 to c47f5ae Compare August 4, 2026 22:32
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from 6c2d8df to a423504 Compare August 4, 2026 22:32
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from c47f5ae to e239eca Compare August 5, 2026 00:31
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch 2 times, most recently from 3374c16 to ce5c18d Compare August 5, 2026 00:38
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch 2 times, most recently from 78b62a5 to a43f464 Compare August 5, 2026 01:18
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch 2 times, most recently from 69fa638 to f717ca1 Compare August 5, 2026 01:21
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from a43f464 to 311ecaa Compare August 5, 2026 01:21
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 311ecaa to 4bf36a2 Compare August 7, 2026 20:41
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch 2 times, most recently from 085ded4 to 7361fbb Compare August 9, 2026 18:27
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch 2 times, most recently from 29de7d4 to 7646455 Compare August 13, 2026 03:21
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from 7361fbb to a9713ff Compare August 13, 2026 03:21
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 7646455 to ef0eb5c Compare August 13, 2026 03:26
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from a9713ff to 1e8a8f0 Compare August 13, 2026 03:26
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from ef0eb5c to 353ef22 Compare August 13, 2026 04:37
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from 1e8a8f0 to 46c3278 Compare August 13, 2026 04:37
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 353ef22 to 6ad08aa Compare August 13, 2026 23:17
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from 46c3278 to 4d13895 Compare August 13, 2026 23:17
@lstein
lstein force-pushed the feat/image-map-08-live-theme branch from 79f59b2 to cc3029f Compare August 15, 2026 21:16
Base automatically changed from feat/image-map-08-live-theme to main August 16, 2026 15:04
lstein and others added 2 commits August 16, 2026 15:24
Adds PhotoMapAI's cluster-click behavior as an opt-in mode:

- A header toggle (persisted in the widget's values) switches map clicks
  between selecting one image and selecting the clicked point's whole
  DBSCAN cluster.
- Cluster clicks multi-select the cluster in the gallery via
  setGalleryMultiSelection, ordered by distance from the clicked point
  (proximity ordering) and capped at 5000 members; the clicked image
  becomes the primary selection, so Preview follows it. Noise points fall
  back to single selection.
- The gallery's multi-selection renders on the map as a "Highlighted
  Points" trace — larger, white-outlined, between the base points and the
  gold current-image marker — regardless of where the selection was made.

Amended during the rebase onto the re-architected webv2:

- The header toggle ships through the widget implementation's
  `headerActions` slot (the old single-manifest index.ts is gone) and
  patches its persisted value via `useWorkbenchCommands().widgets
  .patchValues('image-map', ...)` instead of dispatching
  `patchWidgetValues`; IconButton/Tooltip come from `@platform/ui` and
  `WidgetViewProps` from `@workbench/widgetContracts`.
- Cluster clicks now go through `useWorkbenchCommands().gallery
  .setItemMultiSelection(itemKeys, primaryItem)`: the ordered names are
  wrapped as kind-tagged keys with `toGalleryItemKey({ kind: 'image',
  name })` (map points are always images) and the clicked image is
  hydrated through `galleryImages.resolveMany` +
  `legacyGeneratedImageToGalleryItem`, sharing the single-select path's
  monotonic click sequence. Single select (the plain-click clear) is
  unchanged: `gallery.selectItem` already collapses the multi-selection.
- The highlight overlay reads the persisted selection with
  `getPersistedSelectedGalleryItemKeys` and parses the kind-tagged keys
  back to bare image names (`parseGalleryItemKey`, images only) — on this
  architecture `selectedImageNames` stores GalleryItemKeys, not names.
- Performance baselines recaptured (build + browser, browser verified
  passing twice): the image-map implementation chunk loads at editor boot
  via the singleton host, so the header toggle and cluster wiring grow
  editor owned bytes by 65 (launchpad owned -3 minifier drift).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The base PRs merged, so this branch was replayed onto main and its stale
baseline-regeneration commits dropped. These numbers come from a fresh build.
@lstein
lstein force-pushed the feat/image-map-09-cluster-select branch from 5fce8c1 to 76adc9a Compare August 16, 2026 19:29
lstein added 3 commits August 16, 2026 16:03
…trol

Three findings from an adversarial review of cluster selection:

- `selectCluster` now calls `commands.gallery.selectBoard` before dispatching,
  as `selectImage` already did. The map spans every accessible board while the
  selection commands stamp `selectedImageQuery` from whatever board the gallery
  is currently showing, so a cluster picked from another board left that query
  describing a list none of its images were in — the grid never showed the
  selection and Preview's next/prev went dead. That is the regression the
  sibling call's comment exists to prevent; the cluster path had simply not
  inherited it.
- The header control is `ToggleIconButton` from @platform/ui rather than a
  hand-rolled IconButton. The shared component is this exact control, and its
  filled-when-on convention is what makes "on" legible; the local copy rendered
  subtle-gray where every other toggle in the app renders solid accent.
- `selectCluster` has tests: board-before-selection ordering, the name to
  image-kinded-key mapping with its clicked-point anchor, the unresolvable
  primary, and the sequence guard shared with `selectImage` — the last being
  the behaviour the hook's doc comment claims but nothing checked.

The duplicated `Boolean(values.clickSelectsCluster)` reads move behind one
accessor, which costs the editor bundle one small module; the baseline records
it. Selection semantics (unhydrated cluster keys, and the size of a persisted
selection) are deliberately left alone.
…ster-select

Only the two generated performance baselines conflicted; main's webv2 changes
(232 files, including the new queue-status widget) do not touch this branch's
code. Both baselines are regenerated from a fresh build of the merged tree
rather than resolved by hand.
@lstein
lstein enabled auto-merge August 17, 2026 01:07
@lstein
lstein merged commit 0136156 into main Aug 17, 2026
20 of 21 checks passed
@lstein
lstein deleted the feat/image-map-09-cluster-select branch August 17, 2026 01:36
lstein added a commit that referenced this pull request Aug 18, 2026
…'s cursor

Both failures were the same root cause, and neither was a product bug: these
suites drive real browser input, and the cursor keeps whatever position the
last test left it in.

ConditioningRebalanceBars — "reports the tap under the pointer" blocked #42's
CI. Instrumenting the failure showed the intended index 7 arriving and then a
spurious 6 after it (calls [1, 7, 7, 6]), because the real cursor was resting
on the track and emitting its own pointer events at its own location, which the
component reported. The same run showed the track's `left` at -393 rather than
0: an earlier test had scrolled the document sideways, moving which bar sits
under a stationary cursor. Teardown now parks the pointer and resets the scroll
offset.

FeatureHint — the harness comments that "the mouse starts at the viewport
origin" and insets the trigger by 120px accordingly. That holds only for the
first test: once one has hovered the trigger, the next render mounts a fresh
trigger underneath the pointer and the card opens before the test asks it to.
It surfaced as a strict-mode violation, `CLIP skip` matching the trigger and
the already-open card's heading and paragraph. Teardown now returns the pointer
to the origin, restoring the assumption the harness documents.

Measured over ten consecutive full browser runs: neither test failed once,
where before each failed roughly one run in three.

Two earlier theories are not in this commit because instrumentation refuted
them: the track's width was stable (960px in both the measured and live rects),
so waiting for layout to settle fixed nothing, and moving the real cursor with
`userEvent.hover(document.body)` made things worse because body's centre can sit
over the component under test.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant