Files
VisionPOC/Sources/App/StatusWindowController.swift
james.maesandClaude Opus 4.7 3cd94535c8 History view uniform with Log Stream + proper line shader + Status panel (⌘I)
Three asks landing together.

## History view — now identical controls to Log Stream
- Oldest-first order with auto-scroll-to-bottom on append (was newest-
  first, no auto-scroll). ScrollViewReader + onChange pattern lifted
  from LogStreamView for behavioral parity.
- Toolbar now matches the Log Stream layout exactly: Category filter,
  Auto-scroll toggle, Pause toggle, filtered/total count, Clear button.
- Poll rate bumped 1 Hz → 0.10 s so new events appear instantly. Pause
  toggle freezes refresh without dropping the running event collection.

## Proper line shader (much better hand + body rendering)
- New `line_vertex` / `line_fragment` in Boxes.metal alongside the
  existing box shader. `LineUniforms` carries endpoints + thickness +
  soft-edge falloff + a dot-mode flag.
- Line mode: emits a rotated thin quad oriented along (end - start)
  with width = thickness. Soft-edge antialiasing via smoothstep on the
  signed perpendicular UV makes diagonal skeleton segments read as
  actual strokes instead of axis-aligned bounding boxes.
- Dot mode: emits a square around `start` and the fragment discards
  outside the unit-circle — produces filled disks for joint markers
  with sub-pixel-clean soft edges.
- Pipelines gains a `lines` blended pipeline.
- Renderer.drawPoses() rewritten to use the line pipeline — one draw
  per segment, one draw per joint, all visually consistent regardless
  of segment angle. Theme.Palette.bodyPose (mint) for body, handPose
  (amber) for hand.

## Status panel (⌘I)
- New `CurrentStateStore` singleton subscribes to the event bus and
  retains the most recent value per concept: expression (smile / frown /
  neutral_face), activity (standing / sitting / etc.), per-chirality
  finger counts, plus a small ring of recent non-finger non-expression
  gestures.
- New StatusView (SwiftUI) reads three sources at 4 Hz: the live
  ObjectDetector for FPS + DET timing + objects + faces, the
  CurrentStateStore for hands/expression/activity/recent-gestures, and
  the HistoryStore for an 8-entry tail of recent events.
- New StatusWindowController hosts the SwiftUI view as a dark-aqua
  floating window with frame persistence (VPOC.StatusWindowFrame).
- AppDelegate `openStatus(_:)` builds a DetectorSnapshot from the live
  windowController + detector each tick, partitioning detections into
  face entries (anything labeled FACE or matching an enrolled name)
  vs object entries. Object entries carry the same per-track color the
  JARVIS pane uses (via ColorHash.colorFor) so the panel's dots match
  the on-screen box colors.
- Renderer exposes `smoothedFPSForReadout` so the panel can show the
  same FPS the footer does without reaching into private state.
- FingerCounter now records its count per chirality via
  CurrentStateStore.recordFingers (in addition to the existing bus
  emit), using VNHumanHandPoseObservation.chirality. The panel
  displays "Left: 3 fingers / Right: 5 fingers" correctly.

Menu structure now:
- App menu: Settings (⌘,) · Log Stream (⌘L) · Status (⌘I) · Save
  Snapshot (⌘S) · Start/Stop Recording (⇧⌘R) · Quit
- Faces menu: Enroll (⌘E) · Forget (⇧⌘E) · History (⌘H)
- Camera menu: Devices ▶ · Refresh · Privacy Mode (⇧⌘P)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 17:46:27 -05:00

43 lines
1.5 KiB
Swift

import AppKit
import SwiftUI
@MainActor
final class StatusWindowController: NSWindowController, NSWindowDelegate {
nonisolated(unsafe) private static let frameDefaultsKey = "VPOC.StatusWindowFrame"
init(detectorAccess: @escaping @MainActor () -> DetectorSnapshot?) {
let hosting = NSHostingController(rootView: StatusView(detectorAccess: detectorAccess))
let window = NSWindow(contentViewController: hosting)
window.title = "VisionPOC Status"
window.styleMask = [.titled, .closable, .resizable, .miniaturizable]
window.appearance = NSAppearance(named: .darkAqua)
window.isReleasedWhenClosed = false
window.center()
super.init(window: window)
window.delegate = self
restoreFrame()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) is not supported")
}
private func restoreFrame() {
guard let window,
let saved = UserDefaults.standard.string(forKey: StatusWindowController.frameDefaultsKey) else {
return
}
let rect = NSRectFromString(saved)
if rect.size.width > 0 && rect.size.height > 0 {
window.setFrame(rect, display: false)
}
}
nonisolated func windowWillClose(_ notification: Notification) {
guard let window = notification.object as? NSWindow else { return }
UserDefaults.standard.set(NSStringFromRect(window.frame),
forKey: StatusWindowController.frameDefaultsKey)
}
}