feat(av-live-body): HUD + menus + pose -> scene

C - HUD coin haut-gauche + touches actives :
- HUDOverlay.swift : statut mode actif, count personnes, list
  touches actives (S/0-9/M/W/C/V), positions head/wrists de la
  1ere personne. allowsHitTesting(false).

Menus + raccourcis SwiftUI complets :
- App > Toggle Settings   (S)
- Calques > Toggle Webcam (C) / Scene Metal (V) /
            Maillage SMPL-X (M) / Fil de fer (W)
- Modes visuels > 0..9 -> storm/.../openpos
Tous propages via NotificationCenter (.toggleSettings,
.toggleLayer, .setVizMode).

B - Pose drive scene uniforms :
- BodyView observe PoseOSCListener en plus de RenderSettings.
- updateNSView pousse pose_count, pose_alive, hand_l_x/y,
  hand_r_x/y dans SceneRenderer.uniforms.
- Mode hands3d (#8) reagit a la position des poignets de la
  1ere personne detectee. Mode openpos (#9) idem.

A - Dashboard data-only : 0 fix code, le probleme etait
bridge.py orphelins multiples + cache navigateur.
This commit is contained in:
L'électron rare
2026-05-13 21:34:26 +02:00
parent 96a326d3c6
commit 214b1545c4
3 changed files with 170 additions and 19 deletions
@@ -10,18 +10,51 @@ struct AVLiveBodyApp: App {
}
.commands {
CommandGroup(replacing: .appSettings) {
Button("Toggle Settings (S)") {
Button("Toggle Settings") {
NotificationCenter.default.post(
name: .toggleSettings, object: nil)
}
.keyboardShortcut("s", modifiers: [])
}
CommandMenu("Calques") {
Button("Toggle Webcam") {
NotificationCenter.default.post(
name: .toggleLayer, object: "camera")
}.keyboardShortcut("c", modifiers: [])
Button("Toggle Scene Metal") {
NotificationCenter.default.post(
name: .toggleLayer, object: "scene")
}.keyboardShortcut("v", modifiers: [])
Button("Toggle Maillage SMPL-X") {
NotificationCenter.default.post(
name: .toggleLayer, object: "mesh")
}.keyboardShortcut("m", modifiers: [])
Button("Toggle Fil de fer") {
NotificationCenter.default.post(
name: .toggleLayer, object: "wireframe")
}.keyboardShortcut("w", modifiers: [])
}
CommandMenu("Modes visuels") {
ForEach(0..<10) { i in
let names = ["storm", "tunnel", "plasma", "kaleido",
"voronoi", "metaballs", "starfield",
"bars", "hands3d", "openpos"]
Button("\(i)\(names[i])") {
NotificationCenter.default.post(
name: .setVizMode, object: i)
}.keyboardShortcut(
KeyEquivalent(Character(String(i))),
modifiers: [])
}
}
}
}
}
extension Notification.Name {
static let toggleSettings = Notification.Name("avlive.toggleSettings")
static let toggleLayer = Notification.Name("avlive.toggleLayer")
static let setVizMode = Notification.Name("avlive.setVizMode")
}
struct ContentView: View {
@@ -30,8 +63,9 @@ struct ContentView: View {
@StateObject private var poseListener = PoseOSCListener()
var body: some View {
ZStack(alignment: .topTrailing) {
BodyView(renderer: renderer, settings: settings)
ZStack(alignment: .topLeading) {
BodyView(renderer: renderer, settings: settings,
poseListener: poseListener)
.onAppear {
renderer.startOSCServer()
poseListener.start()
@@ -40,27 +74,56 @@ struct ContentView: View {
for: .toggleSettings)) { _ in
settings.showPanel.toggle()
}
// Bouton coin haut-droit pour ouvrir le panel
Button(action: { settings.showPanel.toggle() }) {
Image(systemName: settings.showPanel
? "slider.horizontal.3"
: "slider.horizontal.below.rectangle")
.font(.system(size: 18, weight: .medium))
.padding(10)
.background(
Circle().fill(Color.black.opacity(0.45)))
.foregroundColor(.white)
.onReceive(NotificationCenter.default.publisher(
for: .toggleLayer)) { note in
handleLayerToggle(note)
}
.onReceive(NotificationCenter.default.publisher(
for: .setVizMode)) { note in
if let n = note.object as? Int { settings.vizMode = n }
}
// HUD coin haut-gauche : mode + touches + pose
HUDOverlay(settings: settings, poseListener: poseListener)
// Bouton settings coin haut-droit
HStack {
Spacer()
Button(action: { settings.showPanel.toggle() }) {
Image(systemName: settings.showPanel
? "slider.horizontal.3"
: "slider.horizontal.below.rectangle")
.font(.system(size: 18, weight: .medium))
.padding(10)
.background(
Circle().fill(Color.black.opacity(0.45)))
.foregroundColor(.white)
}
.buttonStyle(.plain)
.padding(16)
.help("Settings (S)")
}
.buttonStyle(.plain)
.padding(16)
.help("Settings (S)")
if settings.showPanel {
SettingsPanel(settings: settings)
.transition(.move(edge: .trailing)
.combined(with: .opacity))
HStack {
Spacer()
SettingsPanel(settings: settings)
.transition(.move(edge: .trailing)
.combined(with: .opacity))
}
}
}
.animation(.easeInOut(duration: 0.18), value: settings.showPanel)
}
private func handleLayerToggle(_ note: Notification) {
guard let key = note.object as? String else { return }
switch key {
case "camera": settings.showCamera.toggle()
case "scene": settings.showScene.toggle()
case "mesh": settings.showMesh.toggle()
case "wireframe": settings.showWireframe.toggle()
default: break
}
}
}
@@ -10,6 +10,7 @@ import SwiftUI
struct BodyView: NSViewRepresentable {
@ObservedObject var renderer: MeshRenderer
@ObservedObject var settings: RenderSettings
@ObservedObject var poseListener: PoseOSCListener
func makeNSView(context: Context) -> NSView {
let container = NSView(frame: .zero)
@@ -114,6 +115,24 @@ struct BodyView: NSViewRepresentable {
c.previewLayer?.isHidden = !settings.showCamera
c.mtkView?.isHidden = !settings.showScene
c.sceneRenderer?.uniforms.viz_mode = Float(settings.vizMode)
// Pose -> scene uniforms : drive hands3d (mode 8) et openpos
// (mode 9) avec la premiere personne detectee. Les wrists pilotent
// hand_l/r ; pose_count alimente bg_fragment.
let persons = poseListener.persons
c.sceneRenderer?.uniforms.pose_count = Float(persons.count)
c.sceneRenderer?.uniforms.pose_alive = persons.isEmpty ? 0 : 1
if let first = persons.values.first {
// Convertit coords ecran (0..1) -> NDC-ish (-1..1)
c.sceneRenderer?.uniforms.hand_l_x = (first.wristL.x - 0.5) * 2
c.sceneRenderer?.uniforms.hand_l_y = (first.wristL.y - 0.5) * 2
c.sceneRenderer?.uniforms.hand_r_x = (first.wristR.x - 0.5) * 2
c.sceneRenderer?.uniforms.hand_r_y = (first.wristR.y - 0.5) * 2
} else {
c.sceneRenderer?.uniforms.hand_l_x = 0
c.sceneRenderer?.uniforms.hand_l_y = 0
c.sceneRenderer?.uniforms.hand_r_x = 0
c.sceneRenderer?.uniforms.hand_r_y = 0
}
c.container?.layer?.backgroundColor = NSColor(
white: CGFloat(settings.bgBrightness), alpha: 1.0).cgColor
c.cameraEntity?.camera.fieldOfViewInDegrees =
@@ -0,0 +1,69 @@
import SwiftUI
/// HUD coin haut-gauche : mode actif, nb personnes, fps approx, et la
/// liste des touches actives (mapping clavier). Translucide, ne capte
/// pas les events souris.
struct HUDOverlay: View {
@ObservedObject var settings: RenderSettings
@ObservedObject var poseListener: PoseOSCListener
var body: some View {
VStack(alignment: .leading, spacing: 6) {
// Statut courant
HStack(spacing: 8) {
Image(systemName: "circle.fill")
.font(.system(size: 8))
.foregroundColor(poseListener.count > 0
? .green : .gray)
Text("AV-Live-Body").bold()
Text("\(settings.vizModeName) (#\(settings.vizMode))")
.foregroundColor(.pink)
Spacer()
Text("pose: \(poseListener.count)")
.foregroundColor(.white.opacity(0.7))
}
Divider().background(Color.white.opacity(0.2))
// Liste des touches actives
VStack(alignment: .leading, spacing: 2) {
hudKey("S", "ouvre / ferme le panel reglages")
hudKey("0..9", "viz mode (storm / ... / openpos)")
hudKey("M", "toggle Maillage SMPL-X")
hudKey("W", "toggle Fil de fer")
hudKey("C", "toggle Webcam")
hudKey("V", "toggle Scene Metal")
}
.font(.system(size: 11, design: .monospaced))
.foregroundColor(.white.opacity(0.75))
// Position 3D de la 1ere personne (si detectee)
if let first = poseListener.persons.values.first {
Divider().background(Color.white.opacity(0.2))
Text(String(format:
"head (%.2f, %.2f) wristL (%.2f, %.2f) wristR (%.2f, %.2f)",
first.head.x, first.head.y,
first.wristL.x, first.wristL.y,
first.wristR.x, first.wristR.y))
.font(.system(size: 10, design: .monospaced))
.foregroundColor(.white.opacity(0.6))
}
}
.padding(12)
.frame(maxWidth: 320, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.black.opacity(0.55))
)
.padding(12)
.allowsHitTesting(false)
}
private func hudKey(_ key: String, _ desc: String) -> some View {
HStack(spacing: 6) {
Text(key)
.frame(minWidth: 28, alignment: .leading)
.foregroundColor(.pink)
Text(desc)
}
}
}