diff --git a/launcher/AV-Live-Body/Sources/AVLiveBody/AVLiveBodyApp.swift b/launcher/AV-Live-Body/Sources/AVLiveBody/AVLiveBodyApp.swift index 3cabe32..38932dc 100644 --- a/launcher/AV-Live-Body/Sources/AVLiveBody/AVLiveBodyApp.swift +++ b/launcher/AV-Live-Body/Sources/AVLiveBody/AVLiveBodyApp.swift @@ -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 + } + } } diff --git a/launcher/AV-Live-Body/Sources/AVLiveBody/BodyView.swift b/launcher/AV-Live-Body/Sources/AVLiveBody/BodyView.swift index 190d0b5..77ce42a 100644 --- a/launcher/AV-Live-Body/Sources/AVLiveBody/BodyView.swift +++ b/launcher/AV-Live-Body/Sources/AVLiveBody/BodyView.swift @@ -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 = diff --git a/launcher/AV-Live-Body/Sources/AVLiveBody/HUDOverlay.swift b/launcher/AV-Live-Body/Sources/AVLiveBody/HUDOverlay.swift new file mode 100644 index 0000000..dd08edd --- /dev/null +++ b/launcher/AV-Live-Body/Sources/AVLiveBody/HUDOverlay.swift @@ -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) + } + } +}