Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2260585cfe | ||
|
|
e89188a34e | ||
|
|
39eac8e70b | ||
|
|
3c682c1b30 | ||
|
|
bb6698a028 | ||
|
|
3227d08e2d | ||
|
|
f30bf103be | ||
|
|
c461bcc433 | ||
|
|
c6501d00f2 | ||
|
|
b907747755 | ||
|
|
18fc5596ef |
@@ -307,10 +307,17 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
|||||||
// Combined with poseEnabled (rather than posePhase alone) so
|
// Combined with poseEnabled (rather than posePhase alone) so
|
||||||
// the label can tell "pose off" (hidden) apart from "pose on
|
// the label can tell "pose off" (hidden) apart from "pose on
|
||||||
// but not yet in the target posture" (amber prompt) -- both
|
// but not yet in the target posture" (amber prompt) -- both
|
||||||
// cases otherwise report a null phase.
|
// cases otherwise report a null phase. poseCorrection rides
|
||||||
|
// along the same collector (rather than its own, like
|
||||||
|
// poseMetrics below) since it directly changes what
|
||||||
|
// renderPosePhase puts in the label -- see that function.
|
||||||
launch {
|
launch {
|
||||||
combine(viewModel.poseEnabled, viewModel.posePhase) { enabled, phase -> enabled to phase }
|
combine(
|
||||||
.collect { (enabled, phase) -> renderPosePhase(enabled, phase) }
|
viewModel.poseEnabled,
|
||||||
|
viewModel.posePhase,
|
||||||
|
viewModel.poseCorrection,
|
||||||
|
) { enabled, phase, correction -> Triple(enabled, phase, correction) }
|
||||||
|
.collect { (enabled, phase, correction) -> renderPosePhase(enabled, phase, correction) }
|
||||||
}
|
}
|
||||||
// Raw angle readout backing the label above -- its own
|
// Raw angle readout backing the label above -- its own
|
||||||
// collector since it's driven by a separate StateFlow
|
// collector since it's driven by a separate StateFlow
|
||||||
@@ -395,15 +402,24 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
|||||||
*
|
*
|
||||||
* @param poseEnabled Whether pose detection is currently on at all.
|
* @param poseEnabled Whether pose detection is currently on at all.
|
||||||
* @param phase The bowler's current delivery phase, or null if none currently validates.
|
* @param phase The bowler's current delivery phase, or null if none currently validates.
|
||||||
|
* @param correction A specific "here's what to fix" instruction from
|
||||||
|
* [PosePhaseDetector] when [phase] is null and the bowler is
|
||||||
|
* being measured against one of the stationary phases (starting
|
||||||
|
* stance, pushaway, slide & release) -- shown in place of the
|
||||||
|
* generic "waiting" message when present, so the bowler knows
|
||||||
|
* exactly what to adjust instead of just that they're not there yet.
|
||||||
*/
|
*/
|
||||||
private fun renderPosePhase(poseEnabled: Boolean, phase: BowlingPhase?) {
|
private fun renderPosePhase(poseEnabled: Boolean, phase: BowlingPhase?, correction: String?) {
|
||||||
if (!poseEnabled) {
|
if (!poseEnabled) {
|
||||||
binding.textPoseFeedback.visibility = View.GONE
|
binding.textPoseFeedback.visibility = View.GONE
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
binding.textPoseFeedback.visibility = View.VISIBLE
|
binding.textPoseFeedback.visibility = View.VISIBLE
|
||||||
// Every other BowlingPhase falls back to the "waiting" message too --
|
// Every confirmed phase gets its own label/color below; an
|
||||||
// see PosePhaseDetector's class doc, only STARTING_STANCE is detected today.
|
// unconfirmed one falls back to a specific correction when
|
||||||
|
// PosePhaseDetector has one (see its class doc), otherwise the
|
||||||
|
// generic "waiting" message -- e.g. mid-approach, where per-frame
|
||||||
|
// correction isn't meaningful.
|
||||||
when (phase) {
|
when (phase) {
|
||||||
BowlingPhase.STARTING_STANCE -> {
|
BowlingPhase.STARTING_STANCE -> {
|
||||||
binding.textPoseFeedback.text = getString(R.string.pose_phase_starting_stance)
|
binding.textPoseFeedback.text = getString(R.string.pose_phase_starting_stance)
|
||||||
@@ -429,8 +445,12 @@ class BowlingCameraActivity : AppCompatActivity(), CameraXController.Callback {
|
|||||||
binding.textPoseFeedback.text = getString(R.string.pose_phase_slide_and_release)
|
binding.textPoseFeedback.text = getString(R.string.pose_phase_slide_and_release)
|
||||||
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Slide_and_release_ready))
|
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Slide_and_release_ready))
|
||||||
}
|
}
|
||||||
|
BowlingPhase.FOLLOW_THROUGH -> {
|
||||||
|
binding.textPoseFeedback.text = getString(R.string.pose_phase_follow_through)
|
||||||
|
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Follow_through_ready))
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
binding.textPoseFeedback.text = getString(R.string.pose_phase_waiting)
|
binding.textPoseFeedback.text = correction ?: getString(R.string.pose_phase_waiting)
|
||||||
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_waiting))
|
binding.textPoseFeedback.setBackgroundColor(ContextCompat.getColor(this, R.color.Starting_stance_waiting))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,13 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
//This frame's torso/knee/elbow angle readings, or null if pose detection is off.
|
//This frame's torso/knee/elbow angle readings, or null if pose detection is off.
|
||||||
val poseMetrics: StateFlow<PosePhaseDetector.Metrics?> = _poseMetrics.asStateFlow()
|
val poseMetrics: StateFlow<PosePhaseDetector.Metrics?> = _poseMetrics.asStateFlow()
|
||||||
|
|
||||||
|
// Specific "here's what to fix" instruction while holding one of the
|
||||||
|
// stationary phases (starting stance, pushaway, slide & release) -- see
|
||||||
|
// PosePhaseDetector.correctionFor's doc for why only those three.
|
||||||
|
private val _poseCorrection = MutableStateFlow<String?>(null)
|
||||||
|
/** @brief Live corrective instruction for the current stationary phase, or null if nothing to correct. */
|
||||||
|
val poseCorrection: StateFlow<String?> = _poseCorrection.asStateFlow()
|
||||||
|
|
||||||
/** @brief Steps detected so far in the current attempt, since the last reset. */
|
/** @brief Steps detected so far in the current attempt, since the last reset. */
|
||||||
val stepEvents: StateFlow<List<StepEvent>> get() = stepCountingSession.stepEvents
|
val stepEvents: StateFlow<List<StepEvent>> get() = stepCountingSession.stepEvents
|
||||||
|
|
||||||
@@ -132,6 +139,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
posePhaseDetector.reset()
|
posePhaseDetector.reset()
|
||||||
_posePhase.value = null
|
_posePhase.value = null
|
||||||
_poseMetrics.value = null
|
_poseMetrics.value = null
|
||||||
|
_poseCorrection.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +161,7 @@ class CameraViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
val phaseResult = posePhaseDetector.update(landmarks, angles)
|
val phaseResult = posePhaseDetector.update(landmarks, angles)
|
||||||
_posePhase.value = phaseResult.phase
|
_posePhase.value = phaseResult.phase
|
||||||
_poseMetrics.value = phaseResult.metrics
|
_poseMetrics.value = phaseResult.metrics
|
||||||
|
_poseCorrection.value = phaseResult.correction
|
||||||
|
|
||||||
// Check if the current pose matches the Starting Stance (pure posture query)
|
// Check if the current pose matches the Starting Stance (pure posture query)
|
||||||
val isStartingStance = (phaseResult.phase == BowlingPhase.STARTING_STANCE)
|
val isStartingStance = (phaseResult.phase == BowlingPhase.STARTING_STANCE)
|
||||||
|
|||||||
@@ -406,6 +406,7 @@ class CameraXController(
|
|||||||
BowlingPhase.BACK_SWING -> "Backswing"
|
BowlingPhase.BACK_SWING -> "Backswing"
|
||||||
BowlingPhase.POWER_STEP -> "Power Step"
|
BowlingPhase.POWER_STEP -> "Power Step"
|
||||||
BowlingPhase.SLIDE_AND_RELEASE -> "Slide & Release"
|
BowlingPhase.SLIDE_AND_RELEASE -> "Slide & Release"
|
||||||
|
BowlingPhase.FOLLOW_THROUGH -> "Follow Through"
|
||||||
}
|
}
|
||||||
val colorRes = when (phase) {
|
val colorRes = when (phase) {
|
||||||
BowlingPhase.STARTING_STANCE -> R.color.Starting_stance_ready
|
BowlingPhase.STARTING_STANCE -> R.color.Starting_stance_ready
|
||||||
@@ -414,6 +415,7 @@ class CameraXController(
|
|||||||
BowlingPhase.BACK_SWING -> R.color.Back_swing_ready
|
BowlingPhase.BACK_SWING -> R.color.Back_swing_ready
|
||||||
BowlingPhase.POWER_STEP -> R.color.Power_step_ready
|
BowlingPhase.POWER_STEP -> R.color.Power_step_ready
|
||||||
BowlingPhase.SLIDE_AND_RELEASE -> R.color.Slide_and_release_ready
|
BowlingPhase.SLIDE_AND_RELEASE -> R.color.Slide_and_release_ready
|
||||||
|
BowlingPhase.FOLLOW_THROUGH -> R.color.Follow_through_ready
|
||||||
}
|
}
|
||||||
val textPaint = overlayTextPaint.apply { textSize = 14f * scale }
|
val textPaint = overlayTextPaint.apply { textSize = 14f * scale }
|
||||||
val textWidth = textPaint.measureText(label)
|
val textWidth = textPaint.measureText(label)
|
||||||
|
|||||||
@@ -124,8 +124,16 @@ class PosePhaseDetector(
|
|||||||
* @brief One [update] call's outcome: the classified phase plus the raw angles it was based on.
|
* @brief One [update] call's outcome: the classified phase plus the raw angles it was based on.
|
||||||
* @param phase See [update]'s return doc.
|
* @param phase See [update]'s return doc.
|
||||||
* @param metrics This frame's raw angle readings, for display regardless of whether [phase] validated.
|
* @param metrics This frame's raw angle readings, for display regardless of whether [phase] validated.
|
||||||
|
* @param correction A specific corrective instruction (e.g. "Straighten
|
||||||
|
* your legs") when the bowler is being measured against one of
|
||||||
|
* [STATIONARY_PHASES] but doesn't currently match it, or null
|
||||||
|
* when there's nothing to correct -- either because the current
|
||||||
|
* posture already validates, the target phase is one of the
|
||||||
|
* moving phases this detector doesn't give live corrections for
|
||||||
|
* (see [correctionFor]'s doc), or a needed angle wasn't
|
||||||
|
* confidently read this frame.
|
||||||
*/
|
*/
|
||||||
data class Result(val phase: BowlingPhase?, val metrics: Metrics)
|
data class Result(val phase: BowlingPhase?, val metrics: Metrics, val correction: String? = null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Feeds one frame's landmarks/angles into the detector.
|
* @brief Feeds one frame's landmarks/angles into the detector.
|
||||||
@@ -160,7 +168,11 @@ class PosePhaseDetector(
|
|||||||
BowlingPhase.PUSHAWAY -> BowlingPhase.BACK_SWING
|
BowlingPhase.PUSHAWAY -> BowlingPhase.BACK_SWING
|
||||||
BowlingPhase.BACK_SWING -> BowlingPhase.POWER_STEP
|
BowlingPhase.BACK_SWING -> BowlingPhase.POWER_STEP
|
||||||
BowlingPhase.POWER_STEP -> BowlingPhase.SLIDE_AND_RELEASE
|
BowlingPhase.POWER_STEP -> BowlingPhase.SLIDE_AND_RELEASE
|
||||||
else -> currentPhase
|
// The only BowlingPhase not already matched above -- every
|
||||||
|
// other case (including null) is explicit, so reaching here
|
||||||
|
// means currentPhase is SLIDE_AND_RELEASE, there's no phase
|
||||||
|
// after it to advance to.
|
||||||
|
else -> BowlingPhase.SLIDE_AND_RELEASE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +184,6 @@ class PosePhaseDetector(
|
|||||||
BowlingPhase.BACK_SWING -> isBackSwingValid(metrics)
|
BowlingPhase.BACK_SWING -> isBackSwingValid(metrics)
|
||||||
BowlingPhase.POWER_STEP -> isPowerStepValid(metrics)
|
BowlingPhase.POWER_STEP -> isPowerStepValid(metrics)
|
||||||
BowlingPhase.SLIDE_AND_RELEASE -> isSlideAndReleaseValid(metrics)
|
BowlingPhase.SLIDE_AND_RELEASE -> isSlideAndReleaseValid(metrics)
|
||||||
else -> false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isTargetValid) {
|
if (isTargetValid) {
|
||||||
@@ -210,7 +221,13 @@ class PosePhaseDetector(
|
|||||||
consecutiveInvalidFrames = 0
|
consecutiveInvalidFrames = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result(currentPhase, metrics)
|
val correction = if (!isTargetValid && targetPhase in STATIONARY_PHASES) {
|
||||||
|
correctionFor(targetPhase, metrics)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result(currentPhase, metrics, correction)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @brief Clears all detection state. Call at the start of a new session/attempt. */
|
/** @brief Clears all detection state. Call at the start of a new session/attempt. */
|
||||||
@@ -297,11 +314,118 @@ class PosePhaseDetector(
|
|||||||
@Suppress("UNUSED_PARAMETER")
|
@Suppress("UNUSED_PARAMETER")
|
||||||
private fun isPowerStepValid(metrics: Metrics): Boolean = true
|
private fun isPowerStepValid(metrics: Metrics): Boolean = true
|
||||||
|
|
||||||
/** @brief Placeholder validation for Slide & Release phase (Step 5). */
|
/**
|
||||||
@Suppress("UNUSED_PARAMETER")
|
* @brief Checks whether this single frame's [Metrics] match the slide &
|
||||||
private fun isSlideAndReleaseValid(metrics: Metrics): Boolean = true
|
* release phase (finishing position, step 5).
|
||||||
|
*
|
||||||
|
* Slide & release is characterized by:
|
||||||
|
* - Torso Tilt: 15-45 degrees (the deepest forward lean of any phase --
|
||||||
|
* the bowler is bent into the slide)
|
||||||
|
* - Knee Angle: at least one knee bent to 90-150 degrees (the
|
||||||
|
* sliding/front leg lowers the body through the release; requiring
|
||||||
|
* only one, not both, since we don't know which leg is forward)
|
||||||
|
* - Elbow Angle: at least one elbow extended to 150-180 degrees (the
|
||||||
|
* swing arm straightens through the release -- same release cue
|
||||||
|
* [PoseStageAdvisor] already uses for its own final-step check)
|
||||||
|
*
|
||||||
|
* @param metrics This frame's raw angle readings.
|
||||||
|
* @return true if torso tilt, at least one bent knee, and at least one extended elbow all fall within range.
|
||||||
|
*/
|
||||||
|
private fun isSlideAndReleaseValid(metrics: Metrics): Boolean {
|
||||||
|
val torsoTilt = metrics.torsoTiltDegrees ?: return false
|
||||||
|
if (torsoTilt !in 15f..45f) return false
|
||||||
|
|
||||||
|
val kneeAngles = listOfNotNull(metrics.leftKneeAngleDegrees, metrics.rightKneeAngleDegrees)
|
||||||
|
if (kneeAngles.isEmpty() || kneeAngles.none { it in 90f..150f }) return false
|
||||||
|
|
||||||
|
val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees)
|
||||||
|
return elbowAngles.isNotEmpty() && elbowAngles.any { it in 150f..180f }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Produces a specific corrective instruction for why [metrics]
|
||||||
|
* doesn't currently match [targetPhase].
|
||||||
|
*
|
||||||
|
* Only covers [STATIONARY_PHASES] -- the three phases a bowler actually
|
||||||
|
* holds still in long enough for frame-by-frame angle feedback to be
|
||||||
|
* meaningful. The remaining phases (approach, backswing, power step) are
|
||||||
|
* mid-motion by nature, so a per-frame "here's what's wrong" cue would
|
||||||
|
* either be stale by the time it's read or just describe normal
|
||||||
|
* transitional movement as an error; those are left to [update]'s
|
||||||
|
* existing pass/fail phase label instead.
|
||||||
|
*
|
||||||
|
* Checks each phase's conditions in the same order as its `isXValid`
|
||||||
|
* counterpart and returns on the first one that fails, so the bowler
|
||||||
|
* gets one actionable instruction at a time rather than a list.
|
||||||
|
*
|
||||||
|
* @param targetPhase Which stationary phase to check [metrics] against. Must be one of [STATIONARY_PHASES].
|
||||||
|
* @param metrics This frame's raw angle readings.
|
||||||
|
* @return A short corrective instruction, or null if [metrics] already
|
||||||
|
* validates for [targetPhase] (nothing to correct) or a needed
|
||||||
|
* angle wasn't confidently read this frame (nothing useful to say yet).
|
||||||
|
*/
|
||||||
|
private fun correctionFor(targetPhase: BowlingPhase, metrics: Metrics): String? = when (targetPhase) {
|
||||||
|
BowlingPhase.STARTING_STANCE -> {
|
||||||
|
val torsoTilt = metrics.torsoTiltDegrees
|
||||||
|
val kneeAngles = listOfNotNull(metrics.leftKneeAngleDegrees, metrics.rightKneeAngleDegrees)
|
||||||
|
val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees)
|
||||||
|
when {
|
||||||
|
torsoTilt == null -> null
|
||||||
|
torsoTilt > torsoTiltMaxDegrees -> "Stand up straighter"
|
||||||
|
torsoTilt < torsoTiltMinDegrees -> "Relax your stance slightly"
|
||||||
|
kneeAngles.isEmpty() -> null
|
||||||
|
kneeAngles.any { it < kneeAngleMinDegrees } -> "Straighten your legs"
|
||||||
|
elbowAngles.isEmpty() -> null
|
||||||
|
elbowAngles.any { it > elbowAngleMaxDegrees } -> "Bring the ball in closer to your body"
|
||||||
|
elbowAngles.any { it < elbowAngleMinDegrees } -> "Relax your arms a little"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BowlingPhase.PUSHAWAY -> {
|
||||||
|
val torsoTilt = metrics.torsoTiltDegrees
|
||||||
|
val kneeAngles = listOfNotNull(metrics.leftKneeAngleDegrees, metrics.rightKneeAngleDegrees)
|
||||||
|
val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees)
|
||||||
|
when {
|
||||||
|
torsoTilt == null -> null
|
||||||
|
torsoTilt < 5f -> "Lean forward slightly as you push away"
|
||||||
|
torsoTilt > 25f -> "Don't lean too far forward yet"
|
||||||
|
kneeAngles.isEmpty() -> null
|
||||||
|
kneeAngles.any { it < 145f } -> "Keep your legs mostly straight here"
|
||||||
|
elbowAngles.isEmpty() -> null
|
||||||
|
elbowAngles.none { it in 130f..180f } -> "Push the ball further out"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BowlingPhase.SLIDE_AND_RELEASE -> {
|
||||||
|
val torsoTilt = metrics.torsoTiltDegrees
|
||||||
|
val kneeAngles = listOfNotNull(metrics.leftKneeAngleDegrees, metrics.rightKneeAngleDegrees)
|
||||||
|
val elbowAngles = listOfNotNull(metrics.leftElbowAngleDegrees, metrics.rightElbowAngleDegrees)
|
||||||
|
when {
|
||||||
|
torsoTilt == null -> null
|
||||||
|
torsoTilt < 15f -> "Bend forward more into the slide"
|
||||||
|
torsoTilt > 45f -> "Don't lean in too far"
|
||||||
|
kneeAngles.isEmpty() -> null
|
||||||
|
kneeAngles.none { it in 90f..150f } -> "Bend your sliding knee more"
|
||||||
|
elbowAngles.isEmpty() -> null
|
||||||
|
elbowAngles.none { it in 150f..180f } -> "Extend your swing arm fully"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* @brief The phases a bowler holds still in long enough for
|
||||||
|
* frame-by-frame angle correction to be meaningful -- see
|
||||||
|
* [correctionFor]'s doc for why the rest are excluded.
|
||||||
|
*/
|
||||||
|
private val STATIONARY_PHASES = setOf(
|
||||||
|
BowlingPhase.STARTING_STANCE,
|
||||||
|
BowlingPhase.PUSHAWAY,
|
||||||
|
BowlingPhase.SLIDE_AND_RELEASE,
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Maps a 5-step approach step count (0..5) to its corresponding [BowlingPhase].
|
* @brief Maps a 5-step approach step count (0..5) to its corresponding [BowlingPhase].
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,5 +21,6 @@
|
|||||||
<color name="Back_swing_ready">#CC9C27B0</color> <!-- purple -->
|
<color name="Back_swing_ready">#CC9C27B0</color> <!-- purple -->
|
||||||
<color name="Power_step_ready">#CCFF9800</color> <!-- orange/amber -->
|
<color name="Power_step_ready">#CCFF9800</color> <!-- orange/amber -->
|
||||||
<color name="Slide_and_release_ready">#CCE91E63</color> <!-- pink/red -->
|
<color name="Slide_and_release_ready">#CCE91E63</color> <!-- pink/red -->
|
||||||
|
<color name="Follow_through_ready">#CC00BCD4</color> <!-- cyan -->
|
||||||
<color name="final_position_highlight">#FFFFD600</color>
|
<color name="final_position_highlight">#FFFFD600</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -55,7 +55,8 @@
|
|||||||
<string name="pose_phase_pushaway">Pushaway</string>
|
<string name="pose_phase_pushaway">Pushaway</string>
|
||||||
<string name="pose_phase_back_swing">Backswing</string>
|
<string name="pose_phase_back_swing">Backswing</string>
|
||||||
<string name="pose_phase_power_step">Power Step</string>
|
<string name="pose_phase_power_step">Power Step</string>
|
||||||
<string name="pose_phase_slide_and_release">Slide & Release</string>
|
<string name="pose_phase_slide_and_release">Slide and Release</string>
|
||||||
|
<string name="pose_phase_follow_through">Follow Through</string>
|
||||||
<string name="pose_phase_waiting">Waiting for stance…</string>
|
<string name="pose_phase_waiting">Waiting for stance…</string>
|
||||||
<string name="pose_metrics_format">Torso: %1$s · Knee L: %2$s R: %3$s\nElbow L: %4$s R: %5$s</string>
|
<string name="pose_metrics_format">Torso: %1$s · Knee L: %2$s R: %3$s\nElbow L: %4$s R: %5$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -5,11 +5,30 @@
|
|||||||
## Suggested sections to include
|
## Suggested sections to include
|
||||||
|
|
||||||
- Problem statement / motivation
|
- Problem statement / motivation
|
||||||
|
Provide an app for users to improve their bowling by learning hwo to get into the correct positions.
|
||||||
- Target users
|
- Target users
|
||||||
|
Anyone who is able to bowl
|
||||||
- Proposed solution and key features
|
- Proposed solution and key features
|
||||||
|
Able to see their mistakes
|
||||||
|
Able to see instructions on how to correct their mistakes
|
||||||
|
Able to hear instructions on how to correct their mistakes
|
||||||
|
Able to see the ideal form
|
||||||
- Scope (in-scope vs. out-of-scope for this project)
|
- Scope (in-scope vs. out-of-scope for this project)
|
||||||
|
Five step bowling only
|
||||||
|
Right handed bowling only
|
||||||
|
Starting position correction and finishing position only
|
||||||
- Success criteria
|
- Success criteria
|
||||||
|
User is able to get into the correct position with help from the app
|
||||||
- Team members and roles
|
- Team members and roles
|
||||||
|
Lu Yong Wei (Product Owner)
|
||||||
|
Gabriel Low (Programmer/ Version control manager)
|
||||||
|
Harine S/O Sumen (Tech Lead)
|
||||||
|
Liu Jingwen (Programmer/ Serialization manager)
|
||||||
|
Auvik Kumar Biswas (Programmer/ Feedback manager)
|
||||||
|
Chan Qi Ying (Programmer/ UI lead)
|
||||||
|
Low Yu Sheng Javier (Product Manager)
|
||||||
|
Khalil Belabadia (Programmer/ Audio Lead)
|
||||||
|
|
||||||
|
|
||||||
## Working summary (placeholder, derived from the current codebase — replace with the real proposal)
|
## Working summary (placeholder, derived from the current codebase — replace with the real proposal)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "9.4.0"
|
agp = "9.3.0"
|
||||||
coreKtx = "1.18.0"
|
coreKtx = "1.18.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
#Fri Apr 10 12:25:49 SGT 2026
|
#Fri Apr 10 12:25:49 SGT 2026
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionSha256Sum=b266d5ff6b90eada6dc3b20cb090e3731302e553a27c5d3e4df1f0d76beaff06
|
distributionSha256Sum=bbaeb2fef8710818cf0e261201dab964c572f92b942812df0c3620d62a529a01
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.0-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
|
|||||||
Reference in New Issue
Block a user