+ {/* Video frame */}
+

+
+ {/* Detection overlay */}
+
+ {/* Detected value */}
+
+
+ {frame.detectedValue !== null ? frame.detectedValue : '---'}
+
+ {frame.detectedValue !== null && (
+
+ {Math.round(frame.confidence * 100)}%
+
+ )}
+
+
+ {/* Live indicator */}
+
+
+
+ {isStale ? 'Stale' : 'Live'}
+
+
+
+
+ {/* Vision mode badge */}
+
+ 📷
+ Vision
+
+
+ )
+}
diff --git a/apps/web/src/components/vision/VisionCameraFeed.tsx b/apps/web/src/components/vision/VisionCameraFeed.tsx
index 58fa7bdc..896f9819 100644
--- a/apps/web/src/components/vision/VisionCameraFeed.tsx
+++ b/apps/web/src/components/vision/VisionCameraFeed.tsx
@@ -19,6 +19,8 @@ export interface VisionCameraFeedProps {
showRectifiedView?: boolean
/** Video element ref callback for external access */
videoRef?: (el: HTMLVideoElement | null) => void
+ /** Rectified canvas ref callback for external access (only when showRectifiedView=true) */
+ rectifiedCanvasRef?: (el: HTMLCanvasElement | null) => void
/** Called when video metadata is loaded (provides dimensions) */
onVideoReady?: (width: number, height: number) => void
/** Children rendered over the video (e.g., CalibrationOverlay) */
@@ -55,6 +57,7 @@ export function VisionCameraFeed({
showCalibrationGrid = false,
showRectifiedView = false,
videoRef: externalVideoRef,
+ rectifiedCanvasRef: externalCanvasRef,
onVideoReady,
children,
}: VisionCameraFeedProps): ReactNode {
@@ -82,6 +85,13 @@ export function VisionCameraFeed({
}
}, [externalVideoRef])
+ // Set canvas ref for external access (when rectified view is active)
+ useEffect(() => {
+ if (externalCanvasRef && showRectifiedView) {
+ externalCanvasRef(rectifiedCanvasRef.current)
+ }
+ }, [externalCanvasRef, showRectifiedView])
+
// Attach stream to video element
useEffect(() => {
const video = internalVideoRef.current
diff --git a/apps/web/src/contexts/MyAbacusContext.tsx b/apps/web/src/contexts/MyAbacusContext.tsx
index 447f435b..a8bf2649 100644
--- a/apps/web/src/contexts/MyAbacusContext.tsx
+++ b/apps/web/src/contexts/MyAbacusContext.tsx
@@ -113,6 +113,23 @@ export interface DockAnimationState {
toScale: number
}
+/**
+ * Vision frame data for broadcasting
+ */
+export interface VisionFrameData {
+ /** Base64-encoded JPEG image data */
+ imageData: string
+ /** Detected abacus value (null if not yet detected) */
+ detectedValue: number | null
+ /** Detection confidence (0-1) */
+ confidence: number
+}
+
+/**
+ * Callback type for vision frame broadcasting
+ */
+export type VisionFrameCallback = (frame: VisionFrameData) => void
+
interface MyAbacusContextValue {
isOpen: boolean
open: () => void
@@ -185,6 +202,10 @@ interface MyAbacusContextValue {
openVisionSetup: () => void
/** Close the vision setup modal */
closeVisionSetup: () => void
+ /** Set a callback for receiving vision frames (for broadcasting to observers) */
+ setVisionFrameCallback: (callback: VisionFrameCallback | null) => void
+ /** Emit a vision frame (called by DockedVisionFeed) */
+ emitVisionFrame: (frame: VisionFrameData) => void
}
const MyAbacusContext = createContext