From 9af1442a11d820a3c42edeaada405deb2f627835 Mon Sep 17 00:00:00 2001 From: rUv Date: Sat, 22 Aug 2026 17:29:52 -0400 Subject: [PATCH] feat(ios): add native LiDAR capture UI --- .../native/RuViewLiDAR/ContentView.swift | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 integrations/iphone-lidar/native/RuViewLiDAR/ContentView.swift diff --git a/integrations/iphone-lidar/native/RuViewLiDAR/ContentView.swift b/integrations/iphone-lidar/native/RuViewLiDAR/ContentView.swift new file mode 100644 index 00000000..13eb0155 --- /dev/null +++ b/integrations/iphone-lidar/native/RuViewLiDAR/ContentView.swift @@ -0,0 +1,121 @@ +import SwiftUI + +struct ContentView: View { + @StateObject private var capture = LiDARCaptureManager() + @State private var endpoint = "ws://192.168.1.10:3001/ws/lidar" + @State private var streaming = false + @State private var status = "Idle" + + private let streamer = WebSocketStreamer() + + var body: some View { + NavigationStack { + Form { + Section("Sensor") { + HStack { + Text("State") + Spacer() + Text(stateText) + .foregroundStyle(stateColor) + } + HStack { + Text("Capture FPS") + Spacer() + Text(capture.framesPerSecond.formatted(.number.precision(.fractionLength(1)))) + } + if let frame = capture.lastFrame { + HStack { + Text("Depth") + Spacer() + Text("\(frame.depth.width) × \(frame.depth.height)") + } + HStack { + Text("Sequence") + Spacer() + Text("\(frame.provenance.sequence)") + } + } + + Button("Start LiDAR") { + capture.start(smoothed: false) + } + .disabled(capture.state == .running) + + Button("Stop") { + capture.stop() + } + .disabled(capture.state != .running) + } + + Section("RuView Stream") { + TextField("ws://host:port/ws/lidar", text: $endpoint) + .textInputAutocapitalization(.never) + .autocorrectionDisabled() + + Toggle("Stream geometry", isOn: $streaming) + .onChange(of: streaming) { _, enabled in + Task { + if enabled { + do { + try await streamer.connect(to: endpoint) + status = "Connected" + } catch { + streaming = false + status = error.localizedDescription + } + } else { + await streamer.disconnect() + status = "Disconnected" + } + } + } + + Text(status) + .font(.caption) + .foregroundStyle(.secondary) + } + + Section("Privacy") { + Text("This implementation transmits depth geometry, confidence, camera intrinsics, and device pose. RGB camera frames are not transmitted.") + .font(.footnote) + } + } + .navigationTitle("RuView LiDAR") + .onAppear { + capture.onFrame = { frame in + guard streaming else { return } + Task { + do { + try await streamer.send(frame, maxFPS: 15, sampleStep: 2) + } catch { + await MainActor.run { + status = error.localizedDescription + } + } + } + } + } + .onDisappear { + capture.stop() + Task { await streamer.disconnect() } + } + } + } + + private var stateText: String { + switch capture.state { + case .idle: return "Idle" + case .unsupported: return "No LiDAR" + case .running: return "Live" + case .failed(let message): return "Error: \(message)" + } + } + + private var stateColor: Color { + switch capture.state { + case .running: return .green + case .failed, .unsupported: return .red + case .idle: return .secondary + } + } +}