package com.example.jnicpp.bowling import android.content.Context import android.graphics.Canvas import android.graphics.Matrix import android.graphics.Paint import android.util.AttributeSet import android.view.View import androidx.core.content.ContextCompat import com.example.jnicpp.R import com.google.mlkit.vision.pose.Pose /** * Draws the 33 ML Kit pose landmarks and connecting skeleton lines on top of * the camera preview. The landmark topology and the coordinate mapping math * (analysis-image pixels -> view pixels, replicating `PreviewView`'s * `FILL_CENTER` scaling) live in [PoseSkeletonRenderer], shared with * [CameraXController]'s baked-into-the-recorded-video overlay so both paths * can't drift apart. */ class PoseOverlayView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null ) : View(context, attrs) { private val jointPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ContextCompat.getColor(context, R.color.skeleton_joint) style = Paint.Style.FILL } private val bonePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ContextCompat.getColor(context, R.color.skeleton_bone) style = Paint.Style.STROKE strokeWidth = PoseSkeletonRenderer.STROKE_WIDTH } private val anglePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = ContextCompat.getColor(context, R.color.white) style = Paint.Style.FILL textSize = 36f isFakeBoldText = true // Readable regardless of what's behind it (skin, clothing, wall) -- // the skeleton paints get away without this since a line/dot has a // consistent look, but small text needs the contrast. setShadowLayer(4f, 0f, 0f, ContextCompat.getColor(context, R.color.black)) } private var pose: Pose? = null private var angles: PoseAngles? = null private var isFrontCamera = false // Source image size and rotation, as last reported by the analyzer -- // fed straight into PoseSkeletonRenderer.computeTransform on every // size/frame change. private var sourceWidth = 0 private var sourceHeight = 0 private var sourceRotationDegrees = 0 private var transform = Matrix() /** Called from the main thread with the latest analyzer result, or null to clear. */ fun update(frame: PoseAnalyzer.PoseFrameResult?) { pose = frame?.pose angles = frame?.angles if (frame != null) { isFrontCamera = frame.isFrontCamera sourceWidth = frame.imageWidth sourceHeight = frame.imageHeight sourceRotationDegrees = frame.rotationDegrees } recomputeTransform() invalidate() } fun clear() { pose = null angles = null invalidate() } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) recomputeTransform() } private fun recomputeTransform() { transform = PoseSkeletonRenderer.computeTransform( sourceWidth = sourceWidth, sourceHeight = sourceHeight, sourceRotationDegrees = sourceRotationDegrees, targetWidth = width, targetHeight = height, // Front camera preview is mirrored; flip the x axis about the // view's center so the overlay matches what's on screen. mirror = isFrontCamera ) } override fun onDraw(canvas: Canvas) { super.onDraw(canvas) val currentPose = pose ?: return PoseSkeletonRenderer.draw(canvas, currentPose, transform, bonePaint, jointPaint) angles?.let { PoseSkeletonRenderer.drawAngleLabels(canvas, currentPose, it, transform, anglePaint) } } }