refactor: placement body PNG → Canvas 실루엣 (CKLaw Bezier torso)

placement_body.png 제거, CKLaw BladderSeekScreen의 해부학적
Canvas 실루엣으로 교체 (갈비뼈, 배꼽, 치골 아치, hourglass torso).
sensor_device.png는 유지. 센서 위치 방광 타원 안쪽으로 조정.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-06 16:38:44 +09:00
parent 9d64faf8cf
commit 4b74b88f76
2 changed files with 110 additions and 31 deletions
@@ -149,7 +149,7 @@ fun PlacementGuideView(appState: AppState) {
.padding(horizontal = 24.dp), .padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
// ── 하복부 이미지 + 센서 + 방향 가이드 ── // ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ──
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -157,50 +157,129 @@ fun PlacementGuideView(appState: AppState) {
.padding(top = 8.dp), .padding(top = 8.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
// 배경 이미지 (하복부 실루엣) // 하복부 실루엣 (Canvas — CKLaw body silhouette)
androidx.compose.foundation.Image( val bodyAlpha = if (isGreenZone) 0.6f else 0.9f
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
// 방광 위치 (이미지의 배꼽~치골 사이) // Body silhouette palette
val bladderRy = h * 0.05f val skinFill = Color(0xFFF2C5A0).copy(alpha = bodyAlpha)
val bladderRx = w * 0.12f val skinStroke = Color(0xFFBF947A).copy(alpha = bodyAlpha)
val bladderCy = h * 0.48f + bladderRy // 위쪽 경계 고정 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( 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), topLeft = Offset(cx - bladderRx, bladderCy - bladderRy),
size = Size(bladderRx * 2, bladderRy * 2) size = Size(bladderRx * 2, bladderRy * 2)
) )
drawOval( 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), topLeft = Offset(cx - bladderRx, bladderCy - bladderRy),
size = Size(bladderRx * 2, bladderRy * 2), size = Size(bladderRx * 2, bladderRy * 2),
style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f))) style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f)))
) )
// 센서 위치 계산 (이미지 오버레이용) // ── 방향 화살표 (방광 타원 바깥) ──
val sensorW = 80f
val sensorH = 50f
val sensorY = bladderCy - sensorH / 2
// ── 방향 화살표 (타원 바깥에 크게) ──
if (!greenZone) { if (!greenZone) {
val arrowColor = Color(0xFFFF5722) val arrowColor = Color(0xFFFF5722)
val arrowLen = 50f val arrowLen = 50f
val arrowHead = 20f val arrowHead = 20f
val sw = 7f val sw = 7f
val gap = 12f // 타원 경계에서 간격 val gap = 12f
when { when {
directionIcon.contains("up") -> { directionIcon.contains("up") -> {
@@ -230,14 +309,14 @@ fun PlacementGuideView(appState: AppState) {
} }
} }
// Green 체크마크 // ── Green 체크마크 ──
if (greenZone) { 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 { val checkPath = Path().apply {
moveTo(cx - 15f, sensorY - 30f) moveTo(cx - 15f, checkCy)
lineTo(cx - 5f, sensorY - 20f) lineTo(cx - 5f, checkCy + 10f)
lineTo(cx + 15f, sensorY - 42f) lineTo(cx + 15f, checkCy - 12f)
} }
drawPath(checkPath, Color(0xFF4CAF50), style = Stroke(width = 4f)) drawPath(checkPath, Color(0xFF4CAF50), style = Stroke(width = 4f))
} }
@@ -250,7 +329,7 @@ fun PlacementGuideView(appState: AppState) {
modifier = Modifier modifier = Modifier
.size(width = 200.dp, height = 125.dp) .size(width = 200.dp, height = 125.dp)
.align(Alignment.Center) .align(Alignment.Center)
.offset(x = 5.dp, y = 20.dp), .offset(x = 5.dp, y = 115.dp),
contentScale = androidx.compose.ui.layout.ContentScale.Fit, contentScale = androidx.compose.ui.layout.ContentScale.Fit,
alpha = if (isGreenZone) 0.7f else 0.95f alpha = if (isGreenZone) 0.7f else 0.95f
) )
Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB