This commit is contained in:
Linrador 2026-06-30 14:51:46 +02:00
parent 95bf0fdd79
commit c595bd29f8
4 changed files with 120 additions and 8 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -218,6 +218,9 @@ type PoseInteraction = {
type: 'keypoint' type: 'keypoint'
personIndex: number personIndex: number
keypointIndex: number keypointIndex: number
startClientX: number
startClientY: number
moved: boolean
} }
type PoseKeypointPlacement = { type PoseKeypointPlacement = {
@ -225,6 +228,11 @@ type PoseKeypointPlacement = {
keypointName: string keypointName: string
} }
type PoseKeypointAction = {
personIndex: number
keypointIndex: number
}
type MagnifierState = { type MagnifierState = {
visible: boolean visible: boolean
clientX: number clientX: number
@ -366,6 +374,7 @@ const POSE_RELIABLE_KEYPOINT_MIN_CONFIDENCE = 0.20
const POSE_RELIABLE_MIN_SCORE = 0.30 const POSE_RELIABLE_MIN_SCORE = 0.30
const POSE_RELIABLE_MIN_KEYPOINTS = 6 const POSE_RELIABLE_MIN_KEYPOINTS = 6
const POSE_RELIABLE_MIN_QUALITY = 0.45 const POSE_RELIABLE_MIN_QUALITY = 0.45
const POSE_KEYPOINT_TAP_MOVE_THRESHOLD_PX = 6
const POSE_UNRELIABLE_COLOR = '#94a3b8' const POSE_UNRELIABLE_COLOR = '#94a3b8'
const POSE_PERSON_COLORS = ['#38bdf8', '#a78bfa', '#34d399', '#f59e0b', '#fb7185'] const POSE_PERSON_COLORS = ['#38bdf8', '#a78bfa', '#34d399', '#f59e0b', '#fb7185']
const POSE_SKELETON_EDGES: Array<readonly [string, string]> = [ const POSE_SKELETON_EDGES: Array<readonly [string, string]> = [
@ -4241,6 +4250,7 @@ export default function TrainingTab(props: {
setActivePosePersonIndex(null) setActivePosePersonIndex(null)
setPendingPoseKeypoint(null) setPendingPoseKeypoint(null)
setHoveredPoseKeypoint(null) setHoveredPoseKeypoint(null)
setSelectedPoseKeypointAction(null)
if (poseKeypointMapAnimationFrameRef.current !== null) { if (poseKeypointMapAnimationFrameRef.current !== null) {
cancelAnimationFrame(poseKeypointMapAnimationFrameRef.current) cancelAnimationFrame(poseKeypointMapAnimationFrameRef.current)
poseKeypointMapAnimationFrameRef.current = null poseKeypointMapAnimationFrameRef.current = null
@ -4401,6 +4411,8 @@ export default function TrainingTab(props: {
const [poseInteraction, setPoseInteraction] = useState<PoseInteraction | null>(null) const [poseInteraction, setPoseInteraction] = useState<PoseInteraction | null>(null)
const [pendingPoseKeypoint, setPendingPoseKeypoint] = useState<PoseKeypointPlacement | null>(null) const [pendingPoseKeypoint, setPendingPoseKeypoint] = useState<PoseKeypointPlacement | null>(null)
const [hoveredPoseKeypoint, setHoveredPoseKeypoint] = useState<PoseKeypointPlacement | null>(null) const [hoveredPoseKeypoint, setHoveredPoseKeypoint] = useState<PoseKeypointPlacement | null>(null)
const [selectedPoseKeypointAction, setSelectedPoseKeypointAction] =
useState<PoseKeypointAction | null>(null)
const [poseKeypointMapView, setPoseKeypointMapView] = useState<PoseKeypointMapView>('body') const [poseKeypointMapView, setPoseKeypointMapView] = useState<PoseKeypointMapView>('body')
const [poseKeypointMapViewBoxValue, setPoseKeypointMapViewBoxValue] = const [poseKeypointMapViewBoxValue, setPoseKeypointMapViewBoxValue] =
useState(POSE_BODY_MAP_VIEW_BOX) useState(POSE_BODY_MAP_VIEW_BOX)
@ -4560,6 +4572,7 @@ export default function TrainingTab(props: {
setActivePosePersonIndex(null) setActivePosePersonIndex(null)
setPendingPoseKeypoint(null) setPendingPoseKeypoint(null)
setHoveredPoseKeypoint(null) setHoveredPoseKeypoint(null)
setSelectedPoseKeypointAction(null)
poseFaceImagePanGestureRef.current = null poseFaceImagePanGestureRef.current = null
poseFaceImagePanCenterRef.current = null poseFaceImagePanCenterRef.current = null
@ -7369,14 +7382,38 @@ export default function TrainingTab(props: {
: clampedPos : clampedPos
if (poseInteraction) { if (poseInteraction) {
const pointerDistance = Math.hypot(
snapshot.clientX - poseInteraction.startClientX,
snapshot.clientY - poseInteraction.startClientY
)
const hasMoved =
poseInteraction.moved ||
pointerDistance > POSE_KEYPOINT_TAP_MOVE_THRESHOLD_PX
if (!hasMoved) {
return
}
if (!poseInteraction.moved) {
const nextInteraction = {
...poseInteraction,
moved: true,
}
poseInteractionRef.current = nextInteraction
setPoseInteraction(nextInteraction)
setSelectedPoseKeypointAction(null)
}
markManualCorrection() markManualCorrection()
let nextPoseMagnifier: MagnifierState | null = null let nextPoseMagnifier: MagnifierState | null = null
const activePoseInteraction = poseInteractionRef.current ?? poseInteraction
setCorrection((prev) => { setCorrection((prev) => {
const persons = clonePosePersons(prev.posePersons) const persons = clonePosePersons(prev.posePersons)
const person = persons[poseInteraction.personIndex] const person = persons[activePoseInteraction.personIndex]
const point = person?.keypoints?.[poseInteraction.keypointIndex] const point = person?.keypoints?.[activePoseInteraction.keypointIndex]
if (!person || !point) { if (!person || !point) {
return prev return prev
@ -7402,15 +7439,15 @@ export default function TrainingTab(props: {
box: poseBoxFromKeypoints({ box: poseBoxFromKeypoints({
...person, ...person,
keypoints: person.keypoints.map((candidate, index) => keypoints: person.keypoints.map((candidate, index) =>
index === poseInteraction.keypointIndex ? nextPoint : candidate index === activePoseInteraction.keypointIndex ? nextPoint : candidate
), ),
}), }),
keypoints: person.keypoints.map((candidate, index) => keypoints: person.keypoints.map((candidate, index) =>
index === poseInteraction.keypointIndex ? nextPoint : candidate index === activePoseInteraction.keypointIndex ? nextPoint : candidate
), ),
}) })
persons[poseInteraction.personIndex] = nextPerson persons[activePoseInteraction.personIndex] = nextPerson
const next: CorrectionState = { const next: CorrectionState = {
...prev, ...prev,
@ -7692,6 +7729,7 @@ export default function TrainingTab(props: {
y: clamp01(rawPos.y), y: clamp01(rawPos.y),
} }
) )
setSelectedPoseKeypointAction(null)
return return
} }
@ -7822,6 +7860,17 @@ export default function TrainingTab(props: {
} }
if (activePoseInteraction) { if (activePoseInteraction) {
if (activePoseInteraction.moved) {
setSelectedPoseKeypointAction(null)
} else {
setSelectedPoseKeypointAction({
personIndex: activePoseInteraction.personIndex,
keypointIndex: activePoseInteraction.keypointIndex,
})
setPendingPoseKeypoint(null)
setActivePosePersonIndex(activePoseInteraction.personIndex)
}
setPoseInteraction(null) setPoseInteraction(null)
clearBoxGestureRefs() clearBoxGestureRefs()
return return
@ -8007,6 +8056,18 @@ export default function TrainingTab(props: {
} }
return current return current
}) })
setSelectedPoseKeypointAction((current) => {
if (!current) return null
if (current.personIndex === personIndex) return null
if (current.personIndex > personIndex) {
return {
...current,
personIndex: current.personIndex - 1,
}
}
return current
})
}, [markManualCorrection]) }, [markManualCorrection])
const addPosePerson = useCallback(() => { const addPosePerson = useCallback(() => {
@ -8031,7 +8092,16 @@ export default function TrainingTab(props: {
setActiveBoxIndex(null) setActiveBoxIndex(null)
setActivePosePersonIndex(nextPersonIndex) setActivePosePersonIndex(nextPersonIndex)
setPendingPoseKeypoint(null) setPendingPoseKeypoint(null)
setSelectedPoseKeypointAction(null)
switchPoseKeypointMapView('body') switchPoseKeypointMapView('body')
setMobilePanel('labels')
window.requestAnimationFrame(() => {
mobileLabelsScrollRef.current?.scrollIntoView({
block: 'start',
behavior: 'smooth',
})
})
}, [markManualCorrection, switchPoseKeypointMapView, uiLocked]) }, [markManualCorrection, switchPoseKeypointMapView, uiLocked])
const removePoseKeypoint = useCallback((personIndex: number, keypointIndex: number) => { const removePoseKeypoint = useCallback((personIndex: number, keypointIndex: number) => {
@ -8074,6 +8144,12 @@ export default function TrainingTab(props: {
correctionRef.current = next correctionRef.current = next
return next return next
}) })
setSelectedPoseKeypointAction((current) =>
current?.personIndex === personIndex && current.keypointIndex === keypointIndex
? null
: current
)
}, [markManualCorrection]) }, [markManualCorrection])
const resetPosePersons = useCallback(() => { const resetPosePersons = useCallback(() => {
@ -8091,6 +8167,7 @@ export default function TrainingTab(props: {
}) })
setActivePosePersonIndex(null) setActivePosePersonIndex(null)
setSelectedPoseKeypointAction(null)
if (poseKeypointMapAnimationFrameRef.current !== null) { if (poseKeypointMapAnimationFrameRef.current !== null) {
cancelAnimationFrame(poseKeypointMapAnimationFrameRef.current) cancelAnimationFrame(poseKeypointMapAnimationFrameRef.current)
poseKeypointMapAnimationFrameRef.current = null poseKeypointMapAnimationFrameRef.current = null
@ -8162,11 +8239,15 @@ export default function TrainingTab(props: {
type: 'keypoint', type: 'keypoint',
personIndex, personIndex,
keypointIndex, keypointIndex,
startClientX: e.clientX,
startClientY: e.clientY,
moved: false,
} }
poseInteractionRef.current = nextInteraction poseInteractionRef.current = nextInteraction
setPoseInteraction(nextInteraction) setPoseInteraction(nextInteraction)
setActivePosePersonIndex(personIndex) setActivePosePersonIndex(personIndex)
setSelectedPoseKeypointAction(null)
setTouchMagnifier({ setTouchMagnifier({
visible: true, visible: true,
clientX: e.clientX, clientX: e.clientX,
@ -9803,6 +9884,7 @@ export default function TrainingTab(props: {
if (uiLocked) return if (uiLocked) return
setShowPoseSkeleton(true) setShowPoseSkeleton(true)
setActivePosePersonIndex(personIndex) setActivePosePersonIndex(personIndex)
setSelectedPoseKeypointAction(null)
setPendingPoseKeypoint({ setPendingPoseKeypoint({
personIndex, personIndex,
keypointName: poseKeypointId(point.name), keypointName: poseKeypointId(point.name),
@ -10117,11 +10199,13 @@ export default function TrainingTab(props: {
if (pending) { if (pending) {
setPendingPoseKeypoint(null) setPendingPoseKeypoint(null)
setSelectedPoseKeypointAction(null)
return return
} }
setShowPoseSkeleton(true) setShowPoseSkeleton(true)
setActivePosePersonIndex(targetIndex) setActivePosePersonIndex(targetIndex)
setSelectedPoseKeypointAction(null)
setPendingPoseKeypoint({ setPendingPoseKeypoint({
personIndex: targetIndex, personIndex: targetIndex,
keypointName, keypointName,
@ -10877,14 +10961,18 @@ export default function TrainingTab(props: {
const selected = const selected =
pendingPoseKeypoint?.personIndex === personIndex && pendingPoseKeypoint?.personIndex === personIndex &&
pendingPoseKeypoint.keypointName === keypointId pendingPoseKeypoint.keypointName === keypointId
const actionSelected =
selectedPoseKeypointAction?.personIndex === personIndex &&
selectedPoseKeypointAction.keypointIndex === pointIndex
const hovered = const hovered =
hoveredPoseKeypoint?.personIndex === personIndex && hoveredPoseKeypoint?.personIndex === personIndex &&
hoveredPoseKeypoint.keypointName === keypointId hoveredPoseKeypoint.keypointName === keypointId
const dragging = const dragging =
poseInteraction?.personIndex === personIndex && poseInteraction?.personIndex === personIndex &&
poseInteraction.keypointIndex === pointIndex poseInteraction.keypointIndex === pointIndex
const highlighted = selected || hovered || dragging const highlighted = selected || actionSelected || hovered || dragging
const showLabel = selected || dragging const showLabel = selected || actionSelected || dragging
const showDeleteAction = actionSelected && !dragging && !uiLocked
const facePlacement = poseFaceImageZoomActive const facePlacement = poseFaceImageZoomActive
? poseFaceKeypointLabelPlacement(keypointId) ? poseFaceKeypointLabelPlacement(keypointId)
: null : null
@ -10939,7 +11027,11 @@ export default function TrainingTab(props: {
{showLabel ? ( {showLabel ? (
<div <div
className="pointer-events-none absolute z-[370] flex max-w-[9rem] items-center gap-1 rounded-full bg-white/95 px-2 py-1 text-[10px] font-black leading-none text-gray-950 shadow-lg ring-1 ring-black/10 dark:bg-white/95 dark:text-gray-950" data-pose-control="true"
className={[
'absolute z-[370] flex max-w-[10.5rem] items-center gap-1 overflow-hidden rounded-full bg-white/95 text-[10px] font-black leading-none text-gray-950 shadow-lg ring-1 ring-black/10 dark:bg-white/95 dark:text-gray-950',
showDeleteAction ? 'pointer-events-auto p-0.5 pr-1' : 'pointer-events-none px-2 py-1',
].join(' ')}
style={{ style={{
left, left,
top, top,
@ -10957,6 +11049,26 @@ export default function TrainingTab(props: {
style={{ backgroundColor: color }} style={{ backgroundColor: color }}
/> />
<span className="min-w-0 truncate">{label}</span> <span className="min-w-0 truncate">{label}</span>
{showDeleteAction ? (
<button
type="button"
className="ml-0.5 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-red-600 text-white shadow-sm transition hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500/40"
title={`${label} löschen`}
aria-label={`${label} löschen`}
onPointerDown={(event) => {
event.preventDefault()
event.stopPropagation()
}}
onClick={(event) => {
event.preventDefault()
event.stopPropagation()
removePoseKeypoint(personIndex, pointIndex)
}}
>
<TrashIcon className="h-3 w-3" aria-hidden="true" />
</button>
) : null}
</div> </div>
) : null} ) : null}
</Fragment> </Fragment>