diff --git a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt index 9cefd2d..4b67aee 100644 --- a/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt +++ b/app/src/main/java/com/example/medilightv2android/ui/views/monitoring/PlacementGuideView.kt @@ -149,7 +149,7 @@ fun PlacementGuideView(appState: AppState) { .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // ── 하복부 이미지 + 센서 + 방향 가이드 ── + // ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ── Box( modifier = Modifier .fillMaxWidth() @@ -157,50 +157,129 @@ fun PlacementGuideView(appState: AppState) { .padding(top = 8.dp), 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 — CKLaw body silhouette) + val bodyAlpha = if (isGreenZone) 0.6f else 0.9f Canvas(modifier = Modifier.fillMaxSize()) { val w = size.width val h = size.height val cx = w / 2f val greenZone = isGreenZone - // 방광 위치 (이미지의 배꼽~치골 사이) - val bladderRy = h * 0.05f - val bladderRx = w * 0.12f - val bladderCy = h * 0.48f + bladderRy // 위쪽 경계 고정 + // Body silhouette palette + val skinFill = Color(0xFFF2C5A0).copy(alpha = bodyAlpha) + val skinStroke = Color(0xFFBF947A).copy(alpha = bodyAlpha) + val skinShade = Color(0xFF8E6647).copy(alpha = bodyAlpha) + val navelInk = Color(0xFF5A3D26).copy(alpha = bodyAlpha) + + // ── Anatomical landmarks (vertical %) ── + val chestY = h * 0.06f + val waistY = h * 0.42f + val hipY = h * 0.76f + val pubisY = h * 0.96f + val chestW = w * 0.42f + val waistW = w * 0.32f + val hipW = w * 0.46f + + // ── Torso silhouette — hourglass via cubic bezier ── + val bodyPath = Path().apply { + moveTo(cx - chestW, chestY) + cubicTo( + cx - chestW * 1.05f, chestY + (waistY - chestY) * 0.45f, + cx - waistW * 1.05f, waistY - (waistY - chestY) * 0.20f, + cx - waistW, waistY) + cubicTo( + cx - waistW * 0.98f, waistY + (hipY - waistY) * 0.35f, + cx - hipW * 0.95f, hipY - (hipY - waistY) * 0.20f, + cx - hipW, hipY) + cubicTo( + cx - hipW * 0.92f, hipY + (pubisY - hipY) * 0.55f, + cx - hipW * 0.45f, pubisY, + cx, pubisY) + cubicTo( + cx + hipW * 0.45f, pubisY, + cx + hipW * 0.92f, hipY + (pubisY - hipY) * 0.55f, + cx + hipW, hipY) + cubicTo( + cx + hipW * 0.95f, hipY - (hipY - waistY) * 0.20f, + cx + waistW * 0.98f, waistY + (hipY - waistY) * 0.35f, + cx + waistW, waistY) + cubicTo( + cx + waistW * 1.05f, waistY - (waistY - chestY) * 0.20f, + cx + chestW * 1.05f, chestY + (waistY - chestY) * 0.45f, + cx + chestW, chestY) + close() + } + drawPath(bodyPath, skinFill) + drawPath(bodyPath, skinStroke, style = Stroke(width = 1.6f)) + + // ── Costal margin (갈비뼈 라인) ── + val ribY = chestY + h * 0.10f + val ribPathL = Path().apply { + moveTo(cx - chestW * 0.85f, chestY + h * 0.04f) + cubicTo( + cx - chestW * 0.55f, chestY + h * 0.13f, + cx - chestW * 0.20f, chestY + h * 0.13f, + cx, ribY) + } + val ribPathR = Path().apply { + moveTo(cx + chestW * 0.85f, chestY + h * 0.04f) + cubicTo( + cx + chestW * 0.55f, chestY + h * 0.13f, + cx + chestW * 0.20f, chestY + h * 0.13f, + cx, ribY) + } + drawPath(ribPathL, skinShade.copy(alpha = 0.55f * bodyAlpha), + style = Stroke(width = 1.2f)) + drawPath(ribPathR, skinShade.copy(alpha = 0.55f * bodyAlpha), + style = Stroke(width = 1.2f)) + + // ── Linea alba (복부 정중선) ── + val navelY = waistY + (hipY - waistY) * 0.20f + drawLine(skinShade.copy(alpha = 0.30f * bodyAlpha), + Offset(cx, ribY + 4f), Offset(cx, navelY - 6f), + strokeWidth = 1.0f) + + // ── Navel (배꼽) ── + drawCircle(navelInk, 3.5f, Offset(cx, navelY)) + drawCircle(skinShade.copy(alpha = 0.4f * bodyAlpha), 5.5f, Offset(cx, navelY), + style = Stroke(width = 0.8f)) + + // ── Pubic arch (치골 아치) ── + val archY = pubisY - h * 0.04f + val archPath = Path().apply { + moveTo(cx - hipW * 0.30f, archY) + cubicTo( + cx - hipW * 0.10f, archY + h * 0.025f, + cx + hipW * 0.10f, archY + h * 0.025f, + cx + hipW * 0.30f, archY) + } + drawPath(archPath, skinShade.copy(alpha = 0.45f * bodyAlpha), + style = Stroke(width = 1.4f)) + + // ── 방광 타원 (배꼽~치골 사이) ── + val bladderCy = (navelY + pubisY) / 2f + val bladderRx = hipW * 0.42f + val bladderRy = bladderRx * 0.70f + val ovalColor = if (greenZone) Color(0xFF4CAF50) else Color.Cyan drawOval( - color = if (greenZone) Color(0xFF4CAF50).copy(alpha = 0.15f) else Color.Cyan.copy(alpha = 0.12f), + color = ovalColor.copy(alpha = 0.12f), topLeft = Offset(cx - bladderRx, bladderCy - bladderRy), size = Size(bladderRx * 2, bladderRy * 2) ) drawOval( - color = if (greenZone) Color(0xFF4CAF50).copy(alpha = 0.4f) else Color.Cyan.copy(alpha = 0.35f), + color = ovalColor.copy(alpha = 0.4f), topLeft = Offset(cx - bladderRx, bladderCy - bladderRy), size = Size(bladderRx * 2, bladderRy * 2), style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f))) ) - // 센서 위치 계산 (이미지 오버레이용) - val sensorW = 80f - val sensorH = 50f - val sensorY = bladderCy - sensorH / 2 - - // ── 방향 화살표 (타원 바깥에 크게) ── + // ── 방향 화살표 (방광 타원 바깥) ── if (!greenZone) { val arrowColor = Color(0xFFFF5722) val arrowLen = 50f val arrowHead = 20f val sw = 7f - val gap = 12f // 타원 경계에서 간격 + val gap = 12f when { directionIcon.contains("up") -> { @@ -230,14 +309,14 @@ fun PlacementGuideView(appState: AppState) { } } - // Green 체크마크 + // ── Green 체크마크 ── if (greenZone) { - drawCircle(Color(0xFF4CAF50).copy(alpha = 0.2f), radius = 35f, center = Offset(cx, sensorY - 30f)) - // ✓ 모양 + val checkCy = bladderCy - bladderRy - 40f + drawCircle(Color(0xFF4CAF50).copy(alpha = 0.2f), radius = 35f, center = Offset(cx, checkCy)) val checkPath = Path().apply { - moveTo(cx - 15f, sensorY - 30f) - lineTo(cx - 5f, sensorY - 20f) - lineTo(cx + 15f, sensorY - 42f) + moveTo(cx - 15f, checkCy) + lineTo(cx - 5f, checkCy + 10f) + lineTo(cx + 15f, checkCy - 12f) } drawPath(checkPath, Color(0xFF4CAF50), style = Stroke(width = 4f)) } @@ -250,7 +329,7 @@ fun PlacementGuideView(appState: AppState) { modifier = Modifier .size(width = 200.dp, height = 125.dp) .align(Alignment.Center) - .offset(x = 5.dp, y = 20.dp), + .offset(x = 5.dp, y = 115.dp), contentScale = androidx.compose.ui.layout.ContentScale.Fit, alpha = if (isGreenZone) 0.7f else 0.95f ) diff --git a/app/src/main/res/drawable/placement_body.png b/app/src/main/res/drawable/placement_body.png deleted file mode 100644 index 1015059..0000000 Binary files a/app/src/main/res/drawable/placement_body.png and /dev/null differ