diff --git a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ClinicalLiveView.kt b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ClinicalLiveView.kt index 8567545..b4415ef 100644 --- a/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ClinicalLiveView.kt +++ b/app/src/main/java/com/medithings/vesiscan/ui/views/clinical/ClinicalLiveView.kt @@ -81,6 +81,17 @@ fun ClinicalLiveView(appState: AppState) { // V4.1 wall detection — 채널별 ant/post peak 표시용 (사내 테스트 가시화 요청) val v41 = remember { com.medithings.vesiscan.walldetect.V41Detector() } var channelWalls by remember { mutableStateOf?>>(List(6) { null }) } + // BV 실시간 표시 — cycle 마다 3 가지 method 병행 계산 (2026-07-15 사용자 요청). + // V41 = SphereFit (Method C) + // FRUSTUM = legacy analyzer walls + estimateBladderVolume6ch (Tanaka frustum) + // METHOD_D = Method D walls + apply_cross + estimateBladderVolume6ch (임상 Python 1:1) + var bvV41 by remember { mutableStateOf(null) } + var bvFrustum by remember { mutableStateOf(null) } + var bvMethodD by remember { mutableStateOf(null) } + // 사용자가 chip 으로 선택한 표시 method (GreenZoneConstants.bvMethod 와 동기). + var selectedBvMethod by remember { + mutableStateOf(com.medithings.vesiscan.managers.GreenZoneConstants.bvMethod) + } DisposableEffect(Unit) { bleManager.piezoCollector.onMultiChannelComplete = { channels -> @@ -109,6 +120,24 @@ fun ClinicalLiveView(appState: AppState) { val a = r?.antIdx; val p = r?.postIdx if (a != null && p != null) Pair(a, p) else null } + // BV[V41] = SphereFit 결과 (V4.1 detector 가 이미 계산). + bvV41 = result.summary.bvDispatch?.bvMl?.toDouble() + // BV[FRUSTUM] = V4.1 walls 를 frustum 알고리즘에 통과. + bvFrustum = com.medithings.vesiscan.managers.estimateBladderVolume6ch(channelWalls) + ?.volumeMl?.takeIf { it.isFinite() && it > 0 } + // BV[METHOD_D] = MethodD walls (apply_cross=true) + frustum. + val signalsD = (0..5).map { ch -> + channels.find { it.channel == ch }?.buffer?.let { buf -> + DoubleArray(buf.size) { buf[it].toDouble() } + } ?: DoubleArray(100) + } + val mdResults = com.medithings.vesiscan.walldetect.MethodDRunner + .detectMultichannel(signalsD, applyCross = true) + val mdWalls: List?> = mdResults.map { r -> + r?.let { Pair(it.ant, it.post) } + } + bvMethodD = com.medithings.vesiscan.managers.estimateBladderVolume6ch(mdWalls) + ?.volumeMl?.takeIf { it.isFinite() && it > 0 } } } bleManager.imuCollector.onComplete = { imuSamples -> @@ -181,6 +210,59 @@ fun ClinicalLiveView(appState: AppState) { Spacer(modifier = Modifier.height(8.dp)) + // ── BV 실시간 표시 + Method 선택 chip (2026-07-15) ── + // 6ch grid 아래에 세 method (V41/FRUSTUM/METHOD_D) 를 chip 으로 전환하며 + // 선택된 method 의 BV 를 큼직하게 표시. session log 에는 저장 안 함 (표시 전용). + val bvByMethod = mapOf( + com.medithings.vesiscan.managers.BvMethod.V41 to bvV41, + com.medithings.vesiscan.managers.BvMethod.FRUSTUM to bvFrustum, + com.medithings.vesiscan.managers.BvMethod.METHOD_D to bvMethodD, + ) + val currentBv = bvByMethod[selectedBvMethod] + Card( + modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp), + shape = RoundedCornerShape(10.dp), + colors = CardDefaults.cardColors(containerColor = MlCardBackground), + ) { + Column(modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp)) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text("BV", fontSize = 12.sp, color = MlSecondaryText, + fontWeight = FontWeight.SemiBold) + Spacer(modifier = Modifier.width(10.dp)) + Text(currentBv?.let { "${"%.1f".format(it)} mL" } ?: "— mL", + fontSize = 26.sp, fontWeight = FontWeight.ExtraBold, + color = MlPrimary) + Spacer(modifier = Modifier.weight(1f)) + Text(selectedBvMethod.name, fontSize = 11.sp, + color = MlSecondaryText, fontWeight = FontWeight.Medium) + } + Spacer(modifier = Modifier.height(6.dp)) + // Method 전환 chip row — 클릭 시 GreenZoneConstants.bvMethod 도 동기 갱신. + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + com.medithings.vesiscan.managers.BvMethod.values().forEach { m -> + val selected = m == selectedBvMethod + val bv = bvByMethod[m] + FilterChip( + selected = selected, + onClick = { + selectedBvMethod = m + com.medithings.vesiscan.managers.GreenZoneConstants.bvMethod = m + }, + label = { + Text( + "${m.name}: ${bv?.let { "%.0f".format(it) } ?: "-"}", + fontSize = 11.sp, + fontWeight = if (selected) FontWeight.Bold else FontWeight.Normal, + ) + }, + ) + } + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + // ── IMU 정보 ── if (lastImu.isNotEmpty()) { val posture = remember(lastImu) { ImuAnalysis.estimatePosture(lastImu) }