Corner tracking for a two-click rectangle lived inline in the component and was never exercised. The rule worth pinning is that a first corner yields no rectangle at all: acting on it would analyse a zero-width area. The hook holds only interaction state — drawing mode, placed corner, the coordinate fields. What happens with a finished rectangle stays in the workspace, which owns retiring stale results and starting the analysis. Injecting that would have made the hook depend on values declared after it, which is what a first attempt at a wider extraction ran into. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
79 lines
2.7 KiB
TypeScript
79 lines
2.7 KiB
TypeScript
import { useCallback, useState, type Dispatch, type SetStateAction } from 'react'
|
|
|
|
import type { VectorSelectionBBox } from '../types'
|
|
import { bboxToInputState, normalizeBboxFromCorners, parseBboxInput } from '../components/map/mapWorkspaceUtils'
|
|
|
|
/**
|
|
* The state of drawing a rectangle on the map: whether a gesture is in
|
|
* progress, which corner has been placed, and what is typed in the coordinate
|
|
* fields.
|
|
*
|
|
* Deliberately only the interaction state. What happens *with* a finished
|
|
* rectangle — retiring stale results, starting an analysis — belongs to the
|
|
* workspace, which owns those. Injecting that here would make the hook depend
|
|
* on values declared after it.
|
|
*/
|
|
|
|
export type BboxInputState = ReturnType<typeof bboxToInputState>
|
|
|
|
export interface MapRectangleSelection {
|
|
/** A rectangle gesture is in progress. */
|
|
drawing: boolean
|
|
setDrawing: Dispatch<SetStateAction<boolean>>
|
|
/** The first click of a two-click rectangle, if one has been placed. */
|
|
firstCorner: [number, number] | null
|
|
bboxInput: BboxInputState
|
|
setBboxInput: Dispatch<SetStateAction<BboxInputState>>
|
|
/** The rectangle currently in the coordinate fields, if it parses. */
|
|
typedBbox: VectorSelectionBBox | null
|
|
/**
|
|
* Place a corner. Returns the finished rectangle on the second corner, and
|
|
* ``null`` on the first — the caller only acts when a rectangle exists.
|
|
*/
|
|
placeCorner: (coordinate: [number, number]) => VectorSelectionBBox | null
|
|
/** Abandon a half-drawn rectangle and leave drawing mode. */
|
|
reset: () => void
|
|
/** Show a rectangle in the coordinate fields. */
|
|
showBbox: (bbox: VectorSelectionBBox | null) => void
|
|
}
|
|
|
|
export function useMapRectangleSelection(initialBbox: VectorSelectionBBox | null): MapRectangleSelection {
|
|
const [drawing, setDrawing] = useState(false)
|
|
const [firstCorner, setFirstCorner] = useState<[number, number] | null>(null)
|
|
const [bboxInput, setBboxInput] = useState<BboxInputState>(bboxToInputState(initialBbox))
|
|
|
|
const placeCorner = useCallback(
|
|
(coordinate: [number, number]): VectorSelectionBBox | null => {
|
|
if (!firstCorner) {
|
|
setFirstCorner(coordinate)
|
|
return null
|
|
}
|
|
setFirstCorner(null)
|
|
setDrawing(false)
|
|
return normalizeBboxFromCorners(firstCorner, coordinate)
|
|
},
|
|
[firstCorner],
|
|
)
|
|
|
|
const reset = useCallback(() => {
|
|
setFirstCorner(null)
|
|
setDrawing(false)
|
|
}, [])
|
|
|
|
const showBbox = useCallback((bbox: VectorSelectionBBox | null) => {
|
|
setBboxInput(bboxToInputState(bbox))
|
|
}, [])
|
|
|
|
return {
|
|
drawing,
|
|
setDrawing,
|
|
firstCorner,
|
|
bboxInput,
|
|
setBboxInput,
|
|
typedBbox: parseBboxInput(bboxInput),
|
|
placeCorner,
|
|
reset,
|
|
showBbox,
|
|
}
|
|
}
|