@@ -19,7 +19,7 @@ enum ViewState: Equatable {
1919 case startVolume
2020 case instructions
2121 case introBoundingBox
22- case boundingBox( enterTime : Date )
22+ case boundingBox
2323 case introExercise( Exercise )
2424 case exercise( SessionData , enterTime: Date )
2525 case results( SessionData )
@@ -36,6 +36,15 @@ enum ViewState: Equatable {
3636 return nil
3737 }
3838 }
39+
40+ var features : [ QuickPose . Feature ] ? {
41+ switch self {
42+ case . introBoundingBox, . boundingBox:
43+ return [ . inside( edgeInsets: QuickPose . RelativeCameraEdgeInsets ( top: 0.1 , left: 0.2 , bottom: 0.01 , right: 0.2 ) ) ]
44+ default :
45+ return nil
46+ }
47+ }
3948}
4049
4150struct QuickPoseBasicView : View {
@@ -47,18 +56,12 @@ struct QuickPoseBasicView: View {
4756 @State private var feedbackText : String ? = nil
4857
4958 @State private var counter = QuickPoseThresholdCounter ( )
59+ @State private var unchanged = QuickPoseDoubleUnchangedDetector ( similarDuration: 2 , leniency: 0 )
5060 @State private var state : ViewState = . startVolume
5161
62+ @State private var boundingBoxVisibility = 1.0
5263 @State private var countScale = 1.0
5364 @State private var boundingBoxMaskWidth = 0.0
54-
55- func canMoveFromBoundingBox( landmarks: QuickPose . Landmarks ) -> Bool {
56-
57- let xsInBox = landmarks. poseLandmarks. allSatisfy { 0.5 - ( 0.6 / 2 ) < $0 [ 0 ] && $0 [ 0 ] < 0.5 + ( 0.6 / 2 ) }
58- let ysInBox = landmarks. poseLandmarks. allSatisfy { 0.5 - ( 0.8 / 2 ) < $0 [ 1 ] && $0 [ 1 ] < 0.5 + ( 0.8 / 2 ) }
59-
60- return xsInBox && ysInBox
61- }
6265
6366 var body : some View {
6467 GeometryReader { geometry in
@@ -102,12 +105,12 @@ struct QuickPoseBasicView: View {
102105 }
103106 . frame ( width: geometry. size. width * 0.6 , height: geometry. size. height * 0.8 )
104107 . padding ( . horizontal, ( geometry. size. width * 1 - 0.6 ) / 2 )
105-
108+
106109 case . boundingBox:
107110 ZStack {
108111 RoundedRectangle ( cornerRadius: 15 )
109- . stroke ( . green, lineWidth: 5 )
110-
112+ . stroke ( boundingBoxVisibility == 1 ? . green : . red , lineWidth: 5 )
113+
111114 RoundedRectangle ( cornerRadius: 15 )
112115 . fill ( . green. opacity ( 0.5 ) )
113116 . mask ( alignment: . leading) {
@@ -117,7 +120,7 @@ struct QuickPoseBasicView: View {
117120 }
118121 . frame ( width: geometry. size. width * 0.6 , height: geometry. size. height * 0.8 )
119122 . padding ( . horizontal, ( geometry. size. width * 1 - 0.6 ) / 2 )
120-
123+
121124 case . results( let results) :
122125 WorkoutResultsView ( sessionData: results)
123126 . environmentObject ( viewModel)
@@ -176,89 +179,94 @@ struct QuickPoseBasicView: View {
176179 AVSpeechSynthesizer ( ) . speak ( utterance)
177180 }
178181
179- if state == . introBoundingBox {
180- quickPose. start ( features: sessionConfig. exercise. features, onFrame: { status, image, features, feedback, landmarks in
181- overlayImage = image
182- if case . success( _, _) = status {
183-
184- switch state {
185- case . introBoundingBox:
186-
187- if let landmarks = landmarks, canMoveFromBoundingBox ( landmarks: landmarks) {
182+ if case . results( let result) = state {
183+ let sessionDataDump = SessionDataModel ( exercise: sessionConfig. exercise. name, count: result. count, seconds: result. seconds, date: Date ( ) )
184+ appendToJson ( sessionData: sessionDataDump)
185+ }
186+
187+ quickPose. update ( features: state. features ?? sessionConfig. exercise. features)
188+ }
189+ . onAppear ( ) {
190+ UIApplication . shared. isIdleTimerDisabled = true
191+ quickPose. start ( features: state. features ?? sessionConfig. exercise. features, onFrame: { status, image, features, feedback, landmarks in
192+ overlayImage = image
193+ if case . success( _, _) = status {
188194
189- state = . boundingBox( enterTime: Date ( ) )
190- boundingBoxMaskWidth = 0
191- withAnimation ( . easeInOut( duration: 2 ) ) {
192- boundingBoxMaskWidth = 1.0
193- }
194- }
195- case . boundingBox( let enterDate) :
196- if let landmarks = landmarks, canMoveFromBoundingBox ( landmarks: landmarks) {
197- if - enterDate. timeIntervalSinceNow > 2 {
198- state = . introExercise( sessionConfig. exercise)
195+ switch state {
196+ case . introBoundingBox:
197+
198+ if let result = features. first? . value, result. value == 1.0 {
199+ unchanged. reset ( )
200+ state = . boundingBox
201+ }
202+ case . boundingBox:
203+ if let dictionaryEntry = features. first {
204+ let result = dictionaryEntry. value
205+ boundingBoxVisibility = result. value
206+ unchanged. count ( result: result. value) {
207+ if result. value == 1 { // been in for 2 seconds
208+ boundingBoxMaskWidth = 0
209+ withAnimation ( . easeInOut( duration: 1 ) ) {
210+ boundingBoxMaskWidth = 1.0
211+ }
212+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 1 ) {
213+ state = . introExercise( sessionConfig. exercise)
214+ }
215+ } else {
216+ state = . introBoundingBox
199217 }
200- } else {
201- state = . introBoundingBox
202218 }
219+ }
203220
204- case . introExercise( _) :
205- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.5 ) {
206- state = . exercise( SessionData ( count: 0 , seconds: 0 ) , enterTime: Date ( ) )
207- }
208- case . exercise( _, let enterDate) :
209- let secondsElapsed = Int ( - enterDate. timeIntervalSinceNow)
210-
211- if let feedback = feedback [ . fitness( . bicepCurls) ] {
212- feedbackText = feedback. displayString
213- } else {
214- feedbackText = nil
215-
216- if case . fitness = sessionConfig. exercise. features. first, let result = features [ sessionConfig. exercise. features. first!] {
217- _ = counter. count ( result. value) { newState in
218- if !newState. isEntered {
219- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.1 ) {
220- withAnimation ( . easeInOut( duration: 0.1 ) ) {
221- countScale = 2.0
222- }
223- DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.4 ) {
224- withAnimation ( . easeInOut( duration: 0.2 ) ) {
225- countScale = 1.0
226- }
221+ case . introExercise( _) :
222+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.5 ) {
223+ state = . exercise( SessionData ( count: 0 , seconds: 0 ) , enterTime: Date ( ) )
224+ }
225+ case . exercise( _, let enterDate) :
226+ let secondsElapsed = Int ( - enterDate. timeIntervalSinceNow)
227+
228+ if let feedback = feedback [ . fitness( . bicepCurls) ] {
229+ feedbackText = feedback. displayString
230+ } else {
231+ feedbackText = nil
232+
233+ if case . fitness = sessionConfig. exercise. features. first, let result = features [ sessionConfig. exercise. features. first!] {
234+ _ = counter. count ( result. value) { newState in
235+ if !newState. isEntered {
236+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.1 ) {
237+ withAnimation ( . easeInOut( duration: 0.1 ) ) {
238+ countScale = 2.0
239+ }
240+ DispatchQueue . main. asyncAfter ( deadline: . now( ) + 0.4 ) {
241+ withAnimation ( . easeInOut( duration: 0.2 ) ) {
242+ countScale = 1.0
227243 }
228244 }
229245 }
230246 }
231247 }
232248 }
249+ }
233250
234- let newResults = SessionData ( count: counter. state. count, seconds: secondsElapsed)
235- state = . exercise( newResults, enterTime: enterDate) // refresh view for every updated second
236- var hasFinished = false
237- if sessionConfig. useReps {
238- hasFinished = counter. state. count >= sessionConfig. nReps
239- } else {
240- hasFinished = secondsElapsed >= sessionConfig. nSeconds + sessionConfig. nMinutes * 60
241- }
251+ let newResults = SessionData ( count: counter. state. count, seconds: secondsElapsed)
252+ state = . exercise( newResults, enterTime: enterDate) // refresh view for every updated second
253+ var hasFinished = false
254+ if sessionConfig. useReps {
255+ hasFinished = counter. state. count >= sessionConfig. nReps
256+ } else {
257+ hasFinished = secondsElapsed >= sessionConfig. nSeconds + sessionConfig. nMinutes * 60
258+ }
242259
243- if hasFinished {
244- state = . results( newResults)
245- }
246- default :
247- break
260+ if hasFinished {
261+ state = . results( newResults)
248262 }
249- } else {
250- state = . introBoundingBox
263+ default :
264+ break
251265 }
252- } )
253- }
254-
255- if case . results( let result) = state {
256- let sessionDataDump = SessionDataModel ( exercise: sessionConfig. exercise. name, count: result. count, seconds: result. seconds, date: Date ( ) )
257- appendToJson ( sessionData: sessionDataDump)
258- }
259- }
260- . onAppear ( ) {
261- UIApplication . shared. isIdleTimerDisabled = true
266+ } else if state != . startVolume && state != . instructions{
267+ state = . introBoundingBox
268+ }
269+ } )
262270 }
263271 . onDisappear {
264272 quickPose. stop ( )
0 commit comments