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>
43 lines
1.5 KiB
Swift
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)
|
|
}
|
|
}
|