fix: guard hand landmark visualization

This commit is contained in:
Tom Boullay
2026-04-29 09:52:46 +02:00
parent cc78420d9c
commit 28e3ac4c06
6 changed files with 110 additions and 1 deletions
+7
View File
@@ -68,6 +68,13 @@ Server responds with detected hands:
"x": 0.5, "x": 0.5,
"y": 0.3, "y": 0.3,
"z": 0.1, "z": 0.1,
"landmarks": [
{
"x": 0.48,
"y": 0.32,
"z": 0.02
}
],
"handedness": "Right", "handedness": "Right",
"isPinch": true, "isPinch": true,
"pinchDistance": 0.05, "pinchDistance": 0.05,
+7 -1
View File
@@ -19,16 +19,18 @@ class HandData:
x: float x: float
y: float y: float
z: float z: float
landmarks: list[dict[str, float]]
handedness: str handedness: str
is_pinch: bool is_pinch: bool
pinch_distance: float pinch_distance: float
score: float score: float
def to_payload(self) -> dict[str, float | str | bool]: def to_payload(self) -> dict[str, float | str | bool | list[dict[str, float]]]:
return { return {
"x": self.x, "x": self.x,
"y": self.y, "y": self.y,
"z": self.z, "z": self.z,
"landmarks": self.landmarks,
"handedness": self.handedness, "handedness": self.handedness,
"isPinch": self.is_pinch, "isPinch": self.is_pinch,
"pinchDistance": self.pinch_distance, "pinchDistance": self.pinch_distance,
@@ -86,6 +88,10 @@ class HandTracker:
x=index_tip.x, x=index_tip.x,
y=index_tip.y, y=index_tip.y,
z=index_tip.z, z=index_tip.z,
landmarks=[
{"x": point.x, "y": point.y, "z": point.z}
for point in landmarks
],
handedness=handedness.category_name, handedness=handedness.category_name,
is_pinch=pinch_distance < 0.07, is_pinch=pinch_distance < 0.07,
pinch_distance=pinch_distance, pinch_distance=pinch_distance,
@@ -0,0 +1,77 @@
import { useHandTrackingSnapshot } from "@/hooks/useHandTrackingSnapshot";
const HAND_CONNECTIONS: Array<[number, number]> = [
[0, 1],
[1, 2],
[2, 3],
[3, 4],
[0, 5],
[5, 6],
[6, 7],
[7, 8],
[5, 9],
[9, 10],
[10, 11],
[11, 12],
[9, 13],
[13, 14],
[14, 15],
[15, 16],
[13, 17],
[17, 18],
[18, 19],
[19, 20],
[0, 17],
];
export function HandTrackingVisualizer(): React.JSX.Element | null {
const { hands, status } = useHandTrackingSnapshot();
if (status === "idle" || hands.length === 0) {
return null;
}
return (
<svg className="hand-tracking-visualizer" aria-hidden="true">
{hands.map((hand, handIndex) => {
const landmarks = hand.landmarks ?? [];
if (landmarks.length === 0) return null;
const color = hand.isPinch ? "#facc15" : "#38bdf8";
return (
<g key={`${hand.handedness}-${handIndex}`}>
{HAND_CONNECTIONS.map(([from, to]) => {
const fromPoint = landmarks[from];
const toPoint = landmarks[to];
if (!fromPoint || !toPoint) return null;
return (
<line
key={`${from}-${to}`}
x1={`${(1 - fromPoint.x) * 100}%`}
y1={`${fromPoint.y * 100}%`}
x2={`${(1 - toPoint.x) * 100}%`}
y2={`${toPoint.y * 100}%`}
stroke={color}
strokeWidth="2"
strokeLinecap="round"
/>
);
})}
{landmarks.map((landmark, landmarkIndex) => (
<circle
key={landmarkIndex}
cx={`${(1 - landmark.x) * 100}%`}
cy={`${landmark.y * 100}%`}
r={landmarkIndex === 8 ? 5 : 3}
fill={landmarkIndex === 8 ? "#ffffff" : color}
/>
))}
</g>
);
})}
</svg>
);
}
+10
View File
@@ -418,6 +418,16 @@ canvas {
color: #fca5a5; color: #fca5a5;
} }
.hand-tracking-visualizer {
position: fixed;
inset: 0;
z-index: 15;
width: 100vw;
height: 100vh;
pointer-events: none;
filter: drop-shadow(0 0 8px rgba(56, 189, 248, 0.55));
}
/* Editor page */ /* Editor page */
.editor-container { .editor-container {
position: fixed; position: fixed;
+2
View File
@@ -3,6 +3,7 @@ import { Canvas } from "@react-three/fiber";
import { Crosshair } from "@/components/ui/Crosshair"; import { Crosshair } from "@/components/ui/Crosshair";
import { HandTrackingOverlay } from "@/components/ui/HandTrackingOverlay"; import { HandTrackingOverlay } from "@/components/ui/HandTrackingOverlay";
import { HandTrackingProvider } from "@/components/ui/HandTrackingProvider"; import { HandTrackingProvider } from "@/components/ui/HandTrackingProvider";
import { HandTrackingVisualizer } from "@/components/ui/HandTrackingVisualizer";
import { InteractPrompt } from "@/components/ui/InteractPrompt"; import { InteractPrompt } from "@/components/ui/InteractPrompt";
import { DebugPerf } from "@/components/debug/DebugPerf"; import { DebugPerf } from "@/components/debug/DebugPerf";
import { World } from "@/world/World"; import { World } from "@/world/World";
@@ -18,6 +19,7 @@ export function HomePage(): React.JSX.Element {
</Canvas> </Canvas>
<Crosshair /> <Crosshair />
<InteractPrompt /> <InteractPrompt />
<HandTrackingVisualizer />
<HandTrackingOverlay /> <HandTrackingOverlay />
</HandTrackingProvider> </HandTrackingProvider>
); );
+7
View File
@@ -1,7 +1,14 @@
export interface HandTrackingLandmark {
x: number;
y: number;
z: number;
}
export interface HandTrackingHand { export interface HandTrackingHand {
x: number; x: number;
y: number; y: number;
z: number; z: number;
landmarks: HandTrackingLandmark[];
handedness: string; handedness: string;
isPinch: boolean; isPinch: boolean;
pinchDistance: number; pinchDistance: number;