From 5caa7e19021924fbf054132b004903403fcc2c86 Mon Sep 17 00:00:00 2001 From: Andreas Heinz Date: Wed, 4 Mar 2026 16:27:26 +0100 Subject: [PATCH] feat(web): bounding box for faces when hovering over the face in photo view (#26667) * feat(web): when hovering over a face already deteced, display the bounding box also shown when hovering over the person in the details-pane. * prevent lint error * fix unused var --- .../asset-viewer/photo-viewer.svelte | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 69a6f0f103..3e609ff130 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -11,7 +11,7 @@ import { imageManager } from '$lib/managers/ImageManager.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { ocrManager } from '$lib/stores/ocr.svelte'; - import { boundingBoxesArray } from '$lib/stores/people.store'; + import { boundingBoxesArray, type Faces } from '$lib/stores/people.store'; import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store'; import { getAssetUrl, targetImageSize as getTargetImageSize, handlePromiseError } from '$lib/utils'; import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils'; @@ -195,6 +195,42 @@ } lastUrl = imageLoaderUrl; }); + + const faceToNameMap = $derived.by(() => { + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const map = new Map(); + for (const person of asset.people ?? []) { + for (const face of person.faces ?? []) { + map.set(face, person.name); + } + } + return map; + }); + + const faces = $derived(Array.from(faceToNameMap.keys())); + + const handleImageMouseMove = (event: MouseEvent) => { + $boundingBoxesArray = []; + if (!assetViewerManager.imgRef || !element || isFaceEditMode.value || ocrManager.showOverlay) { + return; + } + + const containerRect = element.getBoundingClientRect(); + const mouseX = event.clientX - containerRect.left; + const mouseY = event.clientY - containerRect.top; + + const faceBoxes = getBoundingBox(faces, overlayMetrics); + + for (const [index, box] of faceBoxes.entries()) { + if (mouseX >= box.left && mouseX <= box.left + box.width && mouseY >= box.top && mouseY <= box.top + box.height) { + $boundingBoxesArray.push(faces[index]); + } + } + }; + + const handleImageMouseLeave = () => { + $boundingBoxesArray = []; + }; @@ -218,6 +254,9 @@ class="relative h-full w-full select-none" bind:clientWidth={containerWidth} bind:clientHeight={containerHeight} + role="presentation" + onmousemove={handleImageMouseMove} + onmouseleave={handleImageMouseLeave} > {#if !imageLoaded}
@@ -248,11 +287,20 @@ : slideshowLookCssMapping[$slideshowLook]}" draggable="false" /> - {#each getBoundingBox($boundingBoxesArray, overlayMetrics) as boundingbox (boundingbox.id)} + {#each getBoundingBox($boundingBoxesArray, overlayMetrics) as boundingbox, index (boundingbox.id)}
+ {#if faceToNameMap.get($boundingBoxesArray[index])} +
+ {faceToNameMap.get($boundingBoxesArray[index])} +
+ {/if} {/each} {#each ocrBoxes as ocrBox (ocrBox.id)}