UI: PlacementGuide(Sensor Alignment) 리디자인

빌드 107-121 누적 적용:
- 3-stop vertical gradient 배경 (MlTeal 14% / 흰 / MlPrimary 10%) — GREEN 진입 시 단색 톤으로 덮임
- 큰 "Sensor Alignment" 타이틀 38sp ExtraBold 검정 (Maximum Bladder Capacity 톤 통일)
- Hint 텍스트 "pubic bone" 인라인 빨강 ExtraBold 강조 (AnnotatedString)
- Canvas 방향 화살표 4종 제거 — hint 텍스트로만 방향 안내
- Canvas 치골 아치 라인+halo+endpoint 제거 — pubic bone 강조는 hint 인라인 1곳에만
- 실루엣 chestY 16% UP shift (0.06 → 0.09, 비례 유지)
- Body 상단 Spacer 8dp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 16:25:55 +09:00
parent 3e1a875b5b
commit 898fb77605
@@ -30,8 +30,11 @@ import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.example.medilightv2android.AppState
@@ -146,10 +149,18 @@ fun PlacementGuideView(appState: AppState) {
animationSpec = androidx.compose.animation.core.tween(600), label = "bg"
)
// 3-stop vertical gradient — MlTeal 14% / 흰 / MlPrimary 10% (GREEN 진입 시 단색 톤으로 덮임)
val gradientBrush = Brush.verticalGradient(
0f to MlTeal.copy(alpha = 0.14f),
0.5f to Color.White,
1f to MlPrimary.copy(alpha = 0.10f)
)
Column(
modifier = Modifier
.fillMaxSize()
.background(bgColor)
.background(if (isGreenZone) Brush.verticalGradient(listOf(bgColor, bgColor)) else gradientBrush)
) {
// Top bar
Row(
@@ -218,12 +229,22 @@ fun PlacementGuideView(appState: AppState) {
}
}
// ── 가이드 텍스트 (Step 아래, 몸 위) ──
// ── 큰 타이틀 "Sensor Alignment" 38sp ExtraBold 검정 (Maximum Bladder Capacity 톤 통일) ──
Text(
text = "Sensor Alignment",
fontSize = 38.sp,
fontWeight = FontWeight.ExtraBold,
color = Color.Black,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
)
// ── 가이드 텍스트 (Step 아래, 몸 위) — "pubic bone" 인라인 빨강 ExtraBold 강조 ──
val hintGreen = isGreenZone
val hintBg = if (hintGreen) Color(0xFF4CAF50).copy(alpha = 0.12f) else Color(0xFFFFF3E0)
val hintFg = if (hintGreen) Color(0xFF4CAF50) else Color(0xFFFF9800)
val dots = ".".repeat((scanCount % 3) + 1)
val displayHint = buildString {
val rawHint = buildString {
val hint = directionHint.ifEmpty { " " }
append(hint)
if (hint.startsWith("Slide") || hint.startsWith("Optimizing") ||
@@ -234,6 +255,19 @@ fun PlacementGuideView(appState: AppState) {
append(" (CV=${"%.3f".format(lastCv)})")
}
}
val displayHint = buildAnnotatedString {
val key = "pubic bone"
val idx = rawHint.indexOf(key, ignoreCase = true)
if (idx >= 0) {
append(rawHint.substring(0, idx))
withStyle(SpanStyle(color = Color.Red, fontWeight = FontWeight.ExtraBold)) {
append(rawHint.substring(idx, idx + key.length))
}
append(rawHint.substring(idx + key.length))
} else {
append(rawHint)
}
}
Text(
text = displayHint,
fontSize = 28.sp,
@@ -246,6 +280,9 @@ fun PlacementGuideView(appState: AppState) {
.padding(horizontal = 20.dp, vertical = 18.dp)
)
// 큰 타이틀과 몸 사이 간격 (Body 상단 Spacer 32→8dp)
Spacer(modifier = Modifier.height(8.dp))
// ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ──
Box(
modifier = Modifier
@@ -280,7 +317,8 @@ fun PlacementGuideView(appState: AppState) {
val navelInk = Color(0xFF5A3D26).copy(alpha = bodyAlpha)
// ── Anatomical landmarks (vertical %) ──
val chestY = h * 0.06f
// 빌드 120-121: 실루엣 Y값 16% UP shift, 비례 유지 (25→9% 또는 6→9% 누적)
val chestY = h * 0.09f
val waistY = h * 0.42f
val hipY = h * 0.76f
val pubisY = h * 0.96f
@@ -332,17 +370,7 @@ fun PlacementGuideView(appState: AppState) {
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))
// 빌드 111: Pubic arch 라인 + halo + endpoint 제거 — pubic bone 강조는 hint 인라인 1곳에만
// ── 방광 타원 (배꼽~치골 사이) ──
val bladderCy = (navelY + pubisY) / 2f
@@ -361,7 +389,7 @@ fun PlacementGuideView(appState: AppState) {
style = Stroke(width = 1.5f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(6f, 4f)))
)
// ── Sensor device (Canvas 안에서 drawImage — oval 중심에 정렬) ──
// ── Sensor device (Canvas drawImage — oval 중심에 정렬) ── [Charles 833c110]
// 너비를 bladder oval 직경 × 1.15 로 묶어 화면 비율 무관하게 비례.
val sensorTargetW = (bladderRx * 2f) * 1.15f
val sensorAspect = sensorBitmap.height.toFloat() / sensorBitmap.width.toFloat()
@@ -381,43 +409,7 @@ fun PlacementGuideView(appState: AppState) {
alpha = 0.95f,
)
// ── 방향 화살표 (센서/타원 바깥, 충분한 간격) ──
if (!greenZone) {
val arrowColor = Color(0xFFFF5722)
val arrowLen = 70f
val arrowHead = 28f
val sw = 9f
// 센서 영역 밖으로 확실히 벗어나는 간격
val sensorHalfH = h * 0.08f // 센서 이미지 대략 높이의 절반
when {
directionIcon.contains("up") -> {
// 타원 위 + 센서 위 확실히 벗어남
val ay = bladderCy - bladderRy - sensorHalfH - 20f
drawLine(arrowColor, Offset(cx, ay + arrowLen), Offset(cx, ay), strokeWidth = sw)
drawLine(arrowColor, Offset(cx - arrowHead, ay + arrowHead), Offset(cx, ay), strokeWidth = sw)
drawLine(arrowColor, Offset(cx + arrowHead, ay + arrowHead), Offset(cx, ay), strokeWidth = sw)
}
directionIcon.contains("down") -> {
val ay = bladderCy + bladderRy + sensorHalfH + 20f
drawLine(arrowColor, Offset(cx, ay - arrowLen), Offset(cx, ay), strokeWidth = sw)
drawLine(arrowColor, Offset(cx - arrowHead, ay - arrowHead), Offset(cx, ay), strokeWidth = sw)
drawLine(arrowColor, Offset(cx + arrowHead, ay - arrowHead), Offset(cx, ay), strokeWidth = sw)
}
directionIcon.contains("left") -> {
val ax = cx - bladderRx - 30f
drawLine(arrowColor, Offset(ax + arrowLen, bladderCy), Offset(ax, bladderCy), strokeWidth = sw)
drawLine(arrowColor, Offset(ax + arrowHead, bladderCy - arrowHead), Offset(ax, bladderCy), strokeWidth = sw)
drawLine(arrowColor, Offset(ax + arrowHead, bladderCy + arrowHead), Offset(ax, bladderCy), strokeWidth = sw)
}
directionIcon.contains("right") -> {
val ax = cx + bladderRx + 30f
drawLine(arrowColor, Offset(ax - arrowLen, bladderCy), Offset(ax, bladderCy), strokeWidth = sw)
drawLine(arrowColor, Offset(ax - arrowHead, bladderCy - arrowHead), Offset(ax, bladderCy), strokeWidth = sw)
drawLine(arrowColor, Offset(ax - arrowHead, bladderCy + arrowHead), Offset(ax, bladderCy), strokeWidth = sw)
}
}
}
// 빌드 111(e0f92eb): Canvas 방향 화살표 4종 제거 — hint 텍스트로만 방향 안내
}
// (sensor 이미지는 위 Canvas drawImage 로 이동 — body silhouette과 동일 좌표계)