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.Stroke
import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.drawscope.rotate
import androidx.compose.ui.platform.LocalContext 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.font.FontWeight
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.example.medilightv2android.AppState import com.example.medilightv2android.AppState
@@ -146,10 +149,18 @@ fun PlacementGuideView(appState: AppState) {
animationSpec = androidx.compose.animation.core.tween(600), label = "bg" 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( Column(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(bgColor) .background(bgColor)
.background(if (isGreenZone) Brush.verticalGradient(listOf(bgColor, bgColor)) else gradientBrush)
) { ) {
// Top bar // Top bar
Row( 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 hintGreen = isGreenZone
val hintBg = if (hintGreen) Color(0xFF4CAF50).copy(alpha = 0.12f) else Color(0xFFFFF3E0) val hintBg = if (hintGreen) Color(0xFF4CAF50).copy(alpha = 0.12f) else Color(0xFFFFF3E0)
val hintFg = if (hintGreen) Color(0xFF4CAF50) else Color(0xFFFF9800) val hintFg = if (hintGreen) Color(0xFF4CAF50) else Color(0xFFFF9800)
val dots = ".".repeat((scanCount % 3) + 1) val dots = ".".repeat((scanCount % 3) + 1)
val displayHint = buildString { val rawHint = buildString {
val hint = directionHint.ifEmpty { " " } val hint = directionHint.ifEmpty { " " }
append(hint) append(hint)
if (hint.startsWith("Slide") || hint.startsWith("Optimizing") || if (hint.startsWith("Slide") || hint.startsWith("Optimizing") ||
@@ -234,6 +255,19 @@ fun PlacementGuideView(appState: AppState) {
append(" (CV=${"%.3f".format(lastCv)})") 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(
text = displayHint, text = displayHint,
fontSize = 28.sp, fontSize = 28.sp,
@@ -246,6 +280,9 @@ fun PlacementGuideView(appState: AppState) {
.padding(horizontal = 20.dp, vertical = 18.dp) .padding(horizontal = 20.dp, vertical = 18.dp)
) )
// 큰 타이틀과 몸 사이 간격 (Body 상단 Spacer 32→8dp)
Spacer(modifier = Modifier.height(8.dp))
// ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ── // ── 하복부 Canvas 실루엣 + 센서 + 방향 가이드 ──
Box( Box(
modifier = Modifier modifier = Modifier
@@ -280,7 +317,8 @@ fun PlacementGuideView(appState: AppState) {
val navelInk = Color(0xFF5A3D26).copy(alpha = bodyAlpha) val navelInk = Color(0xFF5A3D26).copy(alpha = bodyAlpha)
// ── Anatomical landmarks (vertical %) ── // ── 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 waistY = h * 0.42f
val hipY = h * 0.76f val hipY = h * 0.76f
val pubisY = h * 0.96f 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), drawCircle(skinShade.copy(alpha = 0.4f * bodyAlpha), 5.5f, Offset(cx, navelY),
style = Stroke(width = 0.8f)) style = Stroke(width = 0.8f))
// ── Pubic arch (치골 아치) ── // 빌드 111: Pubic arch 라인 + halo + endpoint 제거 — pubic bone 강조는 hint 인라인 1곳에만
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 bladderCy = (navelY + pubisY) / 2f
@@ -361,7 +389,7 @@ 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)))
) )
// ── Sensor device (Canvas 안에서 drawImage — oval 중심에 정렬) ── // ── Sensor device (Canvas drawImage — oval 중심에 정렬) ── [Charles 833c110]
// 너비를 bladder oval 직경 × 1.15 로 묶어 화면 비율 무관하게 비례. // 너비를 bladder oval 직경 × 1.15 로 묶어 화면 비율 무관하게 비례.
val sensorTargetW = (bladderRx * 2f) * 1.15f val sensorTargetW = (bladderRx * 2f) * 1.15f
val sensorAspect = sensorBitmap.height.toFloat() / sensorBitmap.width.toFloat() val sensorAspect = sensorBitmap.height.toFloat() / sensorBitmap.width.toFloat()
@@ -381,43 +409,7 @@ fun PlacementGuideView(appState: AppState) {
alpha = 0.95f, alpha = 0.95f,
) )
// ── 방향 화살표 (센서/타원 바깥, 충분한 간격) ── // 빌드 111(e0f92eb): Canvas 방향 화살표 4종 제거 — hint 텍스트로만 방향 안내
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)
}
}
}
} }
// (sensor 이미지는 위 Canvas drawImage 로 이동 — body silhouette과 동일 좌표계) // (sensor 이미지는 위 Canvas drawImage 로 이동 — body silhouette과 동일 좌표계)