From c0a562e79d2670cc979befa49ba9396420b5db99 Mon Sep 17 00:00:00 2001 From: Miha Lunar Date: Tue, 8 Oct 2024 22:56:24 +0200 Subject: [PATCH] Implement closest region check in API, replaces slow frontend impl --- api.yaml | 9 +++++++++ internal/layout/common.go | 8 ++++++++ internal/openapi/api.gen.go | 17 ++++++++++++++++- internal/render/scene.go | 27 +++++++++++++++++++++++++++ main.go | 21 +++++++++++++++++++++ ui/src/api.js | 27 +++++++-------------------- ui/src/components/ScrollViewer.vue | 1 - 7 files changed, 88 insertions(+), 22 deletions(-) diff --git a/api.yaml b/api.yaml index 29da1a7..be942be 100644 --- a/api.yaml +++ b/api.yaml @@ -329,6 +329,15 @@ paths: type: number example: 200 + - name: closest + in: query + description: | + If true, return the closest region to the specified `x` and `y` coordinates. + The `w` and `h` parameters are ignored in this case. + schema: + type: boolean + example: false + - name: limit in: query schema: diff --git a/internal/layout/common.go b/internal/layout/common.go index db20017..6ab121a 100644 --- a/internal/layout/common.go +++ b/internal/layout/common.go @@ -268,6 +268,14 @@ func (regionSource PhotoRegionSource) GetRegionById(id int, scene *render.Scene, return regionSource.getRegionFromPhoto(id, &photo, scene, regionConfig) } +func (regionSource PhotoRegionSource) GetRegionClosestTo(p render.Point, scene *render.Scene, regionConfig render.RegionConfig) (region render.Region, ok bool) { + photo, ok := scene.GetClosestPhotoRef(p) + if !ok { + return render.Region{}, false + } + return regionSource.getRegionFromPhoto(1+photo.Index, photo.Photo, scene, regionConfig), true +} + func layoutFitRow(row []render.Photo, bounds render.Rect, imageSpacing float64) float64 { count := len(row) if count == 0 { diff --git a/internal/openapi/api.gen.go b/internal/openapi/api.gen.go index 5415eca..9fa52b8 100644 --- a/internal/openapi/api.gen.go +++ b/internal/openapi/api.gen.go @@ -273,7 +273,11 @@ type GetScenesSceneIdRegionsParams struct { Y *float32 `json:"y,omitempty"` W *float32 `json:"w,omitempty"` H *float32 `json:"h,omitempty"` - Limit *Limit `json:"limit,omitempty"` + + // If true, return the closest region to the specified `x` and `y` coordinates. + // The `w` and `h` parameters are ignored in this case. + Closest *bool `json:"closest,omitempty"` + Limit *Limit `json:"limit,omitempty"` } // GetScenesSceneIdTilesParams defines parameters for GetScenesSceneIdTiles. @@ -844,6 +848,17 @@ func (siw *ServerInterfaceWrapper) GetScenesSceneIdRegions(w http.ResponseWriter return } + // ------------- Optional query parameter "closest" ------------- + if paramValue := r.URL.Query().Get("closest"); paramValue != "" { + + } + + err = runtime.BindQueryParameter("form", true, false, "closest", r.URL.Query(), ¶ms.Closest) + if err != nil { + http.Error(w, fmt.Sprintf("Invalid format for parameter closest: %s", err), http.StatusBadRequest) + return + } + // ------------- Optional query parameter "limit" ------------- if paramValue := r.URL.Query().Get("limit"); paramValue != "" { diff --git a/internal/render/scene.go b/internal/render/scene.go index 8cd7cf4..3524508 100644 --- a/internal/render/scene.go +++ b/internal/render/scene.go @@ -74,6 +74,7 @@ type RegionSource interface { GetRegionsFromImageId(image.ImageId, *Scene, RegionConfig) []Region GetRegionChanFromBounds(Rect, *Scene, RegionConfig) <-chan Region GetRegionById(int, *Scene, RegionConfig) Region + GetRegionClosestTo(Point, *Scene, RegionConfig) (region Region, ok bool) } type SceneId = string @@ -267,6 +268,28 @@ func (scene *Scene) GetVisiblePhotoRefs(view Rect, maxCount int) <-chan PhotoRef return out } +func (s *Scene) GetClosestPhotoRef(p Point) (ref PhotoRef, ok bool) { + minIndex := -1 + minDistSq := math.MaxFloat64 + for i := range s.Photos { + photo := &s.Photos[i] + dx := photo.Sprite.Rect.X - p.X + dy := photo.Sprite.Rect.Y - p.Y + distSq := dx*dx + dy*dy + if distSq < minDistSq { + minDistSq = distSq + minIndex = i + } + } + if minIndex == -1 { + return PhotoRef{}, false + } + return PhotoRef{ + Index: minIndex, + Photo: &s.Photos[minIndex], + }, true +} + func (scene *Scene) GetVisiblePhotos(view Rect) <-chan Photo { out := make(chan Photo, 100) go func() { @@ -316,6 +339,10 @@ func (scene *Scene) GetRegionsByImageId(id image.ImageId, limit int) []Region { return scene.RegionSource.GetRegionsFromImageId(id, scene, query) } +func (scene *Scene) GetRegionClosestTo(p Point) (region Region, ok bool) { + return scene.RegionSource.GetRegionClosestTo(p, scene, RegionConfig{}) +} + func (scene *Scene) GetRegionChan(bounds Rect) <-chan Region { if scene.RegionSource == nil { return nil diff --git a/main.go b/main.go index f9c16b4..fd20262 100644 --- a/main.go +++ b/main.go @@ -855,6 +855,27 @@ func (*Api) GetScenesSceneIdRegions(w http.ResponseWriter, r *http.Request, scen return } regions = scene.GetRegionsByImageId(image.ImageId(*params.FileId), limit) + } else if params.Closest != nil && *params.Closest { + if params.X == nil || params.Y == nil { + problem(w, r, http.StatusBadRequest, "x and y required") + return + } + if params.Limit == nil || *params.Limit != 1 { + problem(w, r, http.StatusBadRequest, "limit must be set to 1") + return + } + + p := render.Point{ + X: float64(*params.X), + Y: float64(*params.Y), + } + + region, ok := scene.GetRegionClosestTo(p) + if !ok { + regions = []render.Region{} + } else { + regions = []render.Region{region} + } } else { if params.X == nil || params.Y == nil || params.W == nil || params.H == nil { problem(w, r, http.StatusBadRequest, "bounds or file_id required") diff --git a/ui/src/api.js b/ui/src/api.js index 5d9e150..861d175 100644 --- a/ui/src/api.js +++ b/ui/src/api.js @@ -79,6 +79,13 @@ export async function getRegionsWithFileId(sceneId, id) { return response.items; } +export async function getRegionClosestTo(sceneId, x, y) { + if (!sceneId) return null; + const response = await get(`/scenes/${sceneId}/regions?x=${x}&y=${y}&closest=true&limit=1`); + if (!response.items?.length) return null; + return response.items[0]; +} + export async function getRegion(sceneId, id) { return get(`/scenes/${sceneId}/regions/${id}`); } @@ -105,26 +112,6 @@ export async function getCenterRegion(sceneId, x, y, w, h) { return minRegion; } -export async function getRegionClosestTo(sceneId, x, y, w, h, rx, ry) { - const regions = await getRegions(sceneId, x, y, w, h); - if (!regions) return null; - let minDistSq = Infinity; - let minRegion = null; - for (let i = 0; i < regions.length; i++) { - const region = regions[i]; - const rcx = region.bounds.x + region.bounds.w*0.5; - const rcy = region.bounds.y + region.bounds.h*0.5; - const dx = rcx - rx; - const dy = rcy - ry; - const distSq = dx*dx + dy*dy; - if (distSq < minDistSq) { - minDistSq = distSq; - minRegion = region; - } - } - return minRegion; -} - export async function getCollections() { return get(`/collections`); } diff --git a/ui/src/components/ScrollViewer.vue b/ui/src/components/ScrollViewer.vue index e92be58..eeab646 100644 --- a/ui/src/components/ScrollViewer.vue +++ b/ui/src/components/ScrollViewer.vue @@ -305,7 +305,6 @@ watchDebounced(scrollY, async (sy) => { const { x, y, w, h } = view.value; const center = await getRegionClosestTo( scene.value.id, - x, y, w, h, x, y + h * focusScreenRatioY, ); const fileId = center?.data?.id;