From 3e5c836eb12564a9212b3fc027ef3ee06b941186 Mon Sep 17 00:00:00 2001 From: Ben Altschuler Date: Thu, 23 Apr 2020 01:55:21 -0400 Subject: [PATCH 1/2] Added writing to test --- .../UserInterfaceState.xcuserstate | Bin 43435 -> 59141 bytes inductionApp.xcodeproj/project.pbxproj | 16 +- inductionApp/AnswerSheetRow.swift | 2 + inductionApp/CanvasRepresentable.swift | 41 ++ inductionApp/CanvasView.swift | 89 ++++ .../Draw Old App/CGDrawingEngine.swift | 370 ++++++++++++++++ .../Draw Old App/CGMathExtensions.swift | 112 +++++ inductionApp/Draw Old App/DrawOnView.swift | 407 ++++++++++++++++++ inductionApp/Draw Old App/DrawView.swift | 104 +++++ .../Draw Old App/StrokeCollection.swift | 301 +++++++++++++ .../StrokeGestureRecognizer.swift | 276 ++++++++++++ inductionApp/Draw/CanvasView.swift | 23 + inductionApp/Draw/DrawingPad.swift | 23 + inductionApp/Drawing Engine/DrawView.swift | 104 +++++ .../{ => Login and Signup}/ContentView.swift | 0 .../{ => Login and Signup}/LoginView.swift | 0 inductionApp/SceneDelegate.swift | 3 +- inductionApp/TestView.swift | 40 +- inductionApp/UserHomepageView.swift | 1 + 19 files changed, 1901 insertions(+), 11 deletions(-) create mode 100644 inductionApp/CanvasRepresentable.swift create mode 100644 inductionApp/CanvasView.swift create mode 100644 inductionApp/Draw Old App/CGDrawingEngine.swift create mode 100644 inductionApp/Draw Old App/CGMathExtensions.swift create mode 100644 inductionApp/Draw Old App/DrawOnView.swift create mode 100644 inductionApp/Draw Old App/DrawView.swift create mode 100644 inductionApp/Draw Old App/StrokeCollection.swift create mode 100644 inductionApp/Draw Old App/StrokeGestureRecognizer.swift create mode 100644 inductionApp/Draw/CanvasView.swift create mode 100644 inductionApp/Draw/DrawingPad.swift create mode 100644 inductionApp/Drawing Engine/DrawView.swift rename inductionApp/{ => Login and Signup}/ContentView.swift (100%) rename inductionApp/{ => Login and Signup}/LoginView.swift (100%) diff --git a/InductionApp.xcworkspace/xcuserdata/baltschuler.xcuserdatad/UserInterfaceState.xcuserstate b/InductionApp.xcworkspace/xcuserdata/baltschuler.xcuserdatad/UserInterfaceState.xcuserstate index 30e790df777d399c7e60d10ac3344ed0c21c9ed3..686004d929d33df580de0624e0267da3d7cbc0f9 100644 GIT binary patch delta 30929 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 delta 21573 zcmajH1z;4%`#-!h>w9q}K*$jzAudEnNQe_xTtWx|B1A7Pw-YBMPBjSk!B9TZUl8F>zFp)}R5EF_;S#tMeh1gWb?`Vm0Z+nH z@CSGr{s_;&v+x`|4==;t;5B#`-h=nyGx#@r4*!8K;7bycBxz3eA}vTc*_*T^tw?Lq zhO{O7kPf6f=|Osu{z`HH89)vqBgrT-nv5Z1$vAQdIh4#J^T`6TkSrnco4iBbCGU~<$p_?L5`?oa#C{`5dPf*wRi(us5u zolK9W$Iz+tSb7qjN!QW!bOYT;H_^@X6uO0;O1IK&^bGnNdN$oochH^mDta}&hE}en z*U{_gZS;2fEPakXPhX%f(wFGV^iT8^`YQc1eT}|D-=*)-f6#x@f6>qAH}qSk8)L?F zXL>L_8FQu=W5LLo-i#$<%Q!L4j0@w<_%Oaq2ouVLG2u)E6VD_tlbB2eunw#vJCF@xgV_)^lnrCU*$7rSh>c+r*km?^9n21AQ`xcXcs7Hb$WCH&*h035 zRj@7WRJN6EW2dpx*)P}`?3b*HoymU1e$CEezhP&yo$MlZF}s9a&2C}0vfJ40><)G( zdzd}K9%YZQ=h*Y?1@<@g8heYq%f4V=vai_J>>Cbn6i0K6k`p*R&WJPRtT{W*o^#@y zId{&3^X7awe{KL5#07KVTm%=*#c&B+BA3pM=Q6kn+(d2?m&s*u*_@ISImFd+bzD8y zz%_DB+*jP!+$`=JZZP?jiSx`-}UBCwRa^UWeD^^>}^WfH&lgcsbvPci>%k58jjS$NTb;d=wwe$MCUy z93Rh*=Ev}<{8&DXAIDGRC-Is5WWJOyu~L@O$}v{C@rbe~>@KALftn$N5wIIsQC#c!X4p(@I?4acqY6M-Ux4H-DGC6?y?@To-%V;FPVi*F6%9`lv&C8$ed+fGH+Rs zELavI3zdb*Vr6l%cv-S+s4QJJUX~%7Ae$&Fk||`x-DD-QQdzaER@N$OlTDL-BbzOo zBU>nISNRJ86#PP5rm7K|O<)9qQ3#GeupGe(1gj9N7Oho1Wq-tk5YFEbeuO_UfCwN0 ziGf5A5iIJ7`l5koC>n{zqKRnw9T7@|5#dAxF^GsHqKIg*n`kEni=)MSu@pfw1f50e zRJ}ebw5z8qCU0VBUV8E5j9i6ks@8SYQf&{_I4%2xvxGLGL+BDih!KQyCoz;5Mhq9t z#GYcWPGTf6iWn`*MH{h?XnmF#i;JZZkq~=` z-Bm(EKmH6ck(fke5?QLf`;AnqbU2GlB70a|a{RcgjC94PE#;|enXC5Jd+UE}U!hL5 zP?gntch?NBlqe^h+lex zk*!IsCmK@q4C9hxixp|(axy|Q(-oO{xoL&vokTs+NMw(SOa7SF1xho~BK0~&wEfs? zaz+lOA!ED-O;vTRY*1J6>BNlBi+5#wMa+^iz83AST@ z_qAV0EGC>65$!|=(Mc>49YrV6S#()MR1iyv?}%lXepk^=^u)Y!Pk)B~I%0#=`g+m5 zo!BUP;M7(FC*Q5a9>RGEv5nYH>>zd$yNKOlKhaC{7JWotvHuccFR_o~hf${P~fMa~7{BH_Gnp=fOtc$!fE z;kvS}62D1o^fU1b@v9gjhKUiK#5LkNaYKw0lf>blQ+`KHd1$I$xSH}&Mp{N6dElWs zGdxu!8Tl4(3qXL_=uc9d7Tx%qlH%6+_is;f1_vXF);7l(BX(@H(euJ&y~-~VXe5jf%YM~EY(74rXT z+)ZlSJy0Cg)q20rTK56{F;4+saZEe#6UTlwZ(yJr{nS*wp=$J(n_4-BfI*mJK`00V z;UGdx6UT|^;`l`%k~jpSL5!GzIV)Gp>sq%xR7TxgERr#kffR9qXmuV8C3L|sFdU2k zBf%&z8jJy{s&U==Xl03$#Yy5sG4o?@J<_d$_VszUKlw(C9&r|o$2Bv+1TYaylEhJd zUS_VMBrRtIelN<;NY6~m5wpb{Jus%y$)g_z+uPUM*}Wge7|SS-4RWxs7?PG#oY4WY z-wO+n3-VMw%q-YWkPixoZ2TTR(yZ}i$G5Vd=ZF7ARU1c1lkZ-g1~MB&LQv+ zLGU@D8`OlBr|KoCr#{xa+s9Q2wx}~JQ}v>yOiQeCGINJyW|TS?m1a)(RP2MD>XOwe zx$WSOVjt{Nrz&x(jmpJbOEu3_NAxE?WOlgfgjZcus)Dl@mIOo7=AK{3+l`!)mw{{PpVu|r%ut3rfHM3)BmK} zZ|VY5RX@sye3E)go!X{KwUQ|E`lP{o>YVACF}SE6+sHr3e5}r#@o8`U`sjaB;4gIn zRTl+~y*{P?g*x-ARK3Y+@**ufKB@UuojFT0ff_>aBu-RoSqPvu;k*JuNJ0wIkbx}Z zAP)s7gIeNjagI1woF~o~zZDmV3&nP^L+o4ub%;aKUuXagp%FBOCiuOZxCpOz>F;;q za&fh|QT!giZ^AMUMIBSk@!X_@ws--+KH_3YYh(KZ_9c{ypaXQ29&w4dRQ*`#S?O84 zUm(x}dJ-mB>nzhK^hW62At~;zY+vY4m~_DY&`(?;uEfg@2H<74N?bNFCo?xAGIxAN zSw_*A@fsV*LlKjaJ5e!7-*|CX{ZNP{SSJjF;V?p6Bd!(KbwVt?V6?a%k9`9k`_r8% zp0`KYJ=-^ZRD#t&OO58!BfA0Br#*ghfz+hf^XRPy;JLdssXs z{ve*l-+vUXH=wA&APUw=xYmmYyKrriaBT*u(jy+i0!MnR^wPUVJsp0jfo%qc?T9q$ znHaXC*h1~)YGw&%!}$^_bKqP!PdqN35Knf(Z`G)r`iP2ZgR5D0xD2k;(7yuHf9BJc zf{IMW!S!&L1i%Kk5q=Lh!Od_B24EZ94tKzv;yLlWctN}Oiz$_?>zuYctHZ-qWEhU06$3pT)_a~NBj*JmL4lT zSr-7;;VlgSH!%R$rD5O30NfClb@56UtHAwTRUW`65-1PhBluXnCH^kn?u1X(pxj|T zqT=?CnpNzrc0bXuN>}w)@T~@@HyEh#7YN#1WeV#*Q^H_PbR6+Arr;T2r!>? zA;B|=97d)S&dbT+yCue~(ZySs@`*iGX1jM0lsMh*XlI^dMk_fU)Ki zvy`5mSr<%AWQzu-DdGwQOeG>(MN5Q@zhx>5haOg4<-R1p(hxgSBDTk8bx%M8HcT+*`6zx+muq7p7-~N#kqq#PhLR4 z1p(K8agbUqka+dtjh(AL`>LynU&!kcJN-)jMqWd}9RUvnJUd}DrtB6L)0l{USXQr@ zoZ|FeR_np8st?I08nhovX#0HDyTn`*$-gNeG1qhQAMyqHl6*zJCf|^6$#)cH+WrXm zA>fa|00aUM2t;5Y0=TG56EO{B6kkYq}bjFhO$ksulMZ%DM}byX^$iZKIGg;WuxKwu04sfGxQ?Ig1# z2`-6%E^VLGwM?lR3TaRjB^1Yf+K-xVs3z)5%r{gsHHB)Src$j`8#RrZPJKbmKwvxq z83;^3U?KvO5XeLz3xR9|auArjoKk%N@SFGW%|jsfBYXv2*eTxgO#-z{0&6(}d0k+w zlE7Lm{Uz~g&e#0IvfD~etBdIGsVy3$HcLnqN)&99kSf9gDYn9_C$*c}ry+5#M5459 z`B*^BCr7Ar5}zEUj#0;{6VyrS6y}rD)Q{8|>MR1K2$UgEjz9$hSX@>~d{T`-4FXC8 z#1+)}4`lvCU3pLFuLy6E-kXUC)FIH=MeEf6i%%X%q(4NUwu|&9m~>J}jg=m?AjW)x zh4SzZ`zq==^-@Fm3yJc2i7@O9K%n8@JOa+Vhl>U@jd_HIG)YqkG$GK8z?4p1n$hq` z%Lg7w>HA*Fbk)?Q4b(W&`Vx+tF*-&<>|0C%9WNoa;NOTDn0Hl5p@&I` z4W@_CLlJ04paZX}&i~h{qSNT{8mQ7Gs1|?Pk6JX*Sv1~qub{K(9C|XHOXtz~kfRIf zB3eNgBd`>K?+{pq0A31soGT@kTZO=C1lAz1b_HGf0k|rD{K z-s`@d|HE_}rbP_`iS#rHL_80x^W1DLrZgH)uXFh;$bkbLjaR2 zK!R`+UT%4l&1~sK^mh_mi|Hjareg~NTM^jSNiWmz*Y=P6^ee5jcRrK?HE( zVFZpKaCA9+7+a zw7P&`^ce&C6n&F8#9%A&*N;%#K;Uj7F*Le*L*7HfxH2AUXc%`1nwyw5rk@1OEzCm;0?cfg{!9SoA;ypKXRt|f z8-Y6r-0frnH9U0hBM<$LjA8~c(Q4S3C<(O(pS6rxCzDBJMq$=rl9*&Bg&E8YVTLlp znBmL_W+VcS5O|Eh69k?j@CO2aBJdXi&k*<50h0N42YK7dN5hZM1 zNfgvc*u2KrWQSV!WLlUu4T-IoL`ZyAKvHLnidi5r#!Tib=4)ma^9?hbwqWKm^O*U} zw+P~(If)>JAdMh{AdA3E1UUqG1O)_TE0~2J=v>S!VbbXvn9}74dr7+V8Pr1%OF&HQ z2}~?B{hTv4v_(Y$o=X8yvSV&)0+l=*}C z6G3wXdm(6npnMVYjQN{+&isR5Zv-(xRtP#_efARF_h13WipA4nEn1ypX~|Vg=&}sU zvK-3~FH*;3a6AXWJ_y<(XoH~Rhi-?|b??P$vv^nC!RoLWQac3gJ6L_z0KvWp;&6;= zTAe$K-NCFG+gJho5~G=F0Ld61smPeD z>aE*k!$#uZT?HFGCa-XOMq$QyO(d>JVjDlTKaGuL<5Z7yyK!v1CZvJCm};rlBbx}Q zg<^s#MbA~q4q=CW4#o%;%Zhe(B!WTh>}Ujor2*(>l;x*kmz#P3k)cJJcGB4Nc6JtJrF`24Sln#vmArU>t(+2qqwyh+qc{@7QJR za&`r~62VjiGZ4%~FbBbWF}|zuH7t$`cCu^Pb?kZs$0C@9;J8k@hW(!1B$+)h9l`NZ zahFS1JZydAUIJyAgh5IMNA~hhb9zaJTgkVdOgnm@EFkf}8A%?(ECjPf>!kkjQP?S4tPaBl<>#xbhvPMUk-hRC^~o&uXZ9DoJ40|Xg1Mq~ zIw7}#g!~M99oM=(x>bRUDMOme4K#LQwP-VAxfbOaVEm1~u^h8c%MYEC4$>DLyPQiEvvsJHKtmNH9 z{;aWyj;p6nr<=QnD2OsqtFxb%x2TO-MOQRcwNLG#3Y+TZq!*GBo|mgo*Bo4wHZkLj ziB7=4_COrDH<4Nh4m0lv36-Mw9pMqGOH)IrxcG!bDM&v0v-opILeht5`I)%*l)?WS zl1v$r)D`2#`NM|)AEE7G2}wA6wt`32F1sxfUQWKYq2LuuP&{kXXJ;u1W}ki49{!sL9+ z^Btv0pMA%Dl~>?#sY`T}OMO>Wr|KEtXuLvEm^rQ(hs~4mOYX#unk1F`0!O8E$ghrQ zB~-*htfLnZONh0^W@0x!&UX?YUSp$>>VN-Pnh(rYn9z&pW^5~UfU1YhwOUx zG5bWl!V3^A#ETrkA_Nsn=o%2s{>47S5!h(F;6*GmixDivfmpnNQuQn&RW}ySRkCmK z3VO#8(h5>!PR_6j+GkfX%S8SQf+gxPaoGQ?={}=_gG6?!o?c4c__T6)nnK>Mzq`9U zAyzL-E#Wy9u2|jR-a)*n(gyg3~1H@Wb%|u8&3uEB_~k zX8f^)qo&0BV4^w%_erjsCKpE$rC{cVh~FnAyfh_n81X}#?UP(TO>WaCkk?B%sGD#ihu1%%yOH zxgp$8ZWuQl!5IjCiJ%HWZ0ml7;MWMwTFQ;&MiGa&F}nk695b z#$=WLqLqPMGdGQJUd&D5TDYlPE7yi#CxVL*T#Von1eY%6rgL9#Gq^7~OyYM4?nUq{ zf`20Tm(-42mA}|jWw;o{a&x&(37vV|eC}Ir0k@EA=Quo2&D1?FXOdDY*ZaZ@EC%}ajhfY z4fn#QYVc$Qrzz4Wg=1J#GRhQ*#TkX=DHHIw3%oIJ!khBlcr!wm@6PwYs%sqIi?>h> zU)Ik8;*Hk{P9zXKjo_JcxI}N>lDFcmiE&^YZ>w6htj}QhgOq>r49>LU?fJgLrBf_w zm5JaftUJrq5}9}8o$wTW&=VS^+KH2$)#vOq-!(;Dc{f$!@@{ko?~XS#iOapEjSla{ zd#k1|x1@L!E2LG-Jeiw!@5$2Rb9Sg_#MZXoy+x8}TZk3OFcs1ANSpMl^t1h3=0$h(!c zz4O`Y47I8&4ONS&xU zxY}7guXFghs+X%BDSjUApzj)IJ|oS?GsAO2`Z!hE8Y9-rw;CH{YFVemOoFX%OheZk zwezb9=at+%zLQ_XFXor0SD2t@2#^SS4R; z5^41DM4Y>~o1@&<%k%w#IR5BI6M;X$rejSJ+BNnc_#d$l;ZGxyYUj@&634hzlh@hz z`u;rXP$S{yG1OFzMv~;a~7C z5vhwvJw)nv@~<(czeS{hq{IyI#&Y@kM5Vw8!he(%WQ2{NjYwmutckd6G!{N#<1-a` zh0-qBYT!&!ZA!kUPcRTn{;LsF+=v+>yGf0hNeV~Onu8UYIhl&gj3TKeRmBE9rO-=o zB%D_X7J^*pEm#Uxg0)~H*b03FJHcM)D>xvs2O@hS(j1Y!5NUx(IU;)_(h`wYh_pte z4I*t3*=LpDBshaq!BudRR<+=X-@OEH!3SFqq#YvNFwc_ii1bh|bh4lH)eFx?2qN<& zlVXGZ#v))OL<-Rub0G?m_U%FpBKvAER|*M$?GO@$Bt+s7Ie&m#7%U9`FX~6&A&f$# zqeQ)vxNP_b2(1RzEc^_%DlO^EbGq#q&&ATj`vfgePe&;p{dK*5JIa3 z8J9P{{Jq=|rmN)!>93X>!@3mh`&{lWG?4_+F7bO@uPgDOM$~K;Gvhv{3iC7#4OBPu znOX7^7%sF69YUwDh|m=lgJHrFVJSEvETc96tm=g2WGpd`j1^X@Zf*Xedz~JK$Y7B$ zeE0s32E*0D8r9k@-PulIt*{OYv$b3L^%pi^i7#wKWa!69-z;p$b0%yNwhG%28HUJk zL`HN9JD`cM3z35`A*AFYve+7GEF6@oV;+pcr+tN^cx=an*wQcJfCB&_!AaXDwF-uOCAg)DZ&ZiUjv+#>F zfglx;m_=hX6DVe?w>iQMLfI+Y6mAK>BQg$=@rX?LFl+yR=u6-SXf%fKRD*uvztKM{ zxkmA>_=NDB7$?>tGFc=vkiHaNsdVa1=nmmE1{A-khHW=j3h!i6j6_DrfDFn=86~4- zjEt3WGF~PiatI=aB61iaha++XB1a+;ucgt59D_(q;Mf(i4?z-{zRW;##zba<$TYQB z#mCeTnU2Vet|KO7mSkOiJYtfenijQFIuaN!v&Jr785SYq-n+wP_;}L)ameKT0+PAN z+|{mdnHzS6ljE_An!F&z0q}7W$wh4|^O5;uZ<5Sc)?bE~$pl1BMC7DS*#LEngp^K> zy+7IaKQR(nxGYlb{FV*E&TlgNvzGIA1%+TEN|qo?Ow}{`AWjBh-(_B5x#V;0kR?g` z(=_NZ*QY=ms241=7H$O1$bBC=?aY>X@w zM9b0;sX$~oUeqKKt%t>AOi)|Ns+!&1qDI7|mFE>JV)Mpl3{T3OI7yw;h0`S2WU1*) zS(YqYmV?M*M3x}36p>|k2ZJrS3asmbun}je)_BKv*9?=D$tpf*JXs|oE8g$iWi?p( z%9Jt@;d4=B&4-03tCKbThwzj&6E?CIL{>?{vl>s!4|~j%vgtC_f0dny%YKbWrBqhL zlKKN3Rs-wHK9$t6xiWk%v0XL~k+tpWbJUor^zl}uC@&`?JTpg;Q78eS_72KA*qibC zl=-;)96Uj;uIB&?Ns4AzTh+k5{VjAkUXW>N>*(s~8yFfHo0xVp>)xYh59~SYZE0m~ zqnf+7uj=yNg`pfTLLP6WDUsFF6=_jlkI=8KTcc;t$Iia5gQJtP_ke)FFzjJWNFHpR zTbz@Vs%PQtC3kc8mb>8%G2RdNb5-rxXTpEj+^8Px>&Ljby19FLsp5t1`t2Sf*WORm z67|md__|@Ar7CHEHZ?FPI0SnLtqp=hKllXChDW#!ii}b{-yi;eZ@p9XOg@@YZocmL z=)0Snw>LJX1_s5&V~^rcykp5L)v!%RTqsUXl>B)gufcRAhK_M9^Pb>4uHU${j5JRV zSArw-iSC3oVUO?gdgEJ1SxTae5b@>Q1^8;pYV5l^hCOz7iDy6tOn?P&#`kI6fhXvP zJ#{|77kldlfItugLhwC-2oMEQK?c4(I}OYS>%j(a9Gn3^VQ<|7@CaXx{R8}kug1QC z-Jlb6!Pj3s;0QPo=D<>Xrxl-C#V1lb;4Y~A4PJ*g;qN3MyW=aV&ZHaZMTU^!YHlGu zc-&Gn-@87s_=b6y7&F3>aKx9B`{O%SiNp|m-)Rh+#pdI%K?A-OI19Vwzs0Wj#q3h- zYF~-H;P=?)_-daPr-QHe8Q=?kCY-Vxc2o7_TsSv;zt4;F#rOLH@$J4aZV(s6rExXb z714n&2H8s*I=IboiS*f#HXRXe9osBx1boT2U)H$qkROh(PNu3{be$=_Db4M5I+Uths4$)23 zZP8Wfey#hB?i}64y6bf}=x)^Aq`O;pkM2p`bGjFFFX{fI)V-#AL-&^MZQZ-Nf9w9E z`%?F{UYK5zUW(ojyEF?Rs{hu2Hjo)u8n_sE8Tc6VH}E$IF$gn=Fo-mW zHpnrUZm`zih{2Br%5w%63@#a5Gq`SW)8MJWKL#%iUK_kMqzzd^-cV+!ZD?cYZ0Kg_ zVc5?wz%bA-$S}?@(J}MQeJjgiFIN!L`_-o^p#_NoC8SgdTZ+yu3i19Jwi^i9YuNePq zVq@ZN;%(w<;%73@B-kX>B-~`2Nxn(BNtH>BiD=Shvesn1$wre+CRL7`x{dCZ+AXbHdbds8_IEqj?QplF zW@cu#W_D(M%^c0j%^J;`&05S_&90i=HM?*2(Cl&dpzd+q6S^mLPwC##eT}mFy6zjg zf8T@eVbY^p4@q(t_o(er-=ncdbB{|sZuPj`<8F`pJ$-wI_Z-wSs%K2kc|Di)T+wq? z&o$;2=8oph=C0=M=1t}^%~zPOGGAl9&U}OU_vV|;x0-J^-)X+ve6RT>^PkMGn*U<{ zoB4I~o94fp-!Z>u{=oc^`IBDSy&QWb^ime~`nuPaUN|Gu8>#DYvfJxDe|x7bLI2p3*?LBOXT0lm&?~G z<-6r4Z;E9Lsr@-&!uT?66#9xx{jV zTJEY3GZs~1+Utln6?vj*0^tOr=9S=U)FwLWV7*v8Pt-6qjygw1H1RGT!L zESn;mVx>)~O}R~_O|^~ErqyPKjmqXLn>jY~Y`(QwXtTy<8Kh z+b7zOu`jYOu`gHJSJ^A=Ywa8Co9(CCPqUw4zr=o-{R;b4_G|3d*>AMpY`@ihyZtWv zJ@!A_zwRsV8{Bt7-{!un`=06h)Ira|%%P`)g+p(Lz78G^UJkwv{tkf-K@K4fVGcTuoR zmct!~`wovBo;v*H@Z900qrRh&qlsfT$L@~ij&esUM;k{w$G(nEjxLTdj@gdW9oIRY zcYN+-=`_eG$7!n5a;FVWo1C^fZFf50bkgax(^;nrPM4joIQ{JOtJ4Fghfa^2o;ba6 zhR&2T>#XIh>ulg`U_%iwDVc#OU_rEe{ufJ`LXkB7kw8a7gLw+F6J(B7b_QA7kd{+7Z;aMmk5_g zmuQz*mw1;XmlT&FF2h_#x{P+IaGCA0)8$uJ($&T_+BMI0n(HdpO|DyAl{;K_xgK^s z?Rv)boa+VGORhh;-gbT9`pEUE>))>bxW00I(bBf%mD?J(b#5EnHo0wa zJMZ?FdpGy~?ql3V_a*Mf-0ymDN)LSxBM(y#GY?A-2M=ctHxEw_Zx3G&KaT+(Q6BLg zi5@8)!#zfNjPV%jk>^q9q4a39kJ%n`J?48X@Yv{a*5j$iTTjxH_T)UZ zJ#{?|JdHf9JY7A*JO_D3d&YStdZu^|^&H_j+H|}R zTRo?H&hT9AdD8P~Kf`|B{f75b_Ur7ouiv$P_xe5T_oUw+{a$%dUaXhkrR}BbrSE0v zCHJ!N>f_bd%f-vh%hSutE6i(<*I=&^UZcEHy~cY@@XGYc_Nwq|^7_`R-D{E8Qm^G+ ztGw2Ft@rxgYm3)*uU%f}y)JqEpD%n=K41AP z@>%M$(Pz8QE}y+V2YinEoboyCbH?YK&s|^6*Vfm?*WWkDH^evGH`+JWH^Dc_H_ca> z>)YVl>^s$Wn(qwXnZC1p=lIU|UFh5CyTo^=?;hWMz6X2{`5y5-?t9AjwC@?;bG{dS zFZ=%8-?+bT|B?Nb{oDJW=>LbGiC;gz6u;4aWBtG=O8qMQs{KU2I==?LCci0u zU;EAVoA0;KZ?WG}zvX@_{kHq<@;mN##_zn}C8ggLzw3Ut{BHZ*^}Fv+_*?i7@Q?D3 z_fPas@gMF#(tnKqSpPi#Du0##*Z#Bp=lL)2@9z*Gl7o>>I`&O4on$XHgL|sJp*qBkwKP0 z4nfXAZb2SF{y||ugMy-i;(`)`l7j{Z4GkI}lo^yAlp9nOR2)PY9dfX zeMD14OGInL^oSV|t0PWEJRM{>$a~Q6LCQg$gZ2%&Ht617r$<*ue;GYD`rGLC=+5Yk(YvDeMjwbi9DOXt zAf`u*U5rzVYm7&XPmEtoK#X!=OhimvOnOXyOnJ=gn1wNWVlKqoh`AYaE9Q30Uop>P zUdFtMC1PQ$ZmfQ+VXR5)_}GHj+SrEJ=GdvR(_&}D&WxQEJ12HU?CRKcu^VGI$8L+= z8M`NTf9%27Ut{mbzKt`B>l4>ME+%e7TzXt#oFc9?PK=usw=iyV+@83-ar>2VH{u?| zy@-1m_bTp9yb!M)uNQ9^ZxY`vzHhuoyjQ$$ynlQ^d_;Uq{K)u8@tN^i@j3D3@m2B4 z_}ch}_@?-;;ydEk#P5qg7=I-Gc>JmOALGx(UyT1L{^$57@qflYi+>*fGX6~hkw7Le z30#7ZV3c5y;FA!YFfO4qp*3Mqf^uiVv4pdUe4<{Weqzr=`$X47k3_FT-^785A&KFM zk%`fXHHotm*C%dIJd}7e@kHVeiDweeCtggvnRq|(QR36YzYl7o}Ok_RP6C&wnIB#%zc$UKsHJo8-U^~}4OPcq+R30c}% zdRc~9CRt`#J+thy9J8FW+_F5gys~_<`e&tP)n~2Fx|DS->u%N`SE> zdsgSkA?q%Q;taewiFM zdF15LlT#;;o4k4Qfysv^ADMhS*DTjI*Dkkju2XJxZcA=!?zG$)xi@p4=Kh)cEcc(h z7-in@yh(Z4dAWH7d5XN!yo$W)yw<$wd0*zu%$t=rCvRTfw|NWm*5p0OH_H#rpO9ah zKRt1p|GNGPT|tR zO@&(vcNFd}+*f$8@JQkD!k-F%F8sCddg0B&-wW>+J}7)t_@=05kw?+cqS~UFMf;0> zD!N^CujpaXlcGP1o+(&`k-}8bU16?}D=ZZ*3V%hAB2*Efh*88Vk`yV5!HRK;JcUA0 zs;E#Zl!{tKgQ7_>Td_p3OtDh2MzLP8QL$HXKyg@cOmR~2gW{^zhNV4AZAu4}2A7T~ zEi5f96-(Pomy{kZJy&|I^k(Vp(tD*3OP`efSw@t>GP;Z{1E56Wt+?Pl^rZQQg*!TRN0SZSId4ayH<9i?Dw*}W%tX;a)L<(td5 zmG3A&TK;qS@8x&PACy0;AS+BNY$|*zQYwa246hhfkyT&%cUakWyf(yY>_azN#%%FN1~%G}C=N@Yc5Q)NqKYvuIHd6f$)+bb7U z9xm}>T%W6sz0lqRXwkIS@ovsUA1YoLv>j7*y_saS=F1W4_05UepCZ%%xWxa ztZHm(oNHWa+?6$fHNiDuHG^uRYhr5(YAR|fYpQEnYNpqGS@TuRoSJzx-_|Uw*;4aU z&Fz|dH4kf^)cjHNm(oCKp|n)mDD9LEN+)HIGD?}KOi>P1j!=$LW-9ZQMamLoxl&Zt zDI1i{$`<7}%Eihp%Hzs&%4^Cy%KOSk%BRYIl&_R;L?t0Ye7w_4w8cNt}ojI@N>g&4L2HYHT===qTzMJyGGc^H)=KNH0n1RHd-|HZ5-H`)Ht$nOk-MOdSgyw zQ)5eGo3e3rZfI_9p4vREc}DZh=2^{in&&q!Z0>Ac(yZLsyr+3z^MU3=%}1J#H=k-g z-F&wBeDkH|pPHXfF`3eT%BU&gl#VGUr~KJs+TztRxMfUBTFdyB2`zaoWi6F0H7%&6 zzNN86)$&ctoR;}59W9GmmbNTw+0t^b3^E7jVw)xOoewO^}Gt6ytC zYeZ{QYiw(LYf@`U>*&_vR){ +// _canvasToDraw = canvasToDraw +// } +// +//// @objc func drawingImageChanged(_ sender: CanvasView) { +//// self.drawingImage = sender.drawingImage +//// } +// } + + func makeUIView(context: Context) -> PKCanvasView { + let c = PKCanvasView() + c.isOpaque = false + c.allowsFingerDrawing = true + return c + } + + func updateUIView(_ uiView: PKCanvasView, context: Context) { + + } + + +} diff --git a/inductionApp/CanvasView.swift b/inductionApp/CanvasView.swift new file mode 100644 index 00000000..57264128 --- /dev/null +++ b/inductionApp/CanvasView.swift @@ -0,0 +1,89 @@ +// +// CanvasView.swift +// InductionApp +// +// Created by Ben Altschuler on 4/22/20. +// Copyright © 2020 Josh Breite. All rights reserved. +// + +import Foundation +import UIKit +import PencilKit + +class CanvasView: UIControl, PKCanvasViewDelegate { + var drawingImage: UIImage? + private let defaultLineWidth: CGFloat = 0.5 + private let minLineWidth: CGFloat = 0.4 + private let forceSensitivity: CGFloat = 1 + + var canvasViewPK: PKCanvasView + + init(drawingImage: UIImage?) { + self.drawingImage = drawingImage + self.canvasViewPK = PKCanvasView() + + super.init(frame: .zero) + self.addSubview(self.canvasViewPK) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func didMoveToWindow() { + canvasViewPK.delegate = self + print("BEN") + canvasViewPK.drawing = PKDrawing() + canvasViewPK.allowsFingerDrawing = false + } + + // MARK: Canvas View Delegate + + /// Delegate method: Note that the drawing has changed. + func canvasViewDrawingDidChange(_ canvasView: PKCanvasView) { + print("Drawing Canvas View Drawing Did Change") + + } + + override func draw(_ rect: CGRect) { + drawingImage?.draw(in: rect) + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + sendActions(for: .valueChanged) + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + drawingImage = UIGraphicsImageRenderer(size: bounds.size).image { context in + UIColor.white.setFill() + context.fill(bounds) + drawingImage?.draw(in: bounds) + var touches = [UITouch]() + if let coalescedTouches = event?.coalescedTouches(for: touch){ + touches = coalescedTouches + }else{ + touches.append(touch) + } + drawStroke(context: context.cgContext, touch: touch) + setNeedsDisplay() + } + } + + private func drawStroke(context: CGContext, touch: UITouch) { + let previousLocation = touch.previousLocation(in: self) + let location = touch.location(in: self) + + var lineWidth: CGFloat = defaultLineWidth + if touch.force > 0{ + lineWidth = touch.force * forceSensitivity + } + context.setLineWidth(lineWidth) + context.setLineCap(.round) + + + context.move(to: previousLocation) + context.addLine(to: location) + context.strokePath() + } +} diff --git a/inductionApp/Draw Old App/CGDrawingEngine.swift b/inductionApp/Draw Old App/CGDrawingEngine.swift new file mode 100644 index 00000000..031179c2 --- /dev/null +++ b/inductionApp/Draw Old App/CGDrawingEngine.swift @@ -0,0 +1,370 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + The view that is responsible for the drawing. StrokeCGView can draw a StrokeCollection as .calligraphy, .ink or .debug. + */ + +import UIKit + +enum StrokeViewDisplayOptions: CaseIterable, CustomStringConvertible { + case ink + + var description: String { + switch self { + case .ink: return "Ink" + } + } +} + +class StrokeCGView: UIView { + + var displayOptions = StrokeViewDisplayOptions.ink { + didSet { + if strokeCollection != nil { + setNeedsDisplay() + } + } + } + + var strokeCollection: StrokeCollection? { + didSet { + if oldValue !== strokeCollection { + setNeedsDisplay() + } + if let lastStroke = strokeCollection?.strokes.last { + setNeedsDisplay(for: lastStroke) + } + strokeToDraw = strokeCollection?.activeStroke + } + } + + var strokeToDraw: Stroke? { + didSet { + if oldValue !== strokeToDraw && oldValue != nil { + setNeedsDisplay() + } else { + if let stroke = strokeToDraw { + setNeedsDisplay(for: stroke) + } + } + } + } + + let strokeColor = UIColor.black + + // Hold samples when attempting to draw lines that are too short. + private var heldFromSample: StrokeSample? + private var heldFromSampleUnitVector: CGVector? + + private var lockedAzimuthUnitVector: CGVector? + private let azimuthLockAltitudeThreshold = CGFloat.pi / 2.0 * 0.80 // locking azimuth at 80% altitude + + // MARK: - Dirty rect calculation and handling. + var dirtyRectViews: [UIView]! + var lastEstimatedSample: (Int, StrokeSample)? + + func dirtyRects(for stroke: Stroke) -> [CGRect] { + var result = [CGRect]() + for range in stroke.updatedRanges() { + var lowerBound = range.lowerBound + if lowerBound > 0 { lowerBound -= 1 } + + if let (index, _) = lastEstimatedSample { + if index < lowerBound { + lowerBound = index + } + } + + let samples = stroke.samples + var upperBound = range.upperBound + if upperBound < samples.count { upperBound += 1 } + let dirtyRect = dirtyRectForSampleStride(stroke.samples[lowerBound..) -> CGRect { + var first = true + var frame = CGRect.zero + for sample in sampleStride { + let sampleFrame = CGRect(origin: sample.location, size: .zero) + if first { + first = false + frame = sampleFrame + } else { + frame = frame.union(sampleFrame) + } + } + let maxStrokeWidth = CGFloat(20.0) + return frame.insetBy(dx: -1 * maxStrokeWidth, dy: -1 * maxStrokeWidth) + } + + //Goes through each rect + func updateDirtyRects(for stroke: Stroke) { + let updateRanges = stroke.updatedRanges() + for (index, dirtyRectView) in dirtyRectViews.enumerated() { + if index < updateRanges.count { + dirtyRectView.alpha = 1.0 + dirtyRectView.frame = dirtyRectForSampleStride(stroke.samples[updateRanges[index]]) + } else { + dirtyRectView.alpha = 0.0 + } + } + } + + func setNeedsDisplay(for stroke: Stroke) { + for dirtyRect in dirtyRects(for: stroke) { + setNeedsDisplay(dirtyRect) + } + } + + // MARK: - Inits + + override init(frame: CGRect) { + super.init(frame: frame) + + layer.drawsAsynchronously = true + + let dirtyRectView = { () -> UIView in + let view = UIView(frame: CGRect(x: -10, y: -10, width: 0, height: 0)) + view.layer.borderColor = UIColor.red.cgColor + view.layer.borderWidth = 0.5 + view.isUserInteractionEnabled = false + view.isHidden = true + self.addSubview(view) + return view + } + dirtyRectViews = [dirtyRectView(), dirtyRectView()] + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + +} + +// MARK: - Drawing methods. + +extension StrokeCGView { + + override func draw(_ rect: CGRect) { +// UIColor.white.set() +// UIRectFill(rect) + print("OVERRRIDE DRAW") + // Optimization opportunity: Draw the existing collection in a different view, + // and only draw each time we add a stroke. + if let strokeCollection = strokeCollection { + for stroke in strokeCollection.strokes { + draw(stroke: stroke, in: rect) + } + } + + if let stroke = strokeToDraw { + draw(stroke: stroke, in: rect) + } + } + +} + +private extension StrokeCGView { + + /** + Note: this is not a particularily efficient way to draw a great stroke path + with CoreGraphics. It is just a way to produce an interesting looking result. + For a real world example you would reuse and cache CGPaths and draw longer + paths instead of an awful lot of tiny ones, etc. You would also respect the + draw rect to cull your draw requests. And you would use bezier paths to + interpolate between the points to get a smooother curve. + */ + func draw(stroke: Stroke, in rect: CGRect) { + print("OVERRRIDE DRAW") + + stroke.clearUpdateInfo() + + guard stroke.samples.isEmpty == false, + let context = UIGraphicsGetCurrentContext() + else { return } + + prepareToDraw() + lineSettings(in: context) + + if stroke.samples.count == 1 { + // Construct a fake segment to draw for a stroke that is only one point. + let sample = stroke.samples.first! + let tempSampleFrom = StrokeSample( + timestamp: sample.timestamp, + location: sample.location + CGVector(dx: -0.5, dy: 0.0), + coalesced: false, + predicted: false, + force: sample.force, + azimuth: sample.azimuth, + altitude: sample.altitude, + estimatedProperties: sample.estimatedProperties, + estimatedPropertiesExpectingUpdates: []) + + let tempSampleTo = StrokeSample( + timestamp: sample.timestamp, + location: sample.location + CGVector(dx: 0.5, dy: 0.0), + coalesced: false, + predicted: false, + force: sample.force, + azimuth: sample.azimuth, + altitude: sample.altitude, + estimatedProperties: + sample.estimatedProperties, + estimatedPropertiesExpectingUpdates: []) + + let segment = StrokeSegment(sample: tempSampleFrom) + segment.advanceWithSample(incomingSample: tempSampleTo) + segment.advanceWithSample(incomingSample: nil) + + draw(segment: segment, in: context) + } else { + for segment in stroke { + draw(segment: segment, in: context) + } + } + + } + + func draw(segment: StrokeSegment, in context: CGContext) { + print("OVERRRIDE DRAW") + + + guard let toSample = segment.toSample else { return } + + let fromSample: StrokeSample = heldFromSample ?? segment.fromSample + + // Skip line segments that are too short. + if (fromSample.location - toSample.location).quadrance < 0.003 { + if heldFromSample == nil { + heldFromSample = fromSample + heldFromSampleUnitVector = segment.fromSampleUnitNormal + } + return + } + + fillColor(in: context, toSample: toSample, fromSample: fromSample) + draw(segment: segment, in: context, toSample: toSample, fromSample: fromSample) + + if heldFromSample != nil { + heldFromSample = nil + heldFromSampleUnitVector = nil + } + } + + func draw(segment: StrokeSegment, + in context: CGContext, + toSample: StrokeSample, + fromSample: StrokeSample) { + debugPrint("OVERRRIDE DRAW") + + + let forceAccessBlock = self.forceAccessBlock() + + + let unitVector = heldFromSampleUnitVector != nil ? heldFromSampleUnitVector! : segment.fromSampleUnitNormal + let fromUnitVector = unitVector * forceAccessBlock(fromSample) + let toUnitVector = segment.toSampleUnitNormal * forceAccessBlock(toSample) + + let isForceEstimated = fromSample.estimatedProperties.contains(.force) || toSample.estimatedProperties.contains(.force) + if isForceEstimated { + if lastEstimatedSample == nil { + lastEstimatedSample = (segment.fromSampleIndex + 1, toSample) + } + forceEstimatedLineSettings(in: context) + } else { + lineSettings(in: context) + } + + context.beginPath() + context.addLines(between: [ + fromSample.location + fromUnitVector, + toSample.location + toUnitVector, + toSample.location - toUnitVector, + fromSample.location - fromUnitVector + ]) + context.closePath() + context.drawPath(using: .fillStroke) + + + + } + + + + func prepareToDraw() { + lastEstimatedSample = nil + heldFromSample = nil + heldFromSampleUnitVector = nil + lockedAzimuthUnitVector = nil + } + + func lineSettings(in context: CGContext) { + + + context.setLineWidth(0.25) + context.setStrokeColor(strokeColor.cgColor) + + + } + + func forceEstimatedLineSettings(in context: CGContext) { + lineSettings(in: context) + + } + + func azimuthSettings(in context: CGContext) { + context.setLineWidth(1.5) + context.setStrokeColor(#colorLiteral(red: 0, green: 0.7445889711, blue: 1, alpha: 1).cgColor) + } + + func altitudeSettings(in context: CGContext) { + context.setLineWidth(0.5) + context.setStrokeColor(strokeColor.cgColor) + } + + func forceAccessBlock() -> (_ sample: StrokeSample) -> CGFloat { + + var forceMultiplier = CGFloat(2.0) + var forceOffset = CGFloat(0.1) + var forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return sample.forceWithDefault + } + + if displayOptions == .ink { + forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return sample.perpendicularForce + } + } + + let previousGetter = forceAccessBlock + forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return previousGetter(sample) * forceMultiplier + forceOffset + } + + return forceAccessBlock + } + + func fillColor(in context: CGContext, toSample: StrokeSample, fromSample: StrokeSample) { + let fillColorRegular = UIColor.black.cgColor + let fillColorCoalesced = UIColor.lightGray.cgColor + let fillColorPredicted = UIColor.red.cgColor + context.setFillColor(fillColorRegular) + if toSample.predicted { + context.setFillColor(fillColorRegular) + } + } +} + + diff --git a/inductionApp/Draw Old App/CGMathExtensions.swift b/inductionApp/Draw Old App/CGMathExtensions.swift new file mode 100644 index 00000000..cb1f907a --- /dev/null +++ b/inductionApp/Draw Old App/CGMathExtensions.swift @@ -0,0 +1,112 @@ +/* +See LICENSE folder for this sample’s licensing information. + +Abstract: +Math extensions to Core Graphics structs. +*/ + +import Foundation +import CoreGraphics + +// MARK: - CGRect and Size + +extension CGRect { + var center: CGPoint { + get { + return origin + CGVector(dx: width, dy: height) / 2.0 + } + set { + origin = center - CGVector(dx: width, dy: height) / 2 + } + } +} + +func +(left: CGSize, right: CGFloat) -> CGSize { + return CGSize(width: left.width + right, height: left.height + right) +} + +func -(left: CGSize, right: CGFloat) -> CGSize { + return left + (-1.0 * right) +} + +// MARK: - CGPoint and CGVector math + +func -(left: CGPoint, right: CGPoint) -> CGVector { + return CGVector(dx: left.x - right.x, dy: left.y - right.y) +} + +func /(left: CGVector, right: CGFloat) -> CGVector { + return CGVector(dx: left.dx / right, dy: left.dy / right) +} + +func *(left: CGVector, right: CGFloat) -> CGVector { + return CGVector(dx: left.dx * right, dy: left.dy * right) +} + +func +(left: CGPoint, right: CGVector) -> CGPoint { + return CGPoint(x: left.x + right.dx, y: left.y + right.dy) +} + +func +(left: CGVector, right: CGVector) -> CGVector { + return CGVector(dx: left.dx + right.dx, dy: left.dy + right.dy) +} + +func +(left: CGVector?, right: CGVector?) -> CGVector? { + if let left = left, let right = right { + return CGVector(dx: left.dx + right.dx, dy: left.dy + right.dy) + } else { + return nil + } +} + +func -(left: CGPoint, right: CGVector) -> CGPoint { + return CGPoint(x: left.x - right.dx, y: left.y - right.dy) +} + +extension CGPoint { + init(_ vector: CGVector) { + self.init() + x = vector.dx + y = vector.dy + } +} + +extension CGVector { + init(_ point: CGPoint) { + self.init() + dx = point.x + dy = point.y + } + + func applying(_ transform: CGAffineTransform) -> CGVector { + return CGVector(CGPoint(self).applying(transform)) + } + + func rounding(toScale scale: CGFloat) -> CGVector { + return CGVector(dx: CoreGraphics.round(dx * scale) / scale, + dy: CoreGraphics.round(dy * scale) / scale) + } + + var quadrance: CGFloat { + return dx * dx + dy * dy + } + + var normal: CGVector? { + if !(dx.isZero && dy.isZero) { + return CGVector(dx: -dy, dy: dx) + } else { + return nil + } + } + + /// CGVector pointing in the same direction as self, with a length of 1.0 - or nil if the length is zero. + var normalized: CGVector? { + let quadrance = self.quadrance + if quadrance > 0.0 { + return self / sqrt(quadrance) + } else { + return nil + } + } +} + diff --git a/inductionApp/Draw Old App/DrawOnView.swift b/inductionApp/Draw Old App/DrawOnView.swift new file mode 100644 index 00000000..2098c523 --- /dev/null +++ b/inductionApp/Draw Old App/DrawOnView.swift @@ -0,0 +1,407 @@ +// +// DrawOnView.swift +// InductionApp +// +// Created by Ben Altschuler on 4/22/20. +// Copyright © 2020 Josh Breite. All rights reserved. +// + +import Foundation +import UIKit +import UIKit.UIGestureRecognizerSubclass + +class DrawOnView { + + init(view: UIView) { + self.view = view + let dirtyRectView = { () -> UIView in + let newView = UIView(frame: CGRect(x: -10, y: -10, width: 0, height: 0)) + newView.layer.borderColor = UIColor.red.cgColor + newView.layer.borderWidth = 0.5 + newView.isUserInteractionEnabled = false + newView.isHidden = true + view.addSubview(newView) + return newView + } + dirtyRectViews = [dirtyRectView(), dirtyRectView()] + } + + init(){ //context: CGContext + let dirtyRectView = { () -> UIView in + let newView = UIView(frame: CGRect(x: -10, y: -10, width: 0, height: 0)) + newView.layer.borderColor = UIColor.red.cgColor + newView.layer.borderWidth = 0.5 + newView.isUserInteractionEnabled = false + newView.isHidden = true + //view.addSubview(newView) + return newView + } + dirtyRectViews = [dirtyRectView(), dirtyRectView()] + //self.cgContext = context + } + var cgContext: CGContext? + var currentStrokeIndex: Int = 0 + var view: UIView? + //var strokeCollection: StrokeCollection = StrokeCollection() + + + var displayOptions = StrokeViewDisplayOptions.ink { //Part of Class + didSet { + if strokeCollection != nil { + view?.setNeedsDisplay() + } + } + } + + var strokeCollection: StrokeCollection? { + didSet { + if oldValue !== strokeCollection { + view?.setNeedsDisplay() + } + if let lastStroke = strokeCollection?.strokes.last { + + + setNeedsDisplay(for: lastStroke) + } + strokeToDraw = strokeCollection?.activeStroke + } + } + + var strokeToDraw: Stroke? { + didSet { + if oldValue !== strokeToDraw && oldValue != nil { + view?.setNeedsDisplay() + + } else { + if let stroke = strokeToDraw { + setNeedsDisplay(for: stroke) + + } + } + } + } + + let strokeColor = UIColor.black + + private var heldFromSample: StrokeSample? + private var heldFromSampleUnitVector: CGVector? + + private var lockedAzimuthUnitVector: CGVector? + private let azimuthLockAltitudeThreshold = CGFloat.pi / 2.0 * 0.80 // locking azimuth at 80% altitude + + // MARK: - Dirty rect calculation and handling. + var dirtyRectViews: [UIView]! + var lastEstimatedSample: (Int, StrokeSample)? + + + func updateView(view: UIView){ + self.currentStrokeIndex = 0 + self.view = view + } + func dirtyRects(for stroke: Stroke) -> [CGRect] { + var result = [CGRect]() + for range in stroke.updatedRanges() { + var lowerBound = range.lowerBound + if lowerBound > 0 { lowerBound -= 1 } + + if let (index, _) = lastEstimatedSample { + if index < lowerBound { + lowerBound = index + } + } + + let samples = stroke.samples + var upperBound = range.upperBound + if upperBound < samples.count { upperBound += 1 } + let dirtyRect = dirtyRectForSampleStride(stroke.samples[lowerBound..) -> CGRect { + var first = true + var frame = CGRect.zero + for sample in sampleStride { + let sampleFrame = CGRect(origin: sample.location, size: .zero) + if first { + first = false + frame = sampleFrame + } else { + frame = frame.union(sampleFrame) + } + } + let maxStrokeWidth = CGFloat(20.0) + return frame.insetBy(dx: -1 * maxStrokeWidth, dy: -1 * maxStrokeWidth) + } + + //Goes through each rect + func updateDirtyRects(for stroke: Stroke) { + let updateRanges = stroke.updatedRanges() + for (index, dirtyRectView) in dirtyRectViews.enumerated() { + if index < updateRanges.count { + dirtyRectView.alpha = 1.0 + dirtyRectView.frame = dirtyRectForSampleStride(stroke.samples[updateRanges[index]]) + } else { + dirtyRectView.alpha = 0.0 + } + } + } + + func setNeedsDisplay(for stroke: Stroke) { + for dirtyRect in dirtyRects(for: stroke) { + view?.setNeedsDisplay(dirtyRect) + } + } + + func receivedAllUpdatesForStroke(_ stroke: Stroke) { + + setNeedsDisplay(for: stroke) + stroke.clearUpdateInfo() + } + + + func strokeUpdated(strokeGesture: StrokeGestureRecognizer, isPencil: Bool ){ + var stroke: Stroke? + if strokeGesture.state != .cancelled { + stroke = strokeGesture.stroke + if strokeGesture.state == .began || + (strokeGesture.state == .ended && strokeCollection?.activeStroke == nil) { + strokeCollection?.activeStroke = stroke + } + } else { + strokeCollection?.activeStroke = nil + } + + if let stroke = stroke { + if strokeGesture.state == .ended { + if isPencil == true { + // Make sure we get the final stroke update if needed. + stroke.receivedAllNeededUpdatesBlock = { [weak self] in + self?.receivedAllUpdatesForStroke(stroke) + } + } + strokeCollection?.takeActiveStroke() + } + } + //answerCell.strokeCollection = strokeCollection + //self.cGStrokeCollection = answerCell.strokeCollection + } + + func beginDraw(rect: CGRect){ + if let strokeCollection = strokeCollection { + print("FIRRST BEGIN DRAW") + for stroke in strokeCollection.strokes { + draw(stroke: stroke) //, in: rect + } + } + + if let stroke = strokeToDraw { + print("SECOND BEGIN DRAW") + draw(stroke: stroke) //, in: rect + } + } + + func beginDrawContext(){ + print("BEGIN DRAW CONTEXT") + if let strokeCollection = strokeCollection { + for stroke in strokeCollection.strokes { + draw(stroke: stroke) + } + } + + if let stroke = strokeToDraw { + draw(stroke: stroke) + } + } + + func draw(stroke: Stroke) { //, in rect: CGRect + + stroke.clearUpdateInfo() + + guard stroke.samples.isEmpty == false, + let context = UIGraphicsGetCurrentContext() + else { return } + + prepareToDraw() + lineSettings(in: context) + + if stroke.samples.count == 1 { + // Construct a fake segment to draw for a stroke that is only one point. + let sample = stroke.samples.first! + let tempSampleFrom = StrokeSample( + timestamp: sample.timestamp, + location: sample.location + CGVector(dx: -0.5, dy: 0.0), + coalesced: false, + predicted: false, + force: sample.force, + azimuth: sample.azimuth, + altitude: sample.altitude, + estimatedProperties: sample.estimatedProperties, + estimatedPropertiesExpectingUpdates: []) + + let tempSampleTo = StrokeSample( + timestamp: sample.timestamp, + location: sample.location + CGVector(dx: 0.5, dy: 0.0), + coalesced: false, + predicted: false, + force: sample.force, + azimuth: sample.azimuth, + altitude: sample.altitude, + estimatedProperties: + sample.estimatedProperties, + estimatedPropertiesExpectingUpdates: []) + + let segment = StrokeSegment(sample: tempSampleFrom) + segment.advanceWithSample(incomingSample: tempSampleTo) + segment.advanceWithSample(incomingSample: nil) + + draw(segment: segment, in: context) + } else { + for segment in stroke { + draw(segment: segment, in: context) + } + } + + } + + func draw(segment: StrokeSegment, in context: CGContext) { + + guard let toSample = segment.toSample else { return } + + let fromSample: StrokeSample = heldFromSample ?? segment.fromSample + + // Skip line segments that are too short. + if (fromSample.location - toSample.location).quadrance < 0.003 { + if heldFromSample == nil { + heldFromSample = fromSample + heldFromSampleUnitVector = segment.fromSampleUnitNormal + } + return + } + + fillColor(in: context, toSample: toSample, fromSample: fromSample) + draw(segment: segment, in: context, toSample: toSample, fromSample: fromSample) + + if heldFromSample != nil { + heldFromSample = nil + heldFromSampleUnitVector = nil + } + } + + func draw(segment: StrokeSegment, + in context: CGContext, + toSample: StrokeSample, + fromSample: StrokeSample) { + + let forceAccessBlock = self.forceAccessBlock() + + + let unitVector = heldFromSampleUnitVector != nil ? heldFromSampleUnitVector! : segment.fromSampleUnitNormal + let fromUnitVector = unitVector * forceAccessBlock(fromSample) + let toUnitVector = segment.toSampleUnitNormal * forceAccessBlock(toSample) + + let isForceEstimated = fromSample.estimatedProperties.contains(.force) || toSample.estimatedProperties.contains(.force) + if isForceEstimated { + if lastEstimatedSample == nil { + lastEstimatedSample = (segment.fromSampleIndex + 1, toSample) + } + forceEstimatedLineSettings(in: context) + } else { + lineSettings(in: context) + } + + + context.beginPath() + context.addLines(between: [ + fromSample.location , //+ fromUnitVector, + toSample.location ,//+ toUnitVector, + toSample.location ,//- toUnitVector, + fromSample.location //- fromUnitVector //These help with Force. + ]) + + context.closePath() + context.drawPath(using: .fillStroke) + + + + + } + + + + func prepareToDraw() { + lastEstimatedSample = nil + heldFromSample = nil + heldFromSampleUnitVector = nil + lockedAzimuthUnitVector = nil + } + + func lineSettings(in context: CGContext) { + + + context.setLineWidth(2.0) + context.setStrokeColor(strokeColor.cgColor) + + + } + + func forceEstimatedLineSettings(in context: CGContext) { + lineSettings(in: context) + + } + + func azimuthSettings(in context: CGContext) { + context.setLineWidth(1.5) + context.setStrokeColor(#colorLiteral(red: 0, green: 0.7445889711, blue: 1, alpha: 1).cgColor) + } + + func altitudeSettings(in context: CGContext) { + context.setLineWidth(0.5) + context.setStrokeColor(strokeColor.cgColor) + } + + func forceAccessBlock() -> (_ sample: StrokeSample) -> CGFloat { + + var forceMultiplier = CGFloat(2.0) + var forceOffset = CGFloat(0.1) + var forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return sample.forceWithDefault + } + + if displayOptions == .ink { + forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return sample.perpendicularForce + } + } + + let previousGetter = forceAccessBlock + forceAccessBlock = {(sample: StrokeSample) -> CGFloat in + return previousGetter(sample) * forceMultiplier + forceOffset + } + + return forceAccessBlock + } + + func fillColor(in context: CGContext, toSample: StrokeSample, fromSample: StrokeSample) { + let fillColorRegular = UIColor.black.cgColor + let fillColorCoalesced = UIColor.lightGray.cgColor + let fillColorPredicted = UIColor.red.cgColor + context.setFillColor(fillColorRegular) + if toSample.predicted { + context.setFillColor(fillColorRegular) + } + } + +} + + + diff --git a/inductionApp/Draw Old App/DrawView.swift b/inductionApp/Draw Old App/DrawView.swift new file mode 100644 index 00000000..1b6e3954 --- /dev/null +++ b/inductionApp/Draw Old App/DrawView.swift @@ -0,0 +1,104 @@ +//// +//// DrawView.swift +//// InductionApp +//// +//// Created by Ben Altschuler on 4/22/20. +//// Copyright © 2020 Josh Breite. All rights reserved. +//// +// +//import Foundation +//import UIKit +//import UIKit.UIGestureRecognizerSubclass +//class DrawView: UIView, UIGestureRecognizerDelegate{ +// +// var drawingLayer = UIBezierPath() +// let path = CGMutablePath() +// +// var context: CGContext? +// +// +// var fingerStrokeRecognizer: StrokeGestureRecognizer! +// var pencilStrokeRecognizer: StrokeGestureRecognizer! +// +// var currentStrokeIndex: Int = 0 +// //var page: TestPage! +// +// override func layoutSubviews() { +// super.layoutSubviews() +// super.setNeedsDisplay() +// layer.drawsAsynchronously = true +// self.clipsToBounds = true +// self.isMultipleTouchEnabled = false +// +// self.fingerStrokeRecognizer = setupStrokeGestureRecognizer(isForPencil: false) +// self.pencilStrokeRecognizer = setupStrokeGestureRecognizer(isForPencil: true) +// +// } +// +// override func awakeFromNib() { +// super.awakeFromNib() +// +// } +// +// func setupStrokeGestureRecognizer(isForPencil: Bool) -> StrokeGestureRecognizer { +// let recognizer = StrokeGestureRecognizer(target: self, action: #selector(strokeUpdated(_:))) +// recognizer.delegate = self +// recognizer.cancelsTouchesInView = false +// self.addGestureRecognizer(recognizer) +// recognizer.coordinateSpaceView = self +// recognizer.isForPencil = isForPencil +// return recognizer +// } +// +// @objc +// func strokeUpdated(_ strokeGesture: StrokeGestureRecognizer?) { +// debugPrint("UPDATING STROKE For Test Page") +// // if strokeGesture === pencilStrokeRecognizer { +// // tableViewController.lastSeenPencilInteraction = Date() +// // } +// // +// +// var stroke: Stroke? +// if strokeGesture?.state != .cancelled { +// stroke = strokeGesture?.stroke +// if strokeGesture?.state == .began || +// (strokeGesture?.state == .ended && page.drawHelper.strokeCollection?.activeStroke == nil) { +// page.drawHelper.strokeCollection?.activeStroke = stroke +// } +// } else { +// page.drawHelper.strokeCollection?.activeStroke = nil +// } +// +// if let stroke = stroke { +// if strokeGesture?.state == .ended { +// if strokeGesture === pencilStrokeRecognizer { +// // Make sure we get the final stroke update if needed. +// stroke.receivedAllNeededUpdatesBlock = { [weak self] in +// self?.page.drawHelper.receivedAllUpdatesForStroke(stroke) +// } +// } +// page.drawHelper.strokeCollection?.takeActiveStroke() +// } +// } +// page.drawHelper.strokeCollection = page.drawHelper.strokeCollection +// setNeedsDisplay() +// +// } +// +// +// +// override func touchesEnded(_ touches: Set, with event: UIEvent?) { +// print("TOUCH ENDED TEST PAGE:") +// setNeedsDisplay() +// +// +// } +// +// +// override func draw(_ rect: CGRect) { +// debugPrint("DRAWING The Test Page") +// page?.drawHelper.beginDraw(rect: rect) //Draws all the strokes +// +// } +// +//} diff --git a/inductionApp/Draw Old App/StrokeCollection.swift b/inductionApp/Draw Old App/StrokeCollection.swift new file mode 100644 index 00000000..d4da1ef8 --- /dev/null +++ b/inductionApp/Draw Old App/StrokeCollection.swift @@ -0,0 +1,301 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + The Stroke data model and math extensions for CG primitives for easier math. + */ + +import Foundation +import UIKit + +class StrokeCollection { + var strokes: [Stroke] = [] + var activeStroke: Stroke? + + func takeActiveStroke() { + if let stroke = activeStroke { + strokes.append(stroke) + activeStroke = nil + } + } + + func clearCollection(){ + strokes = [] + activeStroke = nil + } +} + +enum StrokePhase { + case began + case changed + case ended + case cancelled +} + +struct StrokeSample { + // Always. + let timestamp: TimeInterval + let location: CGPoint + + // 3D Touch or Pencil. + var force: CGFloat? + + // Pencil only. + var estimatedProperties: UITouch.Properties = [] + var estimatedPropertiesExpectingUpdates: UITouch.Properties = [] + var altitude: CGFloat? + var azimuth: CGFloat? + + var azimuthUnitVector: CGVector { + var vector = CGVector(dx: 1.0, dy: 0.0) + if let azimuth = self.azimuth { + vector = vector.applying(CGAffineTransform(rotationAngle: azimuth)) + } + return vector + } + + init(timestamp: TimeInterval, + location: CGPoint, + coalesced: Bool, + predicted: Bool = false, + force: CGFloat? = nil, + azimuth: CGFloat? = nil, + altitude: CGFloat? = nil, + estimatedProperties: UITouch.Properties = [], + estimatedPropertiesExpectingUpdates: UITouch.Properties = []) { + + self.timestamp = timestamp + self.location = location + self.force = force + self.coalesced = coalesced + self.predicted = predicted + self.altitude = altitude + self.azimuth = azimuth + } + + /// Convenience accessor returns a non-optional (Default: 1.0) + var forceWithDefault: CGFloat { + return force ?? 1.0 + } + + /// Returns the force perpendicular to the screen. The regular pencil force is along the pencil axis. + var perpendicularForce: CGFloat { + let force = forceWithDefault + if let altitude = altitude { + let result = force / CGFloat(sin(Double(altitude))) + return result + } else { + return force + } + } + + // Values for debug display. + let coalesced: Bool + let predicted: Bool +} + +enum StrokeState { + case active + case done + case cancelled +} + +class Stroke { + static let calligraphyFallbackAzimuthUnitVector = CGVector(dx: 1.0, dy: 1.0).normalized! + + var samples: [StrokeSample] = [] + var predictedSamples: [StrokeSample] = [] + var previousPredictedSamples: [StrokeSample]? + var state: StrokeState = .active + var sampleIndicesExpectingUpdates = Set() + var expectsAltitudeAzimuthBackfill = false + var hasUpdatesFromStartTo: Int? + var hasUpdatesAtEndFrom: Int? + + var receivedAllNeededUpdatesBlock: (() -> Void)? + + func add(sample: StrokeSample) -> Int { + let resultIndex = samples.count + if hasUpdatesAtEndFrom == nil { + hasUpdatesAtEndFrom = resultIndex + } + samples.append(sample) + if previousPredictedSamples == nil { + previousPredictedSamples = predictedSamples + } + if sample.estimatedPropertiesExpectingUpdates != [] { + sampleIndicesExpectingUpdates.insert(resultIndex) + } + predictedSamples.removeAll() + return resultIndex + } + + func update(sample: StrokeSample, at index: Int) { + if index == 0 { + hasUpdatesFromStartTo = 0 + } else if hasUpdatesFromStartTo != nil && index == hasUpdatesFromStartTo! + 1 { + hasUpdatesFromStartTo = index + } else if hasUpdatesAtEndFrom == nil || hasUpdatesAtEndFrom! > index { + hasUpdatesAtEndFrom = index + } + samples[index] = sample + sampleIndicesExpectingUpdates.remove(index) + + if sampleIndicesExpectingUpdates.isEmpty { + if let block = receivedAllNeededUpdatesBlock { + receivedAllNeededUpdatesBlock = nil + block() + } + } + } + + func addPredicted(sample: StrokeSample) { + predictedSamples.append(sample) + } + + func clearUpdateInfo() { + hasUpdatesFromStartTo = nil + hasUpdatesAtEndFrom = nil + previousPredictedSamples = nil + } + + func updatedRanges() -> [CountableClosedRange] { + var ranges = [CountableClosedRange]() + + if let hasUpdatesFromStartTo = self.hasUpdatesFromStartTo, + let hasUpdatesAtEndFrom = self.hasUpdatesAtEndFrom { + ranges = [0...(hasUpdatesFromStartTo), hasUpdatesAtEndFrom...(samples.count - 1)] + + } else if let hasUpdatesFromStartTo = self.hasUpdatesFromStartTo { + ranges = [0...(hasUpdatesFromStartTo)] + + } else if let hasUpdatesAtEndFrom = self.hasUpdatesAtEndFrom { + ranges = [(hasUpdatesAtEndFrom)...(samples.count - 1)] + } + + return ranges + } + +} + +extension Stroke: Sequence { + func makeIterator() -> StrokeSegmentIterator { + return StrokeSegmentIterator(stroke: self) + } +} + +private func interpolatedNormalUnitVector(between vector1: CGVector, and vector2: CGVector) -> CGVector { + if let result = (vector1.normal + vector2.normal)?.normalized { + return result + } else { + // This means they resulted in a 0,0 vector, + // in this case one of the incoming vectors is a good result. + if let result = vector1.normalized { + return result + } else if let result = vector2.normalized { + return result + } else { + // This case should not happen. + return CGVector(dx: 1.0, dy: 0.0) + } + } +} + +class StrokeSegment { + var sampleBefore: StrokeSample? + var fromSample: StrokeSample! + var toSample: StrokeSample! + var sampleAfter: StrokeSample? + var fromSampleIndex: Int + + var segmentUnitNormal: CGVector { + return segmentStrokeVector.normal!.normalized! + } + + var fromSampleUnitNormal: CGVector { + return interpolatedNormalUnitVector(between: previousSegmentStrokeVector, and: segmentStrokeVector) + } + + var toSampleUnitNormal: CGVector { + return interpolatedNormalUnitVector(between: segmentStrokeVector, and: nextSegmentStrokeVector) + } + + var previousSegmentStrokeVector: CGVector { + if let sampleBefore = self.sampleBefore { + return fromSample.location - sampleBefore.location + } else { + return segmentStrokeVector + } + } + + var segmentStrokeVector: CGVector { + return toSample.location - fromSample.location + } + + var nextSegmentStrokeVector: CGVector { + if let sampleAfter = self.sampleAfter { + return sampleAfter.location - toSample.location + } else { + return segmentStrokeVector + } + } + + init(sample: StrokeSample) { + self.sampleAfter = sample + self.fromSampleIndex = -2 + } + + @discardableResult + func advanceWithSample(incomingSample: StrokeSample?) -> Bool { + if let sampleAfter = self.sampleAfter { + self.sampleBefore = fromSample + self.fromSample = toSample + self.toSample = sampleAfter + self.sampleAfter = incomingSample + self.fromSampleIndex += 1 + return true + } + return false + } +} + +class StrokeSegmentIterator: IteratorProtocol { + private let stroke: Stroke + private var nextIndex: Int + private let sampleCount: Int + private let predictedSampleCount: Int + private var segment: StrokeSegment! + + init(stroke: Stroke) { + self.stroke = stroke + nextIndex = 1 + sampleCount = stroke.samples.count + predictedSampleCount = stroke.predictedSamples.count + if (predictedSampleCount + sampleCount) > 1 { + segment = StrokeSegment(sample: sampleAt(0)!) + segment.advanceWithSample(incomingSample: sampleAt(1)) + } + } + + func sampleAt(_ index: Int) -> StrokeSample? { + if index < sampleCount { + return stroke.samples[index] + } + let predictedIndex = index - sampleCount + if predictedIndex < predictedSampleCount { + return stroke.predictedSamples[predictedIndex] + } else { + return nil + } + } + + func next() -> StrokeSegment? { + nextIndex += 1 + if let segment = self.segment { + if segment.advanceWithSample(incomingSample: sampleAt(nextIndex)) { + return segment + } + } + return nil + } +} diff --git a/inductionApp/Draw Old App/StrokeGestureRecognizer.swift b/inductionApp/Draw Old App/StrokeGestureRecognizer.swift new file mode 100644 index 00000000..6e568a09 --- /dev/null +++ b/inductionApp/Draw Old App/StrokeGestureRecognizer.swift @@ -0,0 +1,276 @@ +/* + See LICENSE folder for this sample’s licensing information. + + Abstract: + The custom UIGestureRecognizer subclass to capture strokes. + */ + +import UIKit +import UIKit.UIGestureRecognizerSubclass + +/// A custom gesture recognizer that receives touch events and appends data to the stroke sample. +/// - Tag: StrokeGestureRecognizer + +protocol DrawingGestureRecognizerDelegate: class { + func gestureRecognizerBegan(_ location: CGPoint) + func gestureRecognizerMoved(_ location: CGPoint) + func gestureRecognizerEnded(_ location: CGPoint) +} + + +class StrokeGestureRecognizer: UIGestureRecognizer { + // MARK: - Configuration + var collectsCoalescedTouches = true + var usesPredictedSamples = true + + /// A Boolean value that determines whether the gesture recognizer tracks Apple Pencil or finger touches. + /// - Tag: isForPencil + var isForPencil: Bool = false { + didSet { + if isForPencil { + allowedTouchTypes = [UITouch.TouchType.pencil.rawValue as NSNumber] + } else { + allowedTouchTypes = [UITouch.TouchType.direct.rawValue as NSNumber] + } + } + } + + // MARK: - Data + var stroke = Stroke() + var outstandingUpdateIndexes = [Int: (Stroke, Int)]() + var coordinateSpaceView: UIView? + + // MARK: - State + var trackedTouch: UITouch? + var initialTimestamp: TimeInterval? + var collectForce = false + + var fingerStartTimer: Timer? + private let cancellationTimeInterval = TimeInterval(0.1) + + // MARK: -FOR PDF + weak var drawingDelegate: DrawingGestureRecognizerDelegate? + + var ensuredReferenceView: UIView { + if let view = coordinateSpaceView { + return view + } else { + return view! + } + } + + // MARK: - Stroke data collection + + /// Appends touch data to the stroke sample. + /// - Tag: appendTouches + func append(touches: Set, event: UIEvent?) -> Bool { + // Check that we have a touch to append, and that touches + // doesn't contain it. + //debugPrint("Append Function First") + + guard let touchToAppend = trackedTouch, touches.contains(touchToAppend) == true + else { return false } + + // Cancel the stroke recognition if we get a second touch during cancellation period. + if shouldCancelRecognition(touches: touches, touchToAppend: touchToAppend) { + if state == .possible { + state = .failed + } else { + state = .cancelled + } + return false + } + + let view = ensuredReferenceView + if collectsCoalescedTouches { + if let event = event { + let coalescedTouches = event.coalescedTouches(for: touchToAppend)! + let lastIndex = coalescedTouches.count - 1 + for index in 0.., touchToAppend: UITouch) -> Bool { + var shouldCancel = false + for touch in touches { + if touch !== touchToAppend && + touch.timestamp - initialTimestamp! < cancellationTimeInterval { + shouldCancel = true + break + } + } + return shouldCancel + } + + func saveStrokeSample(stroke: Stroke, touch: UITouch, view: UIView, coalesced: Bool, predicted: Bool ) { + // Only collect samples that actually moved in 2D space. + let location = touch.preciseLocation(in: view) + if let previousSample = stroke.samples.last { + if (previousSample.location - location).quadrance < 0.003 { + return + } + } + + var sample = StrokeSample(timestamp: touch.timestamp, + location: location, + coalesced: coalesced, + predicted: predicted, + force: self.collectForce ? touch.force : nil) + + if touch.type == .pencil { + let estimatedProperties = touch.estimatedProperties + sample.estimatedProperties = estimatedProperties + sample.estimatedPropertiesExpectingUpdates = touch.estimatedPropertiesExpectingUpdates + sample.altitude = touch.altitudeAngle + sample.azimuth = touch.azimuthAngle(in: view) + if stroke.samples.isEmpty && + estimatedProperties.contains(.azimuth) { + stroke.expectsAltitudeAzimuthBackfill = true + } else if stroke.expectsAltitudeAzimuthBackfill && + !estimatedProperties.contains(.azimuth) { + for (index, priorSample) in stroke.samples.enumerated() { + var updatedSample = priorSample + if updatedSample.estimatedProperties.contains(.altitude) { + updatedSample.estimatedProperties.remove(.altitude) + updatedSample.altitude = sample.altitude + } + if updatedSample.estimatedProperties.contains(.azimuth) { + updatedSample.estimatedProperties.remove(.azimuth) + updatedSample.azimuth = sample.azimuth + } + stroke.update(sample: updatedSample, at: index) + } + stroke.expectsAltitudeAzimuthBackfill = false + } + } + + if predicted { + stroke.addPredicted(sample: sample) + } else { + let index = stroke.add(sample: sample) + if touch.estimatedPropertiesExpectingUpdates != [] { + if let estimationUpdateIndex = touch.estimationUpdateIndex { + self.outstandingUpdateIndexes[Int(estimationUpdateIndex.intValue)] = (stroke, index) + } + } + } + } + + // MARK: - Touch handling methods + + /// A set of functions that track touches. + /// - Tag: HandleTouches + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + if trackedTouch == nil { + trackedTouch = touches.first + initialTimestamp = trackedTouch?.timestamp + collectForce = trackedTouch!.type == .pencil || view?.traitCollection.forceTouchCapability == .available + if !isForPencil { + // Give other gestures, such as pan and pinch, a chance by + // slightly delaying the `.begin. + fingerStartTimer = Timer.scheduledTimer( + withTimeInterval: cancellationTimeInterval, + repeats: false, + block: { [weak self] (timer) in + guard let strongSelf = self else { return } + if strongSelf.state == .possible { + strongSelf.state = .began + } + }) + } + } + if append(touches: touches, event: event) { +// debugPrint("Appending \(isForPencil)") + if isForPencil { + state = .began + } + } + + let location = trackedTouch?.location(in: self.view) + drawingDelegate?.gestureRecognizerBegan(location!) + + } + + override func touchesMoved(_ touches: Set, with event: UIEvent?) { + if append(touches: touches, event: event) { + if state == .began { + debugPrint("Touch Moved: \(state)") + } + } + guard let location = touches.first?.location(in: self.view) else { return } + drawingDelegate?.gestureRecognizerMoved(location) + + } + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + if append(touches: touches, event: event) { + stroke.state = .done + state = .ended + } + guard let location = touches.first?.location(in: self.view) else { + state = .ended + return + } + drawingDelegate?.gestureRecognizerEnded(location) + } + + override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + if append(touches: touches, event: event) { + debugPrint("Touch Cancelled?") + stroke.state = .cancelled + state = .failed + } + } + + /// Replaces previously estimated touch data with updated touch data. + /// - Tag: estimatedPropertiesUpdated + override func touchesEstimatedPropertiesUpdated(_ touches: Set) { + for touch in touches { + guard let index = touch.estimationUpdateIndex else { + continue + } + if let (stroke, sampleIndex) = outstandingUpdateIndexes[Int(index.intValue)] { + var sample = stroke.samples[sampleIndex] + let expectedUpdates = sample.estimatedPropertiesExpectingUpdates + if expectedUpdates.contains(.force) { + sample.force = touch.force + if !touch.estimatedProperties.contains(.force) { + // Only remove the estimate flag if the new value isn't estimated as well. + sample.estimatedProperties.remove(.force) + } + } + sample.estimatedPropertiesExpectingUpdates = touch.estimatedPropertiesExpectingUpdates + if touch.estimatedPropertiesExpectingUpdates == [] { + outstandingUpdateIndexes.removeValue(forKey: sampleIndex) + } + stroke.update(sample: sample, at: sampleIndex) + } + } + } + + override func reset() { + stroke = Stroke() + trackedTouch = nil + if let timer = fingerStartTimer { + timer.invalidate() + fingerStartTimer = nil + } + super.reset() + } +} diff --git a/inductionApp/Draw/CanvasView.swift b/inductionApp/Draw/CanvasView.swift new file mode 100644 index 00000000..b1393f7a --- /dev/null +++ b/inductionApp/Draw/CanvasView.swift @@ -0,0 +1,23 @@ +// +// CanvasView.swift +// InductionApp +// +// Created by Ben Altschuler on 4/22/20. +// Copyright © 2020 Josh Breite. All rights reserved. +// + +import Foundation +import UIKit + +class CanvasView: UIControl { + var drawingImage: UIImage? + + init(drawingImage: UIImage?){ + self.drawingImage = drawingImage + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} diff --git a/inductionApp/Draw/DrawingPad.swift b/inductionApp/Draw/DrawingPad.swift new file mode 100644 index 00000000..ea2b2c2c --- /dev/null +++ b/inductionApp/Draw/DrawingPad.swift @@ -0,0 +1,23 @@ +// +// DrawingPad.swift +// InductionApp +// +// Created by Ben Altschuler on 4/22/20. +// Copyright © 2020 Josh Breite. All rights reserved. +// + +import SwiftUI + +struct DrawingPad: View { + @State var drawingImage: UIImage? + + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct DrawingPad_Previews: PreviewProvider { + static var previews: some View { + DrawingPad() + } +} diff --git a/inductionApp/Drawing Engine/DrawView.swift b/inductionApp/Drawing Engine/DrawView.swift new file mode 100644 index 00000000..95ddc714 --- /dev/null +++ b/inductionApp/Drawing Engine/DrawView.swift @@ -0,0 +1,104 @@ +// +// DrawView.swift +// InductionApp +// +// Created by Ben Altschuler on 4/22/20. +// Copyright © 2020 Josh Breite. All rights reserved. +// + +import Foundation +import UIKit +import UIKit.UIGestureRecognizerSubclass +class DrawView: UIView, UIGestureRecognizerDelegate{ + + var drawingLayer = UIBezierPath() + let path = CGMutablePath() + + var context: CGContext? + + + var fingerStrokeRecognizer: StrokeGestureRecognizer! + var pencilStrokeRecognizer: StrokeGestureRecognizer! + + var currentStrokeIndex: Int = 0 + var page: TestPage! + + override func layoutSubviews() { + super.layoutSubviews() + super.setNeedsDisplay() + layer.drawsAsynchronously = true + self.clipsToBounds = true + self.isMultipleTouchEnabled = false + + self.fingerStrokeRecognizer = setupStrokeGestureRecognizer(isForPencil: false) + self.pencilStrokeRecognizer = setupStrokeGestureRecognizer(isForPencil: true) + + } + + override func awakeFromNib() { + super.awakeFromNib() + + } + + func setupStrokeGestureRecognizer(isForPencil: Bool) -> StrokeGestureRecognizer { + let recognizer = StrokeGestureRecognizer(target: self, action: #selector(strokeUpdated(_:))) + recognizer.delegate = self + recognizer.cancelsTouchesInView = false + self.addGestureRecognizer(recognizer) + recognizer.coordinateSpaceView = self + recognizer.isForPencil = isForPencil + return recognizer + } + + @objc + func strokeUpdated(_ strokeGesture: StrokeGestureRecognizer?) { + debugPrint("UPDATING STROKE For Test Page") + // if strokeGesture === pencilStrokeRecognizer { + // tableViewController.lastSeenPencilInteraction = Date() + // } + // + + var stroke: Stroke? + if strokeGesture?.state != .cancelled { + stroke = strokeGesture?.stroke + if strokeGesture?.state == .began || + (strokeGesture?.state == .ended && page.drawHelper.strokeCollection?.activeStroke == nil) { + page.drawHelper.strokeCollection?.activeStroke = stroke + } + } else { + page.drawHelper.strokeCollection?.activeStroke = nil + } + + if let stroke = stroke { + if strokeGesture?.state == .ended { + if strokeGesture === pencilStrokeRecognizer { + // Make sure we get the final stroke update if needed. + stroke.receivedAllNeededUpdatesBlock = { [weak self] in + self?.page.drawHelper.receivedAllUpdatesForStroke(stroke) + } + } + page.drawHelper.strokeCollection?.takeActiveStroke() + } + } + page.drawHelper.strokeCollection = page.drawHelper.strokeCollection + setNeedsDisplay() + + } + + + + override func touchesEnded(_ touches: Set, with event: UIEvent?) { + print("TOUCH ENDED TEST PAGE:") + setNeedsDisplay() + + + } + + + override func draw(_ rect: CGRect) { + debugPrint("DRAWING The Test Page") + page?.drawHelper.beginDraw(rect: rect) //Draws all the strokes + + } + +} diff --git a/inductionApp/ContentView.swift b/inductionApp/Login and Signup/ContentView.swift similarity index 100% rename from inductionApp/ContentView.swift rename to inductionApp/Login and Signup/ContentView.swift diff --git a/inductionApp/LoginView.swift b/inductionApp/Login and Signup/LoginView.swift similarity index 100% rename from inductionApp/LoginView.swift rename to inductionApp/Login and Signup/LoginView.swift diff --git a/inductionApp/SceneDelegate.swift b/inductionApp/SceneDelegate.swift index 60907426..b8962025 100644 --- a/inductionApp/SceneDelegate.swift +++ b/inductionApp/SceneDelegate.swift @@ -22,11 +22,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { var manager = UserAuth() // Create the SwiftUI view that provides the window contents. - + // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: AppRootView().environmentObject(manager)) + //window.rootViewController = UIHostingController(rootView: DrawingPad()) self.window = window window.makeKeyAndVisible() } diff --git a/inductionApp/TestView.swift b/inductionApp/TestView.swift index 6f1085db..f8862073 100644 --- a/inductionApp/TestView.swift +++ b/inductionApp/TestView.swift @@ -7,18 +7,19 @@ // import SwiftUI +import PencilKit struct TestView: View { let pages = testPDF().pages - + @Environment(\.undoManager) var undoManager var body: some View { HStack{ AnswerSheetList().frame(width: 300) ScrollView { VStack { - ForEach(pages, id: \.self){ image in - Image(uiImage: image.uiImage).resizable().aspectRatio(contentMode: .fill) + ForEach(pages, id: \.self){ page in + PageView(model: page) } } @@ -27,27 +28,50 @@ struct TestView: View { } } +//struct DrawViewUI: UIViewRepresentable { +// @Binding var drawView: DrawView +// +// func makeUIView(context: Context) -> DrawView { +// return drawView +// } +// +// func updateUIView(_ uiView: PKCanvasView, context: Context) { } +//} +struct PageView: View{ + var model: PageModel + @State private var canvas: PKCanvasView = PKCanvasView() + + var body: some View { + ZStack{ + Image(uiImage: model.uiImage).resizable().aspectRatio(contentMode: .fill) + CanvasRepresentable(canvasToDraw: $canvas) + } + } +} + struct TestView_Previews: PreviewProvider { static var previews: some View { TestView() } } -struct Page: Hashable { - var id: Int - var uiImage: UIImage + + +struct PageModel: Hashable { + var uiImage: UIImage + var id: Int } class testPDF { - var pages = [Page]() + var pages = [PageModel]() init(){ var pageCounter = 1 let path = Bundle.main.path(forResource: "pdf_sat-practice-test-1", ofType: "pdf") let url = URL(fileURLWithPath: path!) while let pdfImage = createUIImage(url: url, page: pageCounter){ - pages.append(Page(id: pageCounter - 1, uiImage: pdfImage)) + pages.append(PageModel(uiImage: pdfImage, id: pageCounter - 1)) pageCounter = pageCounter + 1 if (pageCounter>30){ //TODO: Get rid of this. Figure out why the PDF file is corrupted break diff --git a/inductionApp/UserHomepageView.swift b/inductionApp/UserHomepageView.swift index 86c98e3c..45800d8b 100644 --- a/inductionApp/UserHomepageView.swift +++ b/inductionApp/UserHomepageView.swift @@ -112,6 +112,7 @@ struct UserHomepageView: View { } } }.navigationViewStyle(StackNavigationViewStyle()) + } } From e0b9a36bc55927b7f268fa299676b74a401fedd7 Mon Sep 17 00:00:00 2001 From: Ben Altschuler Date: Thu, 23 Apr 2020 02:02:11 -0400 Subject: [PATCH 2/2] writing on test --- .../UserInterfaceState.xcuserstate | Bin 59141 -> 59257 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/InductionApp.xcworkspace/xcuserdata/baltschuler.xcuserdatad/UserInterfaceState.xcuserstate b/InductionApp.xcworkspace/xcuserdata/baltschuler.xcuserdatad/UserInterfaceState.xcuserstate index 686004d929d33df580de0624e0267da3d7cbc0f9..de46a88790afa6955117bebbafbd762c50ea9055 100644 GIT binary patch delta 9234 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 delta 9256 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 csGxa4%YrrqCB}