feat: Placement 하복부 일러스트 이미지 + UI 개선
Placement: - Canvas 실루엣 → 하복부 일러스트 PNG 배경 (투명 배경 처리) - 방광+센서+화살표는 Canvas 오버레이 유지 - GREEN 진입/이탈 즉시 반영 (디바운스 무시) - 좌측 라벨(가슴/배꼽/치골) 제거 Spot 측정: - 3회 측정 중 화면 깜빡임 방지 (이전 값 유지, 최종 median만 표시) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
@@ -828,12 +828,14 @@ fun PiezoMonitoringView(appState: AppState) {
|
|||||||
if (!isMeasuring && !isAutoMeasuring && !spotCooldown && !spotInProgress) {
|
if (!isMeasuring && !isAutoMeasuring && !spotCooldown && !spotInProgress) {
|
||||||
spotInProgress = true
|
spotInProgress = true
|
||||||
spotCooldown = true
|
spotCooldown = true
|
||||||
|
val savedVol = lastVolumeMl // 측정 중 화면 깜빡임 방지
|
||||||
spotScope.launch {
|
spotScope.launch {
|
||||||
val spotResults = mutableListOf<Double>()
|
val spotResults = mutableListOf<Double>()
|
||||||
repeat(3) {
|
repeat(3) {
|
||||||
measure()
|
measure()
|
||||||
while (isMeasuring) { kotlinx.coroutines.delay(100) }
|
while (isMeasuring) { kotlinx.coroutines.delay(100) }
|
||||||
if (lastVolumeMl > 0) spotResults.add(lastVolumeMl)
|
if (lastVolumeMl > 0) spotResults.add(lastVolumeMl)
|
||||||
|
lastVolumeMl = savedVol // 중간 값 복원
|
||||||
kotlinx.coroutines.delay(200)
|
kotlinx.coroutines.delay(200)
|
||||||
}
|
}
|
||||||
if (spotResults.size >= 2) {
|
if (spotResults.size >= 2) {
|
||||||
|
|||||||
+23
-61
@@ -149,7 +149,7 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
.padding(horizontal = 24.dp),
|
.padding(horizontal = 24.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
// ── 상반신 일러스트 + 센서 + 방향 가이드 ──
|
// ── 하복부 이미지 + 센서 + 방향 가이드 ──
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -157,55 +157,26 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
.padding(top = 8.dp),
|
.padding(top = 8.dp),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
// 배경 이미지 (하복부 실루엣)
|
||||||
|
androidx.compose.foundation.Image(
|
||||||
|
painter = androidx.compose.ui.res.painterResource(com.example.medilightv2android.R.drawable.placement_body),
|
||||||
|
contentDescription = "Body",
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = androidx.compose.ui.layout.ContentScale.Fit,
|
||||||
|
alpha = if (isGreenZone) 0.6f else 0.9f
|
||||||
|
)
|
||||||
|
|
||||||
|
// 오버레이: 방광 + 센서 + 화살표
|
||||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
val w = size.width
|
val w = size.width
|
||||||
val h = size.height
|
val h = size.height
|
||||||
val cx = w / 2f
|
val cx = w / 2f
|
||||||
val greenZone = isGreenZone
|
val greenZone = isGreenZone
|
||||||
|
|
||||||
// ── 상반신 실루엣 ──
|
// 방광 위치 (이미지의 배꼽~치골 사이)
|
||||||
val skinColor = Color(0xFFFFDDB5)
|
val bladderCy = h * 0.58f
|
||||||
val outlineColor = Color(0xFFD4A574)
|
val bladderRx = w * 0.12f
|
||||||
|
val bladderRy = h * 0.10f
|
||||||
// 몸통 (위: 가슴 아래, 아래: 치골)
|
|
||||||
val bodyTop = h * 0.05f
|
|
||||||
val bodyBot = h * 0.88f
|
|
||||||
val bodyW = w * 0.38f
|
|
||||||
|
|
||||||
// 몸통 타원형
|
|
||||||
val bodyPath = Path().apply {
|
|
||||||
moveTo(cx - bodyW, bodyTop)
|
|
||||||
// 왼쪽 어깨~허리 곡선
|
|
||||||
cubicTo(cx - bodyW * 1.15f, bodyTop + h * 0.15f,
|
|
||||||
cx - bodyW * 0.85f, bodyBot - h * 0.1f,
|
|
||||||
cx - bodyW * 0.7f, bodyBot)
|
|
||||||
// 아래쪽 (치골)
|
|
||||||
lineTo(cx + bodyW * 0.7f, bodyBot)
|
|
||||||
// 오른쪽 허리~어깨
|
|
||||||
cubicTo(cx + bodyW * 0.85f, bodyBot - h * 0.1f,
|
|
||||||
cx + bodyW * 1.15f, bodyTop + h * 0.15f,
|
|
||||||
cx + bodyW, bodyTop)
|
|
||||||
close()
|
|
||||||
}
|
|
||||||
drawPath(bodyPath, skinColor)
|
|
||||||
drawPath(bodyPath, outlineColor, style = Stroke(width = 2f))
|
|
||||||
|
|
||||||
// 배꼽
|
|
||||||
val navelY = h * 0.40f
|
|
||||||
drawCircle(outlineColor.copy(alpha = 0.5f), radius = 4f, center = Offset(cx, navelY))
|
|
||||||
|
|
||||||
// 치골 라인
|
|
||||||
val pubisY = h * 0.82f
|
|
||||||
drawLine(outlineColor.copy(alpha = 0.4f),
|
|
||||||
Offset(cx - bodyW * 0.5f, pubisY),
|
|
||||||
Offset(cx + bodyW * 0.5f, pubisY),
|
|
||||||
strokeWidth = 1.5f,
|
|
||||||
pathEffect = PathEffect.dashPathEffect(floatArrayOf(8f, 4f)))
|
|
||||||
|
|
||||||
// 방광 (점선 타원, 배꼽과 치골 사이)
|
|
||||||
val bladderCy = (navelY + pubisY) / 2f
|
|
||||||
val bladderRx = bodyW * 0.35f
|
|
||||||
val bladderRy = (pubisY - navelY) * 0.35f
|
|
||||||
drawOval(
|
drawOval(
|
||||||
color = if (greenZone) Color(0xFF4CAF50).copy(alpha = 0.15f) else Color.Cyan.copy(alpha = 0.12f),
|
color = if (greenZone) Color(0xFF4CAF50).copy(alpha = 0.15f) else Color.Cyan.copy(alpha = 0.12f),
|
||||||
topLeft = Offset(cx - bladderRx, bladderCy - bladderRy),
|
topLeft = Offset(cx - bladderRx, bladderCy - bladderRy),
|
||||||
@@ -218,9 +189,9 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f)))
|
style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f)))
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── 센서 기기 (둥근 사각형) ──
|
// 센서 기기 (둥근 사각형)
|
||||||
val sensorW = 50f
|
val sensorW = 48f
|
||||||
val sensorH = 70f
|
val sensorH = 64f
|
||||||
val sensorY = bladderCy - sensorH / 2
|
val sensorY = bladderCy - sensorH / 2
|
||||||
val sensorColor = if (greenZone) Color(0xFF4CAF50) else MlPrimary
|
val sensorColor = if (greenZone) Color(0xFF4CAF50) else MlPrimary
|
||||||
drawRoundRect(
|
drawRoundRect(
|
||||||
@@ -303,19 +274,6 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 라벨 (좌측)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.CenterStart)
|
|
||||||
.padding(start = 4.dp),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(0.dp)
|
|
||||||
) {
|
|
||||||
Text("가슴", fontSize = 9.sp, color = MlSecondaryText)
|
|
||||||
Spacer(modifier = Modifier.height(60.dp))
|
|
||||||
Text("배꼽", fontSize = 9.sp, color = MlSecondaryText)
|
|
||||||
Spacer(modifier = Modifier.height(60.dp))
|
|
||||||
Text("치골", fontSize = 9.sp, color = MlSecondaryText)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 가이드 텍스트 (일러스트 아래) ──
|
// ── 가이드 텍스트 (일러스트 아래) ──
|
||||||
@@ -545,7 +503,11 @@ fun PlacementGuideView(appState: AppState) {
|
|||||||
|
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val elapsed = now - lastHintChangeMs
|
val elapsed = now - lastHintChangeMs
|
||||||
if ((debounceCount >= 2 && elapsed >= 3000) || guideResult.isPass) {
|
// GREEN 진입/이탈은 즉시, 일반 힌트는 디바운스
|
||||||
|
val wasGreen = isLocked
|
||||||
|
val isNowGreen = guideResult.isPass
|
||||||
|
val greenChanged = wasGreen != isNowGreen
|
||||||
|
if (greenChanged || (debounceCount >= 2 && elapsed >= 3000)) {
|
||||||
directionHint = guideResult.hint
|
directionHint = guideResult.hint
|
||||||
directionIcon = guideResult.icon
|
directionIcon = guideResult.icon
|
||||||
placementScore = guideResult.score
|
placementScore = guideResult.score
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 121 KiB |
Reference in New Issue
Block a user